lfm-3.1/0000755000175000001440000000000013123766674011176 5ustar inigouserslfm-3.1/etc/0000755000175000001440000000000013123766674011751 5ustar inigouserslfm-3.1/etc/lfm-default.keys0000644000175000001440000000407612611421573015040 0ustar inigousers########## lfm - Last File Manager - Keys ########## [Main] # cursor movement cursor_up: up k cursor_down: down j cursor_pageup: pageup backspace C-p cursor_pagedown: pagedown spc C-n cursor_up10: C-up cursor_down10: C-down cursor_home: home C-a cursor_end: end C-e cursor_goto_file: C-s cursor_goto_file_1char: A-s # change dir dir_up: left dir_enter: right enter goto: g bookmark_goto: b bookmark_set: B bookmark_select_fromlist: C-d history_select_fromlist: C-y # pane & tabs pane_change_focus: tab pane_other_tab_equal: = panes_swap: C-u , panes_cycle_view: . refresh: C-r redraw_screen: A-r dotfiles_toggle: C-h filters_edit: C-f sort_files: s show_dirs_size: # tab_new: : tab_close: ! tab_left: < tab_right: > # selection select: ins select_glob: + deselect_glob: - select_invert: * # files rename_file: F2 view_file: F3 edit_file: F4 copy_file: F5 move_file2: F6 make_dir: F7 delete_file: F8 exec_on_file: @ touch_file: t link_create: l link_edit: L show_file_info: i # general find_grep: / show_tree: C-t main_menu: F9 file_menu: F12 help_menu: h open_shell: C-o toggle_powercli: C-x quit_chdir: q F10 quit_nochdir: C-q lfm-3.1/etc/lfm-default.theme0000644000175000001440000000370412431634211015157 0ustar inigousers########## lfm - Last File Manager - Theme ########## # Format is: item: foreground background # or: item: =previous_item # Valid colors: white, black, red, green, yellow, blue, magenta, cyan # Can use * to intensify a foreground color [Colors] header: yellow blue tab_active: yellow black tab_inactive: =header pane_active: green black pane_inactive: white black pane_header_path: red* black pane_header_titles: white* black statusbar: =header powercli_prompt: blue* black powercli_text: white black selected_files: yellow* black cursor: blue cyan cursor_selected: yellow* cyan files_dir: green black files_exe: red black files_reg: white black files_archive: yellow black files_audio: blue black files_data: magenta* black files_devel: cyan black files_document: blue black files_ebook: =files_document files_graphics: magenta black files_pdf: =files_document files_temp: white black files_web: =files_document files_video: =files_audio dialog: yellow blue dialog_title: yellow* blue button_active: yellow* red button_inactive: =dialog_title dialog_error: black red dialog_error_title: white red dialog_error_text: white* red dialog_perms: green* black selectitem: blue cyan selectitem_title: red cyan selectitem_cursor: yellow blue entryline: yellow* cyan progressbar_fg: black white progressbar_bg: white cyan view_white_on_black: white black view_red_on_black: red black view_blue_on_black: blue black view_green_on_black: green black lfm-3.1/TODO0000644000175000001440000001004113123764303011645 0ustar inigousers============================================================================ TODO for lfm3 Last update: Time-stamp: <2017-06-25 18:45:55 inigo> ============================================================================ . add standalone lfm executable to documentation and web and how to create it Make a standalone lfm executable in your home called "lfm": python3 -m zipapp lfm -m lfm:lfm_start -p /usr/bin/python3 -o ~/lfm but resources (docs, keys, theme) are not stored in .zip. . BB issue #2: common.py file conflict with anaconda New features for v3.x --------------------- Medium Priority: + ui: - new pane views: . horizontal view . multicolumn with only file name, file contents… . quick view . info view + enhancements: - powercli: |: run as tail -f, substitute run sync by default, document + new features: - f12 file menu . folder comparation . folder synchronization . diff and sync dirs: . use filecmp.dircmp or rsync . ui: https://github.com/fourier/ztree/raw/screenshots/screenshots/emacs_diff_xterm.png Low Priority (maybe some day): + ui: - tabs: . dir_enter_new_tab . dir_enter_other_pane . dir_enter_other_pane_new_tab + enhancements: - find/grep . exclude files (f.e. *.o) in findgrep grep --exclude=XXX => NON-POSIX, GNU extension - copy_file: copy block-by-block if file is big + new features: - Allow configuration of custom file types (by extension) and associated program to open them with [BB issue #3, request by Noteworthy] - file encryption/unencryption => gnupg - global copy/cut/paste between tabs or panes - background processes: copy/move/delete - folder monitoring . pyinotify module - new actions menu [f10?] => menu for scripted actions . archive: select target dir, get file prefix name (without version, extension), find other pane in target dir, if found move-otherpane-movecursor else error . remove _: replace _ chars with spc - new vfs: rpm, deb, cpio, xpi, egg, apk, jar… . rpm2cpio PACKAGE | cpio -idmv + other: - use mimetypes module - plugin system Pyview: - relive pyview - tail mode: pyview -f . http://code.activestate.com/recipes/577710-tail-a-continuously-growing-file-like-tail-f-filen/ - use mmap Known Bugs ---------- + general: - after renaming a file, cursor should be placed over the new file name, but this is not always posible because we don't know new file name + vfs.py: - rar with password halts lfm, because process is waiting for a password => => timeout if not output and kill the spawned process - tmpdir are showed in the copy/move/... dialogs or when view/edit/... a file, instead of vfs dir (this is just a minor estetic issue) Some ideas from mc ------------------ Alt + , - switch mc’s layout from left-right to top-bottom. Mind = blown. Useful for operating on files with long names. Alt + t - switch the panel’s listing mode in a loop: default, brief, long, user-defined. “long” is especially useful, because it maximises one panel so that it takes full width of the window and longer filenames fit on screen. Alt + i - synchronize the active panel with the other panel. That is, show the current directory in the other panel. Ctrl + u - swap panels. Alt + o - if the currently selected file is a directory, load that directory on the other panel and move the selection to the next file. If the currently selected file is not a directory, load the parent directory on the other panel and moves the selection to the next file. This is useful for quick checking the contents of a list of directories. Ctrl + PgUp (or just left arrow, if you’ve enabled Lynx-like motion, see later) - move to the parent directory. Alt + Shift + h - show the directory history. Might be easier to navigate than going back one entry at a time. Alt + y - move to the previous directory in history. Alt + u - move to the next directory in history. ============================================================================ lfm-3.1/lfm.10000644000175000001440000000431113123764732012026 0ustar inigousers.\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH lfm "1" "June 25, 2017" "lfm version 3.1" "Last File Manager" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME \fBlfm\fR \- a powerful file manager for the UNIX console .SH SYNOPSIS .BI "lfm [-h] [-d] [-w] [--restore-config] [--restore-keys]" .BI "[--restore-theme] [--delete-history]" .BI "[path1] [path2]" .sp .SH DESCRIPTION .B Last File Manager is a powerful file manager for the UNIX console. It has a curses interface and it's written in Python v3.4+. .SH OPTIONS .TP .B "\-h, \-\-help" Show help and exit .TP .B "\-d, \-\-debug" Enable debug level in log file .TP .B "\-w, \-\-use\-wide\-chars" Enable support for wide chars .TP .B "\-\-restore\-config" Restore default configuration .TP .B "\-\-restore\-keys" Restore default key bindings .TP .B "\-\-restore\-theme" Restore default theme .TP .B "\-\-delete\-history" Delete history .TP .B "[path1]" Path to show in left pane (default: ".", current directory) .TP .B "[path2]" Path to show in right pane (default: ".", current directory) .SH LICENSE This program is distributed under the terms of the GNU General Public License version 3 or later as published by the Free Software Foundation. See the built\-in help for details on the License and the lack of warranty. .SH AVAILABILITY The latest version of this program can be found at https://inigo.katxi.org/devel/lfm. .SH AUTHOR .B lfm was written by Iñigo Serna .PP This manual page was written by Sebastien Bacher for the Debian GNU/Linux system (but may be used by others). Adapted for lfm v3.0 by Iñigo Serna. .SH SEE ALSO The full documentation which includes the keys descriptions is in /usr/share/doc/lfm/README. lfm-3.1/setup.py0000755000175000001440000000460513123766403012706 0ustar inigousers#!/usr/bin/env python3 # -*- coding: utf-8 -*- """lfm v3.1 - (C) 2001-17, by Iñigo Serna 'Last File Manager' is a powerful file manager for UNIX console. It has a curses interface and it's written in Python version 3.4+. Released under GNU Public License, read COPYING file for more details. """ import os from distutils.core import setup from os.path import join from sys import argv, exit, prefix, version_info DOC_FILES = ['COPYING', 'README', 'NEWS', 'TODO'] CONFIG_FILES = ['etc/lfm-default.keys', 'etc/lfm-default.theme'] MAN_FILES = ['lfm.1'] classifiers = """\ Development Status :: 5 - Production/Stable Environment :: Console :: Curses Intended Audience :: End Users/Desktop Intended Audience :: System Administrators License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Natural Language :: English Operating System :: POSIX Operating System :: Unix Programming Language :: Python :: 3 Topic :: Desktop Environment :: File Managers Topic :: System :: Filesystems Topic :: System :: Shells Topic :: System :: System Shells Topic :: Utilities """ print(__doc__) # check python version ver = (version_info.major, version_info.minor) if ver < (3, 4): print('ERROR: Python 3.4 or higher is required to run lfm.') exit(-1) # to avoid bug in pip 7.x. See https://bitbucket.org/pypa/wheel/issues/92 if 'bdist_wheel' in argv: raise RuntimeError("This setup.py does not support wheels") import shutil try: try: os.mkdir('lfm/doc') for f in DOC_FILES: shutil.copy2(f, 'lfm/doc') os.symlink('../etc', 'lfm/etc') except: pass setup(name='lfm', version='3.1', description=__doc__.split("\n")[2], long_description='\n'.join(__doc__.split("\n")[2:]).strip(), author='Iñigo Serna', author_email='inigoserna@gmail.com', url='https://inigo.katxi.org/devel/lfm', platforms='POSIX', keywords=['file manager shell cli'], classifiers=filter(None, classifiers.split("\n")), license='GPL3+', packages=['lfm'], scripts=['lfm/lfm'], data_files=[(join(prefix, 'share/man/man1'), MAN_FILES)], package_data={'': CONFIG_FILES + [join('doc', f) for f in DOC_FILES]}, ) finally: shutil.rmtree('lfm/doc') try: os.unlink('lfm/etc') except IsADirectoryError: pass lfm-3.1/COPYING0000644000175000001440000010451311535256031012217 0ustar inigousers 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 . lfm-3.1/README0000644000175000001440000013056213123765032012050 0ustar inigousers======================= lfm - Last File Manager ======================= :Author: Iñigo Serna, inigoserna AT gmail DOT com :Version: 3.1, June 25th. 2017 :Home page: https://inigo.katxi.org/devel/lfm/ :License: | Copyright © 2001-17, Iñigo Serna | This software has been realised under the `GPL License `_ version 3 or later, read the ``_ file that comes with this package for more information. | There is NO WARRANTY. :Last update: |date| .. meta:: :description: Last File Manager is a powerful file manager for the UNIX console :keywords: lfm, file manager, python, ncurses .. contents:: Table of Contents Introduction ============ **Last File Manager** is a powerful file manager for the UNIX console. It has a curses interface and it's written in Python v3.4+. Some of the features you can find in *lfm*: - console-based file manager for UNIX platforms - 1-pane or 2-pane view - tabs - files filters - bookmarks - history - VFS for compressed files - tree view - dialogs with entry completion - PowerCLI, a command line interface with advanced features - fast access to the shell - direct integration of find/grep, df and other tools - color files by extension [Andrey Skvortsov] - fully customizable themes (colors) - fully customizable key bindings - support for filenames with wide chars, f.e. East Asian - ...and many others Some screenshots: .. table:: **Last File Manager** =============================== =============================== .. image:: lfm-1.png .. image:: lfm-2.png *Tabs and compressed file VFS* *File completion* ------------------------------- ------------------------------- .. image:: lfm-3.png .. image:: lfm-4.png *Tree view* *Edit filter* ------------------------------- ------------------------------- .. image:: lfm-5.png .. image:: lfm-6.png *PowerCLI* *Find & grep* =============================== =============================== When *lfm* starts the first time, it tries to discover the location of some programs in your system to configure itself automatically, but you should take a look to the configuration (`General Menu [F9] -> Edit Configuration [c]`) in case you want to change something. Consult `Customization`_ section for in-depth knowledgement about all the settings and their meaning. Finally, take a look at ``_ file to check known bugs and *not-implemented-yet*\™ features. **Last File Manager** development can be followed in the `BitBucket mercurial repository `_. Download and installation ========================= Requirements ------------ *lfm* is written in `Python `_ language and has a text-based ncurses interface. It should run with Python v3.4 or higher and doesn't need additional modules or any other dependencies, only those provided by the python standard library. For systems with Python v2.x only, use the old version *lfm v2.3*. All modern UNIX flavours (Linux, \*BSD, Solaris, etc) should run it without problems. But note I mostly tested the new 3.x series on Linux. If they appear any issues please notify me. Installation ------------ .. sidebar:: **Files:** all releases :class: warning .. list-table:: :widths: 10 15 10 :header-rows: 1 * - Version - File - Date * - 3.1 - ``_ - 2017/06/25 * - 3.0 - ``_ - 2015/10/23 * - 2.3 - ``_ - 2011/05/21 * - 2.2 - ``_ - 2010/05/22 * - 2.1 - ``_ - 2008/12/21 * - 2.0 - ``_ - 2007/09/03 * - 1.0 - Never released - ~2006 * - 0.91 - ``_ - 2004/07/03 * - 0.9 - ``_ - 2002/09/05 * - 0.8 - ``_ - 2002/03/04 * - 0.7 - ``_ - 2001/11/30 * - 0.5 - ``_ - 2001/08/07 * - 0.4 - ``_ - 2001/07/19 Read about ``_. *lfm* is very easy to install, select one of these options. * First, check if your OS include *lfm* in their repositories. Type as root: ``# dnf install lfm # fedora/redhat/centos/…`` ``# pacman -S lfm # archlinux`` ``# apt-get install lfm # debian/ubuntu/mint/…`` **NOTE 1**: be sure to install *lfm* version 3 or higher, not old version 2.3! **NOTE 2**: `lfm` is not usually included in main distributions repositories. * Using pip: ``$ pip install lfm`` * To install from sources: 1. Download `sources `_ 2. Uncompress file: ``$ tar xvfz lfm-3.1.tar.gz`` 3. Enter the directory and build: ``$ python setup.py build`` 4. Install, as root: ``# python setup.py install`` **WARNING**: Remember *lfm 3.x* requires Python 3.4+. If you have installed both python 2.x and 3.x versions on your system, change ``python`` with ``python3`` and ``pip`` with ``pip3`` above. Now to run it: ``$ lfm`` and to change default settings: `General Menu [F9] -> Edit Configuration [c]` To let *lfm* to change to panel's current directory after quiting with ``q`` or ``F10`` keys, you must add next code to ``/etc/bashrc`` or to your ``~/.bashrc``:: lfm() { /usr/bin/lfm "$@" # type here full path to lfm script LFMPATHFILE=/tmp/lfm-$$.path cd "`cat $LFMPATHFILE`" && rm -f $LFMPATHFILE } If you don't use bash or csh shell, above lines could differ. Upgrading from 2.x to 3.x ------------------------- Some notes about the upgrade process from *lfm* version 2.x to 3.x: - *lfm* 3.0 has been almost completely rewritten from scratch, and it hasn't been tested as much as lfm v2.x series on non-linux OS. - *lfm* 3.x requires python 3.4+, it will not work with python 2.x. - Configuration has moved from the file ``~/.lfmrc`` to the directory ``~/.config/lfm/``. You can delete ``~/.lfmrc`` from your system as it is not used anymore. See section `Customization`_ later. - Also, note that some key bindings and PowerCLI variables have changed since previous versions. Read documentation carefully. - *pyview* —the file viewer— has been removed from *lfm* package, as well as old references to it in configuration, code and documentation. Thus, default viewer has been changed to `less`. See `FAQ`_ entry. More information in the ``_ file. Keys shortcuts ============== In this section you can find the complete list of key shortcuts used in *lfm*. Read `Key bindings`_ section if you want to customize them. Global ------ + **Movement** - up, k - down, j - page_up, backspace, Ctrl-p - page_down, space, Ctrl-n - Ctrl-up: move cursor up 10 - Ctrl-down: move cursor down 10 - home, Ctrl-a: move cursor to first file - end, Ctrl-e: move cursor to last file - Ctrl-s: go to file whose name contains… - Alt-s: go to file whose first letter is… + **Change directory** - left: parent dir - right, enter: enter dir / vfs - g: go to directory… - b: go to bookmark… [0-9a-z] - B: set bookmark… [0-9a-z] - Ctrl-d: select bookmark from menu… - Ctrl-y: select directory from navigation history… + **Panes** - tab: other pane - =: show same directory in both panes - , Ctrl-u: change panes position (left<->right) - .: toggle display 1 or 2 panes - Ctrl-h: toggle show/hide dot-files - Ctrl-f: edit filter for active tab… - s: sort files by… - #: show selected/all directories size - Ctrl-r: refresh contents - Alt-r: redraw screen + **Tabs** - :: new tab - !: close tab - <: go to left tab - >: go to right tab + **Selections** - insert: select item and move cursor to next file - +: select group… - -: deselect group… - \*: invert selection + **Files / Directories operations** - F2: rename file/dir… - F3: view file - F4: edit file - F5: copy file/dir/selection… - F6: move file/dir/selection… - F7: make directory… - F8: delete file/dir/selection - enter: execute file, enter dir / vfs or view 'specially' depending on the extension of the regular file. It is executed in a thread that can be stopped and captures output - @: exec on file… (output is not captured) - t: touch file… - l: create link… - L: edit link… - i: show file info + **Other** - /: find/grep files… - Ctrl-t: tree - Ctrl-o: open shell. Type 'exit' or press Ctrl-d to return to lfm - Ctrl-x: toggle show/hide PowerCLI - F12: file menu - @: exec on file(s) (output is not captured) - i: show file info - p: change file(s) permissions… - o: change file(s) owner and/or group… - a: backup file(s)… - d: diff file with backup - z: Compress/uncompress file(s)… - g: gzip/gunzip - b: bzip2/bunzip2 - x: xz/unxz - l: lzip/lunzip - 4: lz4/unlz4 - x: uncompress .tar.gz, .tar.bz2, .tar.xz, .tar.lz, .tar.lz4, .tar, .zip, .rar, .7z - u: uncompress .tar.gz, etc in other panel - c: compress directory to format… - g: .tar.gz - b: .tar.bz2 - x: .tar.xz - l: .tar.lz - 4: .tar.lz4 - t: .tar - z: .zip - r: .rar - 7: .7z - F9: general menu - /: find/grep file… - #: show directories size - s: sort files by… - t: tree - f: show filesystems info - o: open shell - c: edit configuration - k: edit key bindings file - e: edit theme file - h: delete history - h: help… - q, F10: exit and chdir to current path - Ctrl-q: quit and don't change to current path Dialogs ------- + ***EntryLine* window and *PowerCLI*** - enter: return path or execute command in *PowerCLI* - Ctrl-c, ESC: quit - Ctrl-x: toggle show/hide in *PowerCLI* - insert: toggle insert/overwrite - special: - up, down: history - tab: change to next entry or button, or complete in *PowerCLI* - Ctrl-t: complete… - movement - home, Ctrl-a: move to beginning of line - end, Ctrl-e: move to end of line - left, Ctrl-b: move cursor left - right, Ctrl-f: move cursor right - Ctrl-left, Ctrl-p: move cursor to previous word - Ctrl-right, Ctrl-n: move cursor to next word - deletion - backspace: delete previous character - del: delete character at cursor - Ctrl-w: delete whole line - Ctrl-h: delete from start to cursor position - Ctrl-k: delete from cursor position to end of line - Ctrl-q, Ctrl-backspace: delete until previous word - Ctrl-r, Ctrl-del: delete until next word - insertion - Ctrl-z: restore original content (undo) - Ctrl-v: insert filename at position - Ctrl-s: insert path at position - Ctrl-o: insert other pane tab path at position - Ctrl-d, Ctrl-\: select bookmark at position… - Ctrl-y: select path from navigation history at position… - Ctrl-g: select historic path (not *PowerCLI*)… - Ctrl-g: select historic or stored (from config) command (*PowerCLI*)… + ***SelectItem* window** - [letter]: go to entry whose first char is this - up, k, K - down, j, J - page_up, backspace, Ctrl-b - page_down, space, Ctrl-f - home, Ctrl-a - end, Ctrl-e - Ctrl-l: go to entry in the middle of list - Ctrl-s: go to entry starting by… - enter: return entry - Ctrl-c, q, Q, ESC: quit + ***Permissions* and *Owner/Group* windows** - tab, cursor: move - in permissions: r, w, x, s, t to toggle read, write, exec, setuid or setgid, sticky bit - in user, group: space or enter to select - in recursive: space or enter to toggle - in buttons: space or enter to accept that action - everywhere: space or enter to accept, a to accept all, i to ignore and c, q, esc, Ctrl-c to cancel + ***Tree* panel** - down, j, K: down within current depth, without going out from directory - up, k, K: up within current depth, without going out from directory - page_up, backspace, Ctrl-b: same as up but page-size scroll - page_down, space, Ctrl-f: same as down but page-size scroll - home, Ctrl-a: first directory - end, Ctrl-e: last directory - left: go out from directory - right: enter in directory - enter: return changing to directory - Ctrl-c, q, Q, F10, ESC: quit + ***View* window** - up, k, K - down, j, J - page_up, backspace, Ctrl-b - page_down, space, Ctrl-f - home, Ctrl-a: move cursor to first file - end, Ctrl-e: move cursor to last file - Ctrl-c, q, Q, F3, F10, ESC: quit Some features in detail ======================= Running *lfm* ------------- Type ``lfm --help`` for a complete list of options:: ~$ lfm --help Usage: lfm [-h] [-d] [-w] [--restore-config] [--restore-keys] [--restore-theme] [--delete-history] [path1] [path2] lfm v3.1 - (C) 2001-17, by Iñigo Serna positional arguments: path1 Path to show in left pane (default: ".") path2 Path to show in right pane (default: ".") optional arguments: -h, --help Show this help message and exit -d, --debug Enable debug level in log file -w, --use-wide-chars Enable support for wide chars --restore-config Restore default configuration --restore-keys Restore default key bindings --restore-theme Restore default theme --delete-history Delete history 'Last File Manager' is a powerful file manager for UNIX console. It has a curses interface and it's written in Python version 3.4+. Released under GNU Public License, read COPYING file for more details. As mentioned in the `Installation`_ section, quitting *lfm* with `q` or `F10` keys will leave you in the directory of active tab, if you want to go back to the directory you started *lfm* from, quit the program using `Ctrl-q`. When running *lfm* writes some events to the log file ``~/.config/lfm/lfm.log``. Passing ``-d`` or ``--debug`` to *lfm* increments the verbosity of the logs. Start the program with ``lfm -w`` or ``lfm --use-wide-chars`` to enable the support for East Asian languages. Note you could enable this feature in the configuration permanently. This option is not enabled by default as it makes the program a bit slower. There is an entry in the `FAQ`_ with more information on this regard. Files name encoding ------------------- Since v3.0, *lfm* uses UTF-8 encoding. Since v2.2, *lfm* was rewritten to always use unicode strings internally, but employ terminal encoding (f.e. UTF-8) to interact with the user in input forms, to display contents, and to pass commands to run in shell. When *lfm* detects a file with invalid encoding name it asks the user to convert it (can be automatic with the proper option in the configuration). If not converted, *lfm* will display the file but won't operate on it. Virtual File Systems (VFS) -------------------------- You can navigate inside some special files (known as vfs files in *lfm*) just *entering into* them (press *enter* or *cursor_right* when the cursor bar is over one of these files). By now, supported types are `.tar.gz`, `.tar.bz2`, `.tar.xz`, `.zip`, `.rar`, and `.7z` files. *lfm* even supports navigating nested compressed files (vfs inside vfs)! The virtual directory name (`path_to_vfs_file#vfs/dir`) is not propagated, so the temporary directory (`/tmp/tmpc396zode.lfm/dir`) could be displayed in the copy/move/… dialogs or when view/edit/… a file, but this is just an estetic issue. When returning from one of such vfs files, a question dialog appears asking to allow you to regenerate the vfs file and update all changes (i.e., it is compressed again, so it could be slow in some machines), but *lfm* checks if it can do first, to avoid waste of time. This behaviour (rebuild or not rebuild, ask it or not) can be modified in the configuration file. By default the question is showed but it's set to *not regenerate vfs*. Note that in the case of `panelize` vfs type (vfs with matched files after find/grep), rebuild will cause that all files modifications or deletions be translated to the original directory. So be careful! *lfm* doesn't implement remote vfs such as ssh, ftp, smb, webdav… This is a design criterion, we don't want to add external dependencies beyond python standard library. If you need to access remote file systems you could mount them using something like *fuse* and treat them as local directories from inside *lfm*. Look at the `FAQ`_ section to learn how. Find & grep ----------- You can find and grep for files matching a given pattern (default ``/`` key). Then you can select some actions to perform: - ``go``: chdir to the directory containing the selected file - ``panelize``: create a VFS with matched files - ``view``: view selected file - ``edit``: edit selected file - ``do``: exec on selected file (output is not captured) - ``quit``: quit dialog Note that in the case of `panelize` vfs, rebuild will cause that all files modifications or deletions be translated to the original directory. So be careful! Filters ------- Filters can be used in tabs to hide some files or directories from the view. Use ``Ctrl-f`` to edit current filter. You could see some indication on the frame of pane at the top-right position: for example ``.f`` would mean dotfiles are shown (``.``) and there is an active filter (``f``). The filters are a property of a tab, so they remain active even when chdir. If you what to disable, edit and delete. The default blank filter is the same as ``!*``, i.e. don't hide anything, show all files and directories. Filters can look complex at first sight, but just remember a filter defines the files to hide. They are implemented as `globs`. Some examples: - ``*.png,*.jpg``: hide all PNG and JPEG files - ``*.jpg,!*shot*``: hide all JPEG files except those with 'shot' in the name - ``*,!*py``: hide all except python source files Bookmarks --------- The user can define up to 35 bookmarks, which are associated to characters `0-9`, `a-z`. Upper and lower variant of a letter represent the same bookmark. From the main interface use ``B`` key and then select a letter to set a bookmark for current directory. Later press ``b`` and this same character to go back to the stored path. ``Ctrl-d`` allows to select the bookmark from a list. Note you can insert a bookmark path in `EntryLine` widgets or `PowerCLI`. Move files and directories -------------------------- You can choose between 2 different functions to move files and directories: - ``move_file``: old implementation - ``move_file2``: alternative version using `shtutil.move` instead of copy & delete. Faster but less control of errors. Default Choose the one you prefer and associate it with the `F6` (default key) in the key bindings file. Some historical notes --------------------- **[These comments are probably not necessary nowdays, but I keep them here anyway.]** Since version 0.90, *lfm* needs ncurses >= v5.x to handle terminal resizing. Python v2.5+ and ncurses v5.4+ to use wide characters. Note that python curses module should be linked against ncursesw library (instead of ncurses) to get wide characters support. This is the usual case in later versions of Linux distributions, but maybe not the case in older Linux or other UNIX platforms. Thus, expect problems when using multibyte file names (f.e. UTF-8 or latin-1 encoded) if your curses module isn't compiled against ncursesw. Anyway, I hope this issue will disappear with new releases of those platforms eventually. Consult `Files name encoding`_ section below for more information about support of different encodings. PowerCLI ======== *PowerCLI* is a command line interface with advanced features. To show it press ``Ctrl-x``, and same again to hide, ``ENTER`` to run. Line contents are restored next time PowerCLI is showed. Some features: - uses *EntryLine*, so the same key bindings are available. You can press ``Ctrl-v`` to paste file name for instance - completion (``Ctrl-t`` or ``tab`` key), both for system programs or path files and directories - loops to run the same command for all the selected files - variable substitution - can execute python code - persistent history between sessions - faster than opening a shell (``Ctrl-o``) *lfm* waits until the command is finished, showing output or error. You can stop the command if it seems to run forever. To run a command in background just add a ``&`` at the end of the command. This is useful to open a graphical program and come back to *lfm* quickly. But note you won't get any feedback about the command, even if it has been able to run or not. If the program you want to run needs the terminal (less, vim, emacs -nw…), add ``$`` at the end of the command to let *lfm* know it must temporary free the terminal. Not passing it will fill the screen with garbage. Variables substitution ---------------------- There are a lot of variables you can use to simplify your command typing. Specially useful in loops to apply the same command to many files. - ``$f``: file name including extension - ``$v``: same than ``$f`` - ``$E``: file name without extension - ``$e``: extension - ``$p``: active directory - ``$o``: other pane directory - ``$b#``: path in bookmark # - ``$s``: all selected files, space-separated and enclosed between " - ``$a``: all files, space-separated and enclosed between " - ``$i``: loop index, starting at 1 - ``$tm``: file modification date and time - ``$ta``: file access date and time - ``$tc``: file creation date and time - ``$tn``: now (date and time) - ``$dm``: file modification date - ``$da``: file access date - ``$dc``: file creation date - ``$dn``: now (only date) Python execution ---------------- You can run a subset of python language code in a sandbox, but note this sandbox doesn't allow to import modules or access anything outside for security reasons. But **DON'T TRUST IT'S SECURE**. The sandbox is a very limited environment but powerful enough to satisfy common needs, even you can use the variables inside the code. Code must be enclosed between ``{`` ``}``. You can even use different code chunks in the same command. Consult the examples. Examples -------- * copy current file (or all selected files in a loop) to the other pane path:: cp $f $o * move selected files to path stored in bookmark #3 (no loop):: mv $s "$b3" We have enclosed ``$b3`` between " here in case the path could contain spaces. * show all python files in a directory:: find /to/path -name "*.py" * open current file with `eog` in background and continue inmediately in *lfm*:: eog [Ctrl-v] & * find python files containing some special words in the background and redirect output to a file:: find . -name "*py" -print0 | xargs --null grep -EHcni "TODO|WARNING|FIXME|BUG" > output.txt & Note that if you run a command in the background you won't get any feedback by default, that's why we redirect the output to a file. * edit current file with `vim` in the console:: vim %F $ Note you must end the line with a ``%`` if the command will use the terminal. * convert file (or all selected) to lowercase and change ``.bak`` extension to ``.orig``. F.e., ``FiLeFOO.bak`` => ``filefoo.orig``:: mv $f {$f.lower().replace('.bak', '.orig')} * loop over selected files, copy to the other pane path and rename. F.e., if ``/current/path/img1234.jpeg`` is the 13th file in the selection and was created on 2010/07/22 at 19:43:22 => ``/other/path/13. 20100722194322 - IMG1234.jpg``:: cp $f "$o/{'%2.2d. %s - %s' % ($i, $tm.strftime('%Y%m%d%H%S'), $E.upper())}.jpg" Yes, a stupid convoluted example, but it clearly shows how powerful *PowerCLI* is. Also observe that as the target file name contain spaces, the whole destination must be surrounded with ". Random notes ------------ * Paths or filenames with spaces or special characters must be enclosed between ". Study last example above * Loops are only executed with selected files AND at least one of next variables present within the command: ``$f``, ``$v``, ``$F``, ``$E``, ``$i``, ``$tm``, ``$ta``, ``$tc``. Remember ``$a`` or ``$s`` never loop * Note the differences of running commands with trailing ``&`` vs. ``$`` vs. nothing * If cursor is at the beginning of line, completion will try system programs. If it is in any other position, it will try files or directories first and if nothing is found then programs * Although python code is executed inside a sandbox, it's not completely secure. Anyway, it's the same kind of security issues your system is exposed to when shell access is allowed Customization ============= The configuration of *lfm* is stored across some files in the `~/.config/lfm` directory. This directory is created the first time *lfm* runs, and filled with some files with default settings. To restore default configuration exit from all instances of *lfm* and delete `~/.config/lfm` directory. You could also the command line options to restore default configuration, key bindings, or theme. In next subsections we will discuss the default configuration and the meaning of the different options. Preferences ----------- Program preferences are saved in the ``~/.config/lfm/lfm.ini`` file. To configure *lfm* go to `General Menu [F9] -> Edit Configuration [c]` menu option, or edit this file manually when no instance of the program is running. It contains these parts: Header ~~~~~~ Always the same text. It is used to validate the configuration file:: ########## lfm - Last File Manager Configuration File v3.x ########## [Options] ~~~~~~~~~ Main settings:: # automatic_file_encoding_conversion: never = -1, ask = 0, always = 1 # sort_type: SortType.none, SortType.byName, SortType.byExt, SortType.byPath, SortType.bySize, SortType.byMTime automatic_file_encoding_conversion: 0 detach_terminal_at_exec: 1 find_ignorecase: 1 grep_ignorecase: 1 grep_regex: 1 rebuild_vfs: 0 save_configuration_at_exit: 1 save_history_at_exit: 1 show_dotfiles: 1 show_output_after_exec: 1 sort_mix_cases: 1 sort_mix_dirs: 0 sort_reverse: 0 sort_type: SortType.byName use_wide_chars: 0 * ``automatic_file_encoding_conversion``: Automatically convert filenames when wrong encoding found? Default 0 (no, ask) * ``detach_terminal_at_exec``: Detach terminal at execute? Default 1 (yes) * ``find_ignorecase``: Ignore case in find? Default 0 (no) * ``grep_ignorecase``: Ignore case in grep? Default 1 (yes) * ``grep_regex``: Use regex as grep pattern? Default 1 (yes) * ``rebuild_vfs``: Rebuild vfs? Useful if automatic in confirmations->ask_rebuild_vfs. Default 0 (no) * ``save_configuration_at_exit``: Save configuration at exit? Default 1 (yes) * ``save_history_at_exit``: Save history at exit for future sessions? Default 1 (yes) * ``show_dotfiles``: Show .files? Default 1 (yes) * ``show_output_after_exec``: Show output after exec? Default 1 (yes) * ``sort_mix_cases``: Mix upper and lower case files in sort? Default 1 (yes) * ``sort_mix_dirs``: Mix files and directories in sort? Default 0 (no) * ``sort_reverse``: Reverse sort? Default 0 (no) * ``sort_type``: Sort type. Default SortType.byName (sort by name) * ``use_wide_chars``: Use wide chars? Default 0 (no) [Confirmations] ~~~~~~~~~~~~~~~ These settings indicate whether the user will be prompted in these actions:: ask_rebuild_vfs: 1 delete: 1 overwrite: 1 quit: 1 * ``ask_rebuild_vfs``: when abandoning compressed files, prompt if we should rebuild the file in case we've modified contents. Note that in `find/grep panelize` (vfs with matched files) if rebuild, all files modifications or deletions are translated to original directory. So be careful! [Misc] ~~~~~~ Settings which require a string value:: # diff_type: context, unified, ndiff backup_extension: .bak diff_type: unified * ``backup_extension``: Backup file extension? Default .bak * ``diff_type``: Diff output format? Default unified [Programs] ~~~~~~~~~~ Default programs *lfm* uses to open common file types:: audio: vlc ebook: FBReader editor: vi graphics: eog pager: less pdf: evince shell: bash video: vlc web: firefox The applications listed here must be executable programs in your `$PATH`, shell alias will not work. [Files] ~~~~~~~ File extensions associated with default programs. Used to color files too. See previous subsection:: archive: 7z, arc, arj, ark, bz2, cab, deb, gz, lha, lzh, rar, rpm, tar, tbz2, tgz, txz, xz, z, zip, zoo audio: au, flac, mid, midi, mp2, mp3, mpg, ogg, wma, xm data: cdx, dat, db, dbf, dbi, dbx, dta, fox, mdb, mdn, mdx, msql, mssql, nc, pgsql, sql, sqlite, ssql devel: ada, asm, awk, bash, c, caml, cc, cgi, cpp, css, diff, el, f, f90, glade, h, hh, hpp, hs, inc, jasm, jav, java, js, lua, m, m4, mak, ml, mli, mll, mlp, mly, pas, pas, patch, php, phps, pl, pm, pov, prg, py, pyw, rb, sh, sl, st, tcl, tk, ui, vala document: 1, abw, bib, djvu, doc, docx, dtd, dvi, gnumeric, ics, info, letter, lsm, mail, man, msg, odc, odp, odt, po, pps, ppt, pptx, rtf, sdc, sdp, sdw, sgml, sxc, sxp, sxw, tex, text, txt, vcard, vcs, xls, xlsx, xml, xsd, xslt ebook: azw, azw3, chm, epub, fb2, imp, lit, mobi, prc graphics: ai, bmp, cdr, dia, dwb, dwg, dxf, eps, gif, ico, jpeg, jpg, omf, pcx, pic, png, rle, svg, tif, tiff, wmf, xbm, xcf, xpm pdf: pdf, ps temp: $$$, bak, tmp, ~ video: acc, asf, avi, flv, med, mkv, mol, mov, mp4, mpeg, mpg, mpl, ogv, ogv, swf, wmv web: htm, html, shtml [Bookmarks] ~~~~~~~~~~~ User-defined 35 bookmarks (0-9, a-z). No differences between upper and lower character, as they represent the same bookmark. ``/`` initially:: 0: / 1: / . . . 8: / 9: / a: / b: / . . . y: / z: / [PowerCLI Favs] ~~~~~~~~~~~~~~~~~~~ User-defined 10 favourite PowerCLI stored commands:: 0: mv "$f" "{$f.replace('', '')}" 1: less "$f" % 2: find "$d" -name "*" -print0 | xargs --null grep -EHcni "TODO|WARNING|FIXME|BUG" 3: find "$d" -name "*" -print0 | xargs --null grep -EHcni "TODO|WARNING|FIXME|BUG" >output.txt & 4: cp $s "$o" 5: 6: 7: 8: 9: Key bindings ------------ The currently used key bindings for the main user interface are stored in the ``~/.config/lfm/lfm.keys`` file. To customize select `General Menu [F9] -> Edit keys [k]` from the program or if you edit the file directly be sure no instance of *lfm* is running. Currently, it is not possible to modify the key bindings for the dialogs. The format is:: : key_combination_1 key_combination_2 … Something like ``C-up`` means `Control` & `cursor up` keys pressed simultaneously and ``A-s`` means `Alt` & `s` keys pressed simultaneously. If the definition contains 2 or more bindings, all of them could be used, as is the case with ``C-u`` and ``,`` for `panes_swap` action below. Consult `Keys shortcuts`_ section for more information. Default key bindings:: ########## lfm - Last File Manager - Keys ########## [Main] # cursor movement cursor_up: up k cursor_down: down j cursor_pageup: pageup backspace C-p cursor_pagedown: pagedown spc C-n cursor_up10: C-up cursor_down10: C-down cursor_home: home C-a cursor_end: end C-e cursor_goto_file: C-s cursor_goto_file_1char: A-s # change dir dir_up: left dir_enter: right enter goto: g bookmark_goto: b bookmark_set: B bookmark_select_fromlist: C-d history_select_fromlist: C-y # pane & tabs pane_change_focus: tab pane_other_tab_equal: = panes_swap: C-u , panes_cycle_view: . refresh: C-r redraw_screen: A-r dotfiles_toggle: C-h filters_edit: C-f sort_files: s show_dirs_size: # tab_new: : tab_close: ! tab_left: < tab_right: > # selection select: ins select_glob: + deselect_glob: - select_invert: * # files rename_file: F2 view_file: F3 edit_file: F4 copy_file: F5 move_file2: F6 make_dir: F7 delete_file: F8 exec_on_file: @ touch_file: t link_create: l link_edit: L show_file_info: i # general find_grep: / show_tree: C-t main_menu: F9 file_menu: F12 help_menu: h open_shell: C-o toggle_powercli: C-x quit_chdir: q F10 quit_nochdir: C-q Color themes ------------ The current theme is stored in the ``~/.config/lfm/lfm.theme`` file, where you can adapt the user interface colors to your likings. To customize select `General Menu [F9] -> Edit theme [e]` from the program or if you edit the file directly be sure no instance of *lfm* is running. To edit this file be sure no instance of *lfm* is running. Each entry represents a different entity. The format is:: : foreground_color background_color or to make an entity adopt the same colors as other previous one:: : = Valid colors are: white, black, red, green, yellow, blue, magenta, cyan. Can use * before a foreground color to intensify. Default theme is defined as:: ########## lfm - Last File Manager - Theme ########## # Format is: item: foreground background # or: item: =previous_item # Valid colors: white, black, red, green, yellow, blue, magenta, cyan # Can use * to intensify a foreground color [Colors] header: yellow blue tab_active: yellow black tab_inactive: =header pane_active: green black pane_inactive: white black pane_header_path: red* black pane_header_titles: white* black statusbar: =header powercli_prompt: blue* black powercli_text: white black selected_files: yellow* black cursor: blue cyan cursor_selected: yellow* cyan files_dir: green black files_exe: red black files_reg: white black files_archive: yellow black files_audio: blue black files_data: magenta* black files_devel: cyan black files_document: blue black files_ebook: =files_document files_graphics: magenta black files_pdf: =files_document files_temp: white black files_web: =files_document files_video: =files_audio dialog: yellow blue dialog_title: yellow* blue button_active: yellow* red button_inactive: =dialog_title dialog_error: black red dialog_error_title: white red dialog_error_text: white* red dialog_perms: green* black selectitem: blue cyan selectitem_title: red cyan selectitem_cursor: yellow blue entryline: yellow* cyan progressbar_fg: black white progressbar_bg: white cyan view_white_on_black: white black view_red_on_black: red black view_blue_on_black: blue black view_green_on_black: green black FAQ === **How and why lfm born?** Everything is explained in next sections. `list.com` and `midnight commander` were the muses who guided. **Isn't python slow? why develop lfm on python?** No. It's fast enough. And programming in python is funny. **Does it work with Python v2.x?** Not anymore. lfm v3.x is written for Python 3.4+. If you only have Python 2.x please use old lfm v2.3. **lfm does not change to current directory after quiting** This can't be made inside the program, but you could get it using the shell tip mentioned `Installation`_ section. **lfm does not start, shows the message "Terminal to narrow to show contents"** **lfm shows the message "Terminal to narrow to show contents" and quits when resizing** lfm needs a terminal with 66 columns as mininum. If the terminal is narrower or you resize it to fewer columns program will stop inmediately. **Why doesn't lfm implement remote vfs such as ssh, ftp, smb, webdav, ...?** One of the design goals for *lfm* is simplicity, we don't want to add external dependencies beyond python standard library. Nevertheless you can use something like *fuse* to mount those remote volumes anyway. To use fuse with ssh you need *fuse* and *sshfs* packages installed on your system:: $ mkdir /mount/point/for_ssh_server $ sshfs user@ip_or_hostname:/path /mount/point/for_ssh_server For ftp you need *fuse* and *curlftpfs*:: $ mkdir /mount/point/for_ftp_server $ curlftpfs ftp://user:password@ip_or_hostname /mount/point/for_ftp_server For webdav you need *fuse* and *wdfs* or davfs2 (non fuse based):: $ mkdir /mount/point/for_webdav_server $ wdfs https://user:password@server.org/webdav_dir /mount/point/for_webdav_server For smb take a look at *fuse-smb*. And to umount:: $ fusermount -u /mount/point $ rm -rf /mount/point **Request: add advanced file rename tool** Use *PowerCLI*, it's much... uhmmm... powerful! **I don't like the colors of the interface. Can I change the theme?** Yes!!! lfm v3.x supports color personalization, but only one default theme is provided. Customize colors in the file `~/.config/lfm/lfm.theme`. More information in the section `Color themes`_. And please share your creations. **Key bindings customization?** Yes!!! lfm v3.x supports key bindings personalization. Customize them in the file `~/.config/lfm/lfm.keys`. More information in the section `Key bindings`_. **Some Chinese, Japanese or Korean files make lfm look ugly or even crash** Start the program as ``lfm -w`` or enable it by default setting an option in the configuration file: ``use_wide_chars: 1`` in section ``[Options]`` (see `[Options]`_ above). This option is not enabled by default as it makes the program slower. The characters of these languages can span over 1 or 2 cells, so it's not possible for *lfm* to guess the real width they need, it must be calculated for every string to show. **I can't find pyview anymore** Starting with version 3.0, *pyview* has been removed from *lfm* package, and now ``less`` is used as the default file viewer/pager. Nowdays I use ``emacs`` for almost everything, even as my default file viewer. You can emulate old *pyview* features easily just adding next configuration to your ``.emacs`` file:: [...] (defun eless (&rest args_str) (interactive) (let ((args (pop args_str))) (if (string-match "\\+\\([0-9]+\\)\s+\\(.+\\)" args) (let* ((line (string-to-number (match-string 1 args))) (file (match-string 2 args))) (view-file file) (goto-line line)) (view-file args)))) (add-hook 'view-mode-hook '(lambda () (define-key view-mode-map "q" 'kill-emacs) (define-key view-mode-map '[f3] 'kill-emacs))) (add-hook 'hexl-mode-hook '(lambda () (define-key hexl-mode-map '[f4] 'hexl-mode-exit))) (global-set-key '[f2] 'toggle-truncate-lines) (global-set-key '[f3] 'view-mode) (global-set-key '[f4] 'hexl-mode) (global-set-key "\C-cn" 'linum-mode) [...] create a new executable program ``ve`` with these contents and move it to any directory in your `$PATH`:: emacs -nw --eval "(eless \"$*\")" and finally set ``ve`` as your viewer in *lfm* configuration: `pager` entry under `[Options]` section in ``~/.config/lfm/lfm.ini`` file. Of course you can substitute that ``emacs`` call with ``emacsclient`` and adapt the code if you run ``emacs`` as daemon. If you prefer ``vim`` create the ``ve`` file with something like:: vim -u /usr/share/vim/vimXX/macros/less.vim "$*" where `XX` is the vim version you have, for example `74` for vim 7.4. In any case note that ``ve`` must be an executable program in your `$PATH`, a shell alias will not work. **Mouse support? UI to configure settings?** I'm afraid we speak different languages. **When will be support for internationalization?** If we are talking about translating *lfm*, the answer is mostly never. Ncurses programming makes very difficult to control the length of every text for every possible language translation. If you mean support for file names in foreign languages and encodings then it's almost here already. **[Any other question / feature request]** Consult if it's mentioned in the ``_ file and/or send me an email. History ======= Many many years ago I began to write a program like this in C, but after some weeks of coding I never finished it… I'm too lazy, yes. Then I saw the light and I started writing *lfm* to learn Python. Code evolved and application got more and more features, used by many people around the world on different UNIX systems. But after the release of version 0.91 (June 2004) they were not more releases. Not that I had stopped working on *lfm*, new code was written, tested, rewritten again… silently… different reasons made me to postpone public releases… refactoring, a new essential feature, source cleaning, a wedding, a child, ahem… code refactoring… Anyway, from now on I'll do my best to release often. Thanks ====== Thanks are obviously due to the whole python community, specially to GvR (of course! ;-) and all the people who answered my questions in c.l.p. It's a great pleasure to code in a language like this. Alexei Gilchrist, for his cfm program from which I took some ideas. `Midnight Commander `_ developers, whose program was the mirror. `Vernon D. Buerg's list.com `_, the best program ever coded (well, just after emacs ;-). Added 2012/06/19: I've just read Buerg died on Dec. 30, 2009. RIP. And also to all the people who have contributed with ideas, reporting bugs and code over these years: Antoni Aloy, Sebastien Bacher, Grigory Bakunov, Greg Bell, Jean-François Bercher, Luigi M. Bianchi, Hunter Blanks, Josef Boehm, Witold Bołt, Fabian Braennstroem, Jason Buberel, Ondrej Certik, Kevin Coyner, Tim Daneliuk, Mike Dean, Arnå DG, Maximilian Dietrich, Christian Eichert, Steve Emms, Murat Erten, Daniel Echeverry, Luca Falavigna, Stephen R. Figgins, f1ufx, Roy Fullmer, Francisco Gama, Vlad Glagolev, Ana Beatriz Guerrero Lopez, Kelly Hopkins, Laurent Humblet, Ibu, Tjabo Kloppenburg, Zoran Kolic, Shantanu Kulkarni, Kurka, Max Kutny, Karol M. Langner, Yu-Jie Lin, Martin Lüethi, Thomas Marsaleix, Mateusz Matejuk, Maurício, James Mills, Oliver Mueller, Bartosz Oler, Piotr Ozarowski, Mikhail A. Pokidko, Jerome Prudent, Mikhail Ramendik, Rod, Daniel T. Schmitt, Chengqi Song, Robin Siebler, Andrey Skvortsov, Espartaco Smith, Jörg Sonnenberger, Jonathan Steel, Martin Steigerwald, Wayne Tan, Joshua Tasker, Tim Terlegård, Jean Terrier, Edd Thompson, Sergey Tkachenko, E.R. Uber, Viktor Vad, Walter van den Broek, Jesper Vestergaard, Xin Wang, Alejandro Weil, Yellowprotoss, Hai Zaar and many others… You have made posible to run *lfm* in all those platforms! .. |date| date:: %a, %d %b %Y - %H:%M:%S lfm-3.1/PKG-INFO0000644000175000001440000000224413123766674012275 0ustar inigousersMetadata-Version: 1.1 Name: lfm Version: 3.1 Summary: 'Last File Manager' is a powerful file manager for UNIX console. Home-page: https://inigo.katxi.org/devel/lfm Author: Iñigo Serna Author-email: inigoserna@gmail.com License: GPL3+ Description: 'Last File Manager' is a powerful file manager for UNIX console. It has a curses interface and it's written in Python version 3.4+. Released under GNU Public License, read COPYING file for more details. Keywords: file manager shell cli Platform: POSIX Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Desktop Environment :: File Managers Classifier: Topic :: System :: Filesystems Classifier: Topic :: System :: Shells Classifier: Topic :: System :: System Shells Classifier: Topic :: Utilities lfm-3.1/NEWS0000644000175000001440000005120213123764527011670 0ustar inigousersVersion 3.1 ("Recovering inertia") - 2017/06/25: + New features - support for new compression programs: lzip, lz4 + Changes & improvements: - Improved resources files (docs, default keys and theme) installation and loading at runtime + Fixes: - when installing data files with pip >7.0 & wheel. Thanks to Jean-François Bercher - PowerCLI: error message is always shown when command finishes. Thanks to Jean Terrier for the report - filters: glob when parent dir contains special chars (BB issue #5). Thanks to Viktor Vad for the report - lfm crashes when terminal is too narrow (BB issue #4). Thanks to Wayne Tan for the report Version 3.0 ("Only you") - 2015/10/23: + About the code - almost completely rewritten from scratch . it hasn't been tested as much as lfm v2.x series on non-linux OS - requires Python v3.4+ - pyview, the file viewer, has been removed from lfm package - configuration location has changed to a new directory and files: ~/.config/lfm/{lfm.ini, lfm.keys, lfm.theme, lfm.history} + New features - fully customizable themes (colors) - fully customizable key bindings . allow Alt-key shorcuts (A-) . only for main window (not for dialogs) - files filters (using globs) . information in pane frame: ".F" => show dotfiles, active filters . filters are a property of a tab, they remain active even when chdir . Ctrl-f: edit current filter . Some examples: . "*.png,*.jpg" => hide all PNG and JPEG files . "*.jpg,!*shot*" => hide all JPEG files except those with 'shot' in the name . "*,!*py" => hide all except python source files - up to 35 bookmarks (0-9, a-z) . b: go to bookmark, B: set bookmark, C-d: select bookmark . fix: don't delete bookmark at start if path does not exist - nested archive handling (vfs inside vfs) works now - added optional support for filenames with wide chars, f.e. East Asian . to enable, set 'use_wide_chars' flag in configuration or use -w command line flag . it's disabled by default for performance . it's not perfect, but it mostly works - there are 2 different versions of move_file to chose from in the key bindings file: . move_file: old implementation . move_file2: alternative version using shtutil.move instead of copy & delete. Faster but less control of errors - new action: redraw screen (default key A-r) + Changes & improvements (vs v2.x): - chmod & chown/chgrp are 2 different actions now - cursor_goto_file (C-s): find text pattern (no regex or glob) in the whole file name, not at the beginning as v2.x - cursor_goto_file_1char (A-s): go to file by 1st letter of name (old C-s behaviour) - bookmarks have new key bindings: b: go to bookmark, B: set bookmark, C-d: select bookmark - PowerCLI: . ending command with % must be $ now . added new date variables: dm, dc, da, dn . old $d variable is $p now - find/grep: panelize = create vfs with matched files . if rebuild: all files modifications or deletions are translated to original directory so be careful! - pyview, the file viewer, has been removed from lfm package + Fixes in v3.0 (vs v2.x): - nested archive handling (vfs inside vfs) works now - find & grep with spaces in file name - wide chars file names support (f.e. Eastern languages) => lfm -w - sort by size after show dirs size - move_file: not overwritten files in destination are not deleted Version 2.3 ("Wow, less than a year!") - 2011/05/21: + About the code - lfm needs python version 2.5 or upper now + New features - PowerCLI, an advanced command line interface with completion, persistent history, variable substitution and many other useful features. As this is a very powerful tool, read the documentation for examples - history . use different types of history lists: path, file, glob, grep, exec, cli for the different forms and actions . persistent history between sessions => ~/.lfm_history . controlled by a flag in configuration - find/grep . configuration options for ignorecase and regex . sort results . show results as FILE:lineno . much faster - show diff between xxx.orig and xxx files - tar files compress/uncompress - messages.EntryLine has been rewritten, with many new key shorcuts. This is the core behind most of the forms lfm shows when asking for anything. Consult the documentation + Minor changes - reorganize "un/compress file" and "compress directory xxx" in file_menu - config: sort entries when saving - improve lad/save handling of new options not present in ~/.lfmrc - added new extensions - messages.error rewritten to offer better messages - added some new key shortcuts messages.SelectItem + Documentation - added a note about python v2.5+ is needed from now on - 'lfm' shell function: change "$*" to "$@" to properly handle paths containg spaces - FAQ: added information about fuse to mount ssh, ftp, smb and webdav - reorganized and fixed key bindings section - documented .lfmrc contents - added link to public BitBucket repository + lots of bugs fixed: - pyview: . last char is not shown if file size is small . last line and wrap: cursor_down or page_next . when number of lines == window height - ncurses v5.8 doesn't accept 0 as width or height - UI crashes: . time string could contain non-ascii characters (reported by Martin Steigerwald) . when filenane length is large in full pane mode . MenuWin, SelectItem: ellipsize entries if bigger than screen width - find or find&grep: . pass "-type f" to find as ".#filename" are temporary emacs files/links that break search . show wrong matches if results contain directories or files with spaces . file->goto_file: move to correct page - copy/move "/file" to "/anydir/anyplace" fails, trying to copy/move to "/" - executing non-ascii programname or args - convoluted issue with link to directory in corner cases (reported by Xin Wang) - rename/backup ".." crashes - we should not compress ".." - create_link, edit_link: don't show error if canceled - only store one copy of the same entry in history - tree: "disable" colors of active panel, "enable" at end - Config.save: work with unicode, only convert to encoding when saving Version 2.2 ("Approaching perfection") - 2010/05/22: + New features - use 2 progress bars in copy/move/delete dialog, one for files count and other for files size - added recursive chmod chown chgrp - faster cursor movement . Ctrl-l: center cursor in panel, so now edit-link is in 'L' . Ctrl-cursor_up, Ctrl-P: move cursor 1/4th of page up . Ctrl-cursor_down, Ctrl-N: move cursor 1/4th of page down . P: move cursor 1/4th of page up in other panel . N: move cursor 1/4th of page down in other panel - file_menu new feature: a -> backup file. You can specify the extension to use in .lfmrc - added support for .xz compressed files - Unicode & Encodings . rewrite all internals to use unicode strings, but employ terminal encoding (f.e. utf-8) to interact with the user or to display contents in ncurses functions or to run commands in shell . when lfm detects a file with invalid encoding name it asks the user to convert it (can be automatic with the proper option in the configuration, automatic_file_encoding_conversion, default 0 (ask)). If not converted, lfm will display the file but won't operate on it. . try more encodings when we get a filename with strange characters . lfm will check and require a valid encoding before running - Pyview: . completely rewritten. Code is shorter and more beautiful now Uses a new FileCache class to accelerate the retrieving of file lines . displays contents between 2 and 4 times faster . new command line flag -s/--stdin to force reading from stdin. Now pyview doesn't wait for stdin input by default, so it starts much faster. eg. $ ps efax | pyview -s + Minor changes - add color entries for directories and exe_files - expand ~ to user home - make Tree follow .dotfiles behaviour, new keybinding Ctrl-H - dialogs are bigger now - show filesystem info rewritten - show file info rewritten, now it shows correctly information from fuse-mounted volumes - added new "ebook" category, filetypes and formats + About the code - since python v2.6+, popen* is deprecated, so make lfm check python version and use popen* or subprocess accordingly - correct some python idioms - clean code + Documentation - Added "Files Name Encoding" and "FAQ" sections - Added information about keybindings in permissions window - Updated some other minor changes: wide char support, vfs, thanks + lots of bugs fixed: - file system information was not showed correctly sometimes - devices major and minor numbers were not showed correctly - crash in goto_dir if there aren't any historic entries - crash in a void EntryLine after pressing BACKSPACE on some platforms - unzip => overwrite files without prompting ("unzip -o" option) to avoid ethernal waiting, as messages can not be seen by user - fix make_dir error message - recompressing a vfs compressed file leave some garbage on temporary dir - don't try to copy fifo/socket/block-dev/char-dev files - crash when we don't have enough permissions to write to dest - show_dirs_size: don't show in stderr if we don't have perms for a dir - can't browse /home/ as root if .gvfs is present - EntryLine: non-ascii chars are not showed correctly - lfm crashes with invalid encoding filenames - increment owner and group space to avoid ugly look in 1-pane view - when moving files, don't delete source if some error or if we don't overwrite destination Version 2.1 ("What do you want for Christimas?") - 2008/12/21: + Ctrl-H now show/hide dot files + Ctrl-Y display directories history + It's now posible to move the cursor in the non-active pane Consult the documentation for available keys and actions This behaviour is de/activated with Ctrl-W + added support for .7z compressed files + swapped F2 and F12 keys, now F2 rename files and F12 show file menu + new key shortcuts in dialogs. Read docs + speed up cursor movement + lots of code cleaning and refactoring + and fixed lot of bugs, some of them: - setup.py: change Iñigo for Inigo to avoid problems when installing - sorting by None doesn't crash anymore - MenuWin dialog crashed when title length was greater than length of entries to show Version 2.0 ("Nine 1/2 weeks... ok, ok, and 3 years") - 2007/09/03: + tabs implemented + color files by extension [Andrey Skvortsov] + new IPC code and API; more flexible, powerful and stable + new un/compress vfs API, added support for .zip and .rar files + make sort mode per tab, not globally + support locale [Andrey Skvortsov] + speed up loading directory contents + speed cursor movement, don't waste much CPU + use logging module in lfm and pyview for debugging + overwrite_all_none: yes, all, no => new options: "none", "skip all"" + rewrite/refactor most of code to make lfm more robust and clean + preferences: - change file name preferences.py => config.py - use ConfigParser + use tempfile secure versions mkdtemp() and mkstemp() + added man pages [Sebastien Bacher] + use reST for documentation + check for python version 2.3 or higher in lfm and pyview + upgraded to GPL v3 license + and fixed lot of bugs, some of them: - general: . delete garbage if user stops action . run 'do_special_view_file' as dettached from lfm window . path expand in bookmarks ("~/") [Andrey Skvortsov] . an ugly traceback crash appears when user starts "lfm path" and has no permissions to enter. Show error message and default to current directory . lfm crashes when filename is not encoded with same codec than g_encoding utils.{decode|encode}. Needs curses module linked against ncursesw to work properly . sort_mix_cases = 1 performance degrades on larger dirs. Reported by Andrey Skvortsov . escape filenames with chars $ ". Reported by Andrey Skvortsov - user interface: . maximize/minimize window don't crash lfm anymore . dialogs appear at bad position after terminal is resized . handle window resize in Tree mode . refresh display after canceling completion dialog . "the size of the right pane does not fill the last column in terminal if their number is odd" [Andrey Skvortsov] . fix crash when "df" shows entries in two different lines (device name is too large, f.e. in linux lvm2 volumes) . if you try to enter a directory with insufficient permissions, after the error message is closed the cursorline refreshes to the first line - compress: . added -i flag (--ignore-zeros) flag to tar [Andrey Skvortsov] . standard tar needs - for flags - vfs: . vfs.py: regenerate_file, if user stops process, tempfile can't be deleted - find/grep: . escape special chars (- \ ( ) [ ]) in patterns . don't crash when find/grep returns no results . bug when matches occur in binary files - pyview: . goto line 0 in pyview showed a blank screen . crash in file info if filename is too long Version 1.0 was never publically released - 2006 Version 0.92 was never publically released - 2005 Version 0.91 ("It rocks... yeah!") - 2004/06/30: + quite stable and robust, doesn't crash + faster + new option: show_dotfiles flag + new option: detach_terminal_at_exec flag: useful f.e. if you want to run elinks as web browser attached to lfm terminal + file associations and applications can be configured in preferences + now each application has only 1 associated program, *breaking old .lfmrc* + perms dialog: users & groups sorted alphabetically + uncompress in other panel + resizing terminal works in lfm, pyview. Be careful with dialogs + columns size eliminated from preferences + 1-panel view redesigned + ESC closes dialogs, not lfm or pyview + Ctrl-D: select bookmark dialog + code reorganized: actions.py, vfs.py + added classifiers to setup.py script Version 0.90 was never publically released Version 0.9 ("...and the day arrived") - 2002/09/05: + ZIP files can be un/compressed now. ZIP vfs works too + added 'rebuild vfs' and 'rebuild_vfs' question / option. Configurable. There is no need to wait until vfs file is rebuilt + Applied some good patches from Bartosz Oler (liar AT furrynet DOT org): 1. colors customization: Now you can customize the colors lfm use in the configuration file, 'colors' section. Each color is defined by a string with its name. It looks like this: element foreground_color background_color 2. Allow preferences values to contain colons in the configuration file. 3. Lack of a bookmark's definition shouldn't be an error. + pyview: new features: - read from stdin - go to / set bookmarks - open shell + pyview doesn't show blank screen a the end of the file, now it shows last line + lfm works now with "from __future__ import division", prepared for Python 3.0 + set python2 as default interpreter for setup.py, lfm, lfm.py, pyview and pyview.py + my email address has changed + Bugs fixes. See ChangeLog for complete list - lfm: * create temporary files with mask 0066 and directories with perms 0700, so only owner can read/write them * avoid zombie processes when using 'fork' (now fork twice or threads) * show correct filename when an error occurs while uncompressing file * don't compress '..' directory * when lfm exits after checking command line options, make 'lfm' shell script don't show path error * when un/compressing files cursorbar must remain in the same file * when, at start, lfm can't enter into a dir due to directory permissions * crash if len(line) == width of cursorbar window * don't append '*' to historic in entries * fix cursor bar position after sorting - pyview: * when user hasn't permissions to read file, exit gracefully instead of crashing * last char in file is not showed Version 0.8 ("Close to Paradise") - 2002/03/04: + Implemented VFS feature to enter into .tar.gz and .tar.bz2 files + Panelize vfs option in find/grep implemented + Tree panel implemented + 'lfm' and 'pyview' are simple scripts now, not just a copy of the .py files. __init__.py contains global variables now + A new message window is used to show work in progress, in place of a message in status bar. 'run_thread' function has been cleaned too + Copy / move features now use my own function to walk trees, instead of 'shutil.copytree' which originates some problems and bugs + In ChangePermissions window, the cursor movement is circular now + Change copyright date to years 2001-2 + I don't need a crypt / uncrypt feature, so eliminated from TODO list + Many bugs fixes and functions rewrites. See ChangeLog for complete list - lfm: * if panel2 shows 'a' file in panel, and in panel1 'a' is moved or deleted, lfm crashes * after moving a file cursor goes to next directory as deletion does * catching an exception after not been able to copy => it crashed when trying to delete copied files * "messages.SelectItem, messages.FindfilesWin, messages.MenuWin, messages.ChangePerms: upperleft corner disappears" * findgrep: fix bug: if selected file has a ':' in name - pyview: * changing 'addch' by 'addstr' shows individual chars >= 0xA0 (meta chars) correctly, neither in reversed video or as 2 chars * "if wrap mode => fix prev/next page & up/down cursor". Now they move to screen lines, not to physical lines Version 0.7 ("Hello Darling, I'm here") - 2001/11/30: + 'pyview', a new pager/viewer for use with lfm or standalone, internally or externally. It is used as default pager too. Some features: Text / Hex view, backwards & forwards search, goto line/byte, un/wrap mode, documentation, ... + Rewrite 'show filesystems info' to use internal viewer + New 'run_thread' function in which almost every proccess is executed, so they can be stopped and there is a working signal too. 'do_something_on_file' does not use it + Implemented 'show file info' + Check errors when un/compressing + Support for .bz2 + Removed tar 'v' flag in un/compressing + Fixed completition, it should work perfectly now + Added Ctrl-D key to delete the whole content of the EntryLine + Implemented help: README, NEWS, TODO, ChangeLog or COPYING files + Added new function to show special files: html, graphics, ..., so we have defined new programs and file types too + Added new preference: show_output_after_exec, defaults to yes + Fix cursor position after deleting files + Default configuration is now saved inmediately + Many other bugs fixed again (see ChangeLog) Version 0.6 was never publically released Version 0.5 ("Last call to London") - 2001/08/07: + F2 file menu, added many functions + F9 general menu, added many functions + Implemented find and grep + File permissions, owner and group has a window now + Implemented preferences, edition has to be improved, of course, but loading and saving works + Added 'show filesystems info' feature + Now 'q' or F10 exits to current path, see proper README section + Default pager changed to 'less' + Documentation has been improved + 'setup.py' now installs docs + Many bug fixes and functions rewrites: - home and end keys work ok now - not all people use bash-type shells, so don't use 2>&1 - use popen2.popen3 instead of os.popen to catch messages - manage problems with move while files don't fit into destination - option 'b' does not exist in Solaris' 'du -s' command - fix a problem while moving files if destination has no enough space - many others Version 0.4 - 2001/07/19: + First public release lfm-3.1/lfm/0000755000175000001440000000000013123766702011744 5ustar inigouserslfm-3.1/lfm/folders.py0000644000175000001440000004424013123713415013751 0ustar inigousers# -*- coding: utf-8 -*- import os from glob import glob, escape from operator import attrgetter from tempfile import mkdtemp, mkstemp from os.path import abspath, basename, dirname, exists, expanduser, isabs, isdir, join, normpath, splitext from compress import check_compressed_vfs, get_compressed_file_engine from utils import copy_bulk, delete_bulk, get_filetype, overwrite_vfsfile, \ ProcessCommand, run_in_cli, text2wrap, size2str, time2str, \ perms2str, owner2str, group2str, type2str, rdev2str from common import * ######################################################################## ##### FS Configuration class FSConfig: def __init__(self): self.filters = '' self.show_dotfiles = True self.sort_type = SortType.byName self.sort_reverse = False self.sort_mix_dirs = False self.sort_mix_cases = True def fill_with_app(self, cfg): self.show_dotfiles = cfg.options.show_dotfiles self.sort_type = cfg.options.sort_type self.sort_reverse = cfg.options.sort_reverse self.sort_mix_dirs = cfg.options.sort_mix_dirs self.sort_mix_cases = cfg.options.sort_mix_cases def get_sort_key(self): if self.sort_type == SortType.none: return lambda x: 0 elif self.sort_type == SortType.byName: return lambda x: (attrgetter('name')(x).lower() if self.sort_mix_cases else attrgetter('name')(x)) elif self.sort_type == SortType.byExt: return lambda x: (attrgetter('ext')(x).lower() if self.sort_mix_cases else attrgetter('ext')(x)) elif self.sort_type == SortType.byPath: return lambda x: (attrgetter('path_str')(x).lower() if self.sort_mix_cases else attrgetter('path_str')(x)) elif self.sort_type == SortType.bySize: return attrgetter('size') elif self.sort_type == SortType.byMTime: return attrgetter('mtime') else: raise KeyError ######################################################################## ##### FSEntry class FSEntry: """File System entry""" def __init__(self, pfile): self.pfile = pfile self.pdir = dirname(pfile) self.name = basename(pfile) self.name_noext, self.ext = splitext(self.name) if self.name_noext.endswith('.tar'): self.name_noext = self.name_noext.replace('.tar', '') # self.ext = '.tar' + self.ext st = os.lstat(pfile) self.size = st.st_size self.mode = st.st_mode self.owner = st.st_uid self.group = st.st_gid self.mtime = st.st_mtime self.stat = st self.type = get_filetype(pfile) if self.type in (FileType.cdev, FileType.bdev): try: r = st.st_rdev self.rdev = r >> 8, r & 255 except AttributeError: self.rdev = 0, 0 else: self.rdev = 0, 0 # string representation of attributes self.path_str = str(pfile) self.size_str = size2str(self.size) self.mtime_str = time2str(self.mtime) self.mtime2_str = time2str(self.mtime, short=False) self.mode_str = perms2str(self.mode) self.owner_str = owner2str(self.owner) self.group_str = group2str(self.group) self.type_str = type2str(self.type) self.rdev_str = rdev2str(self.rdev) def format(self, fields, sep='|'): # fields is a list of tuples [('attribute', length)] # ls = ['{0.%s:%ds}' % (attr, length) for attr, length in fields] # fmt = sep.join(ls) # return fmt.format(self) ls = [] for attr, length in fields: ljust = True if attr in ('size', ): if self.type in (FileType.cdev, FileType.bdev): attr, length = 'rdev_str', 7 else: attr, ljust = 'size_str', False elif attr in ('mtime', 'mtime2', 'mode', 'owner', 'group', 'type'): attr += '_str' txt = sep*length if attr=='sep' else text2wrap(getattr(self, attr), length, ljust) ls.append(txt) return ''.join(ls) def __repr__(self): return ''.format(self.name) def get_type_from_ext(self, files_ext): if self.is_dir: return 'dir' elif self.type == FileType.exe: return 'exe' else: for ftype in files_ext.keys(): if self.ext[1:] in files_ext[ftype]: return ftype else: return 'reg' @property def is_dir(self): return self.type in (FileType.dir, FileType.link2dir) @property def is_link(self): return self.type in (FileType.link2dir, FileType.link, FileType.nlink) @property def is_dotfile(self): return self.name[0] == '.' def is_filtered(self, fs_hide, fs_show): """Returns if a file should be filtered (hidden) or not. Accepts 2 lists: first items to hide, second items to show. If a file is in both lists, it is shown""" if len(fs_show)==0 and len(fs_hide)==0: return False return False if self.pfile in fs_show else self.pfile in fs_hide def update_size(self, size): self.size = size self.size_str = size2str(size) ######################################################################## ##### BaseFolder class BaseFolder: """Base Folder""" def __init__(self, base, rel='', parfs=None): self.parfs = parfs self.cfg = FSConfig() adir = expanduser(base if VFS_STRING in base else normpath(base)) if not isabs(adir): adir = abspath(adir) if rel != '': rel = normpath(rel) self.prepare_paths(adir, rel) self.pre_init() self.load() def __len__(self): return len(self._items_sorted) def __getitem__(self, key): return self._items_sorted.__getitem__(key) def __repr__(self): if self.vfs: return '<{}: {} [{}]>'.format(self.clsname, self.path_str, self.pdir) else: return '<{}: {}>'.format(self.clsname, self.path_str) def dump_info(self): log.info('##### {}'.format(self)) log.info('##### base: {} {}'.format(self.base, self.rel)) log.info('##### rbase: {} {}'.format(self.rbase, self.rel)) log.info('##### pdir: {}'.format(self.pdir)) log.info('##### parfs: {}'.format(self.parfs)) @property def nfiltered(self): return len(self._items) - len(self._items_sorted) + 1 # pardir @property def path_str(self): return self.base + self.rel @property def base_filename(self): return self.base[:-len(VFS_STRING)] if self.base.endswith(VFS_STRING) else self.base @property def dirname(self): if self.path_str.endswith(VFS_STRING): if self.clsname == 'CompressedFileFolder': # root of vfsfile (compressedfile) -> dir of vfsfile parent = dirname(self.path_str[:-len(VFS_STRING)]) else: # root of vfsfile (search) -> vfsfile parent = self.path_str[:-len(VFS_STRING)] else: parent = dirname(self.path_str) if parent.endswith('#vfs:'): parent += '//' return parent @property def basename(self): return basename(self.path_str[:-len(VFS_STRING)] if self.path_str.endswith(VFS_STRING) else self.path_str) def prepare_paths(adir, rel): """Method called to initiale paths. 'adir' is str, 'rel' is str""" raise NotImplementedError def pre_init(self): """Method called after initializing paths but before load contents""" raise NotImplementedError def exit(self, all_levels=False, rebuild=False): """Method to be called at folder destruction""" raise NotImplementedError def chdir(self, rel): """Method called before changing directory""" raise NotImplementedError def load(self): """Load folder contents. Call when path contents have change""" log.debug('Load {} [{}]'.format(self, self.pdir)) self._items = [FSEntry(join(self.pdir, f)) for f in os.listdir(self.pdir)] def refresh(self): """Apply filters and sort entries. Call when config, sorting or filters change""" log.debug('Refresh {} [{}]'.format(self, self.pdir)) ds, fs = list(), list() if self.cfg.filters == '': fs_hide, fs_show = self.get_filtered('!.*,!*') # show all else: fs_hide, fs_show = self.get_filtered(self.cfg.filters) if self.cfg.sort_mix_dirs: fs = [it for it in self._items if (self.cfg.show_dotfiles and it.is_dotfile or not it.is_dotfile) and (not it.is_filtered(fs_hide, fs_show))] else: for it in self._items: if not self.cfg.show_dotfiles and it.is_dotfile: continue if it.is_filtered(fs_hide, fs_show): continue if it.is_dir: ds.append(it) else: fs.append(it) key = self.cfg.get_sort_key() ds.sort(key=key, reverse=self.cfg.sort_reverse) ds.insert(0, FSEntry(join(self.pdir, '..'))) # insert parent dir fs.sort(key=key, reverse=self.cfg.sort_reverse) ds.extend(fs) self._items_sorted = ds def lookup(self, filename): """Returns FSEntry if file exists in directory""" for f in self._items_sorted: if f.name == filename: return f else: return None def pos(self, filename): """Returns the position index if file exists in directory""" for f in self._items_sorted: if f.name == filename: return self._items_sorted.index(f) else: return -1 def get_filenames(self, start=0): """Returns a (sub)list with the sorted dirs/files names in directory""" return [f.name for f in self._items_sorted[start:]] def get_filtered(self, filters): """Returns entries that match any of the globs. If glob starts with ! add match to 2nd list""" globs = [] if filters=='' else [f.strip() for f in filters.split(',')] fs_hide, fs_show = [], [] p = escape(self.pdir) for g in globs: if g.startswith('!'): fs_show.extend(glob(join(p, g[1:]))) else: fs_hide.extend(glob(join(p, g))) return fs_hide, fs_show @property def dirs(self): return [it for it in self._items_sorted if it.is_dir and it.name!=os.pardir] ######################################################################## ##### LocalFolder class LocalFolder(BaseFolder): """Local File System Folder""" clsname = 'LocalFolder' def prepare_paths(self, adir, _): assert isdir(adir) self.base = self.rbase = '' self.pdir = self.rel = adir self.vfs = False self.vfs_str = '' def pre_init(self): pass def exit(self, all_levels=False, rebuild=False): pass def chdir(self, rel): raise RuntimeError ######################################################################## ##### CompressedFileFolder class CompressedFileFolder(BaseFolder): """Compressed File Folder""" clsname = 'CompressedFileFolder' def prepare_paths(self, adir, rel): self.base = adir + VFS_STRING self.rbase = mkdtemp(suffix='.lfm') self.rel = rel self.pdir = join(self.rbase, rel) self.vfs = True self.vfs_str = VFS_STRING self.already_exited = False def pre_init(self): log.debug('Preparing VFS: {}'.format(self)) c = get_compressed_file_engine(self.base.replace(VFS_STRING, '')) if c is None: raise st, res, err = ProcessCommand('Creating VFS', self.base_filename, c.cmd_uncompress, path=self.rbase).run() if st == -100: # stopped by user delete_bulk(self.rbase, ignore_errors=True) raise UserWarning('Stopped by user') if err != '': delete_bulk(self.rbase, ignore_errors=True) raise UserWarning(err) def exit(self, all_levels=False, rebuild=False): if self.already_exited: return log.debug('Exiting from VFS: {}'.format(self)) if all_levels: parfs = self.parfs while parfs: parfs.exit() parfs = parfs.parfs if rebuild: self.__rebuild() delete_bulk(self.rbase, ignore_errors=True) self.already_exited = True def chdir(self, rel): self.rel = rel new_pdir = join(self.rbase, rel) if isdir(new_pdir): self.pdir = new_pdir log.debug('VFS: chdir {} -> {}'.format(self, self.pdir)) self.load() return self else: log.debug('VFS in VFS: {} -> {}'.format(self, self.pdir)) log.debug('New folder: path="{}", oldfs={}'.format(new_pdir, '"%s"' % self)) base, rel = split_vfs_path(new_pdir) newfs = open_folder(base, rel, self, True) newfs.base = self.path_str + VFS_STRING return newfs def __rebuild(self): log.debug('Rebuilding VFS: {}'.format(self)) c = get_compressed_file_engine(self.base.replace(VFS_STRING, '')) if c is None: raise _, tmpfile = mkstemp(suffix='.lfm') st, res, err = ProcessCommand('Rebuilding VFS', self.base_filename, c.cmd_compress2('*', tmpfile), path=self.rbase).run() # compress process always create filename with extension delete_bulk(tmpfile, ignore_errors=True) tmpfile_ext = tmpfile + c.exts[0] if st == -100: # stopped by user delete_bulk(tmpfile_ext, ignore_errors=True) delete_bulk(self.rbase, ignore_errors=True) raise UserWarning('Stopped by user') if err != '': delete_bulk(tmpfile_ext, ignore_errors=True) delete_bulk(self.rbase, ignore_errors=True) raise UserWarning(err) try: overwrite_vfsfile(tmpfile_ext, c.path) except OSError as err: delete_bulk(tmpfile_ext, ignore_errors=True) raise ######################################################################## ##### SearchFolder class SearchFolder(BaseFolder): """Search Folder""" clsname = 'SearchFolder' def prepare_paths(self, adir, rel): self.base = adir + VFS_STRING self.rbase = mkdtemp(suffix='.lfm') self.rel = rel self.pdir = join(self.rbase, rel) self.vfs = True self.vfs_str = VFS_STRING def pre_init(self): pass def copy_files(self, files): log.debug('Preparing VFS: {}'.format(self)) self.files = files for f in files: src = join(self.parfs.pdir, f) dest = join(self.pdir, f) os.makedirs(dirname(dest), exist_ok=True) copy_bulk(src, dest) self.load() return self def exit(self, all_levels=False, rebuild=False): log.debug('Exiting from VFS: {}'.format(self)) if rebuild: self.__rebuild() delete_bulk(self.rbase, ignore_errors=True) def chdir(self, rel): self.rel = rel self.pdir = join(self.rbase, rel) log.debug('Search VFS: chdir {} -> {}'.format(self, self.pdir)) self.load() return self def __rebuild(self): log.debug('Rebuilding VFS: {}'.format(self)) files = [f[2:] for f in run_in_cli('find . -type f', self.pdir)[0].split()] deleted = set(self.files) - set(files) for f in files: src = join(self.pdir, f) dest = join(self.parfs.pdir, f) os.makedirs(dirname(dest), exist_ok=True) copy_bulk(src, dest) for f in deleted: dest = join(self.parfs.pdir, f) delete_bulk(dest, ignore_errors=True) ######################################################################## def split_vfs_path(path): n = path.rfind(VFS_STRING) # can be vfs in vfs => rfind, not find! if n == -1: base, rel = path, '' else: base, rel = path[:n+len(VFS_STRING)], path[n+len(VFS_STRING):] return base, rel def open_folder(base, rel, oldfs, vfsinvfs): if isdir(base): if rel != '': raise ValueError return LocalFolder(base) if base.endswith(VFS_STRING): base = base[:-len(VFS_STRING)] if check_compressed_vfs(base): if not vfsinvfs and oldfs and oldfs.parfs and exists(oldfs.parfs.pdir): oldfs.parfs.rel = dirname(oldfs.parfs.rel) return oldfs.parfs else: return CompressedFileFolder(base, rel, oldfs) raise FileNotFoundError def is_delete_oldfs(newpath, oldfs): """Returns if we will exit oldfs in next new_folder()""" base, rel = split_vfs_path(newpath) return oldfs and oldfs.vfs and base!=oldfs.base and not basename(newpath)==basename(oldfs.path_str) def new_folder(path, oldfs=None, rebuild_if_exit=False, files=None): # path is str, oldfs is BaseFolder! if files: log.debug('New searchvfs folder: path="{}", oldfs={}'.format(path, '"%s"' % oldfs if oldfs else 'none')) return SearchFolder(path, parfs=oldfs).copy_files(files) log.debug('New folder: path="{}", oldfs={}'.format(path, '"%s"' % oldfs if oldfs else 'none')) base, rel = split_vfs_path(path) if not oldfs: return open_folder(base, rel, None, False) if base == oldfs.base: return oldfs.chdir(rel) else: if basename(path) != basename(oldfs.path_str) or path.endswith(VFS_STRING) \ and oldfs.path_str.endswith(VFS_STRING+VFS_STRING): # exit vfs oldfs.exit(rebuild=rebuild_if_exit) if oldfs.clsname == 'SearchFolder': return oldfs.parfs return open_folder(base, rel, oldfs, False) ######################################################################## lfm-3.1/lfm/actions.py0000644000175000001440000012735713066244115013770 0ustar inigousers# -*- coding: utf-8 -*- import pkg_resources from glob import glob from tempfile import mkdtemp from os import sep, readlink, environ, uname from os.path import basename, dirname, exists, getsize, isdir, isfile, islink, join, pardir import utils from utils import public from ui_widgets import DialogError, DialogConfirm, DialogGetKey, SelectItem, \ DialogEntry, DialogDoubleEntry, DialogFindGrep, TreeView, \ DialogPerms, DialogOwner, InternalView from common import * ######################################################################## ##### Module variables app = None ######################################################################## ##### Action dispatcher def do(mapp, action): global app app = mapp log.debug('Execute: {}'.format(action)) try: fn = globals()[action] except KeyError: log.warning('ERROR can\'t execute an undefined function: {}()'.format(action)) return RET_NONE if not utils.is_public_api(fn): log.warning('ERROR: function {}() is not public API for action "{}"'.format(fn.__name__, action)) return RET_NONE ret = fn() log.debug('Returns: {}'.format(ret)) return ret ######################################################################## ##### Cursor movement @public def cursor_up(): app.pane_active.tab_active.i -= 1 return RetCode.fix_limits, None @public def cursor_down(): app.pane_active.tab_active.i += 1 return RetCode.fix_limits, None @public def cursor_up10(): app.pane_active.tab_active.i -= 10 return RetCode.fix_limits, None @public def cursor_down10(): app.pane_active.tab_active.i += 10 return RetCode.fix_limits, None @public def cursor_pageup(): app.pane_active.tab_active.i -= app.pane_active.fh return RetCode.fix_limits, None @public def cursor_pagedown(): app.pane_active.tab_active.i += app.pane_active.fh return RetCode.fix_limits, None @public def cursor_home(): app.pane_active.tab_active.i = 0 return RetCode.fix_limits, None @public def cursor_end(): app.pane_active.tab_active.i = app.pane_active.tab_active.n-1 return RetCode.fix_limits, None @public def cursor_goto_file(): text = DialogEntry('Go to file', 'Type part of the file name').run() if not text: return RetCode.nothing, None start = app.pane_active.tab_active.i + 1 entries = app.pane_active.tab_active.fs.get_filenames(start=start) for e in entries: if e.find(text) != -1: app.pane_active.tab_active.i = entries.index(e) + start return RetCode.fix_limits, None return RetCode.fix_limits, None @public def cursor_goto_file_1char(): start = app.pane_active.tab_active.i + 1 entries = app.pane_active.tab_active.fs.get_filenames(start=start) app.win.nodelay(0) ch = app.win.getkey() app.win.nodelay(1) for e in entries: if ch == e[0]: app.pane_active.tab_active.i = entries.index(e) + start return RetCode.fix_limits, None return RetCode.nothing, None ######################################################################## ##### Cursor movement @public def dir_up(): tab = app.pane_active.tab_active if tab.fs.path_str == sep: return RetCode.nothing, None olddirname = tab.fs.basename tab.goto_folder(tab.fs.dirname) tab.focus_file(olddirname) return RetCode.full_redisplay, None @public def dir_enter(): tab = app.pane_active.tab_active fname = tab.fs[tab.i].name if fname == pardir: return dir_up() ft = tab.fs[tab.i].get_type_from_ext(app.cfg.files_ext) if ft in ('dir', 'archive'): tab.goto_folder(join(tab.fs.path_str, fname)) elif ft in ('audio', 'ebook', 'graphics', 'pdf', 'video', 'web'): utils.run_on_current_file(app.cfg.programs[ft], app.pane_active.tab_active.current_filename_full, True) return refresh() else: utils.run_on_current_file(app.cfg.programs['pager'], app.pane_active.tab_active.current_filename_full) return refresh() return RetCode.full_redisplay, None @public def goto(): newdir = DialogEntry('Change directory', 'Type directory name', '', history=app.history['path'][:], is_files=True).run() if not newdir: return RetCode.nothing, None newdir = utils.get_norm_path(newdir, app.pane_active.tab_active.dirname) log.debug('Chdir "{}"'.format(newdir)) app.pane_active.tab_active.goto_folder(newdir, delete_vfs_tree=True) if newdir: app.history.append('path', newdir) return RetCode.full_redisplay, None @public def bookmark_goto(): while True: key = DialogGetKey('Goto bookmark', 'Press 0-9 a-z to select the bookmark, Ctrl-C to quit') if key == -1: # Ctrl-C break elif chr(key) in BOOKMARKS_KEYS: log.debug('Goto bookmark in key "{}" > "{}"'.format(chr(key), app.cfg.bookmarks[chr(key)])) app.pane_active.tab_active.goto_folder(app.cfg.bookmarks[chr(key)], delete_vfs_tree=True) break return RetCode.full_redisplay, None @public def bookmark_set(): fs = app.pane_active.tab_active.fs if fs.vfs: DialogError('Cannot save a bookmark to a VFS') return RetCode.full_redisplay, None while True: key = DialogGetKey('Set bookmark', 'Press 0-9 a-z to save bookmark, Ctrl-C to quit') if key == -1: # Ctrl-C break elif chr(key) in BOOKMARKS_KEYS: log.debug('Save bookmark "{}" to key "{}"'.format(fs.path_str, chr(key))) app.cfg.bookmarks[chr(key)] = fs.path_str break return RetCode.full_redisplay, None @public def bookmark_select_fromlist(): bmks = sorted(['{} {}'.format(k, b) for k, b in app.cfg.bookmarks.items()]) ret = SelectItem('Select Bookmark', bmks).run() if ret != -1: app.pane_active.tab_active.goto_folder(ret[3:], delete_vfs_tree=True) return RetCode.full_redisplay, None @public def history_select_fromlist(): tab = app.pane_active.tab_active if len(tab.history) == 0: DialogError('No entries in history') return RetCode.full_redisplay, None ret = SelectItem('Return to', list(reversed(tab.history)), quick_key=False).run() if ret != -1: app.pane_active.tab_active.goto_folder(ret, delete_vfs_tree=True) return RetCode.full_redisplay, None ######################################################################## ##### Panes @public def pane_change_focus(): otherpane = app.pane2 if app.pane1.focus else app.pane1 app.focus_pane(otherpane) return RetCode.full_redisplay, None @public def pane_other_tab_equal(): path = app.pane_active.tab_active.fs.path_str app.pane_inactive.tab_active.goto_folder(path, delete_vfs_tree=True) return RetCode.full_redisplay, None @public def panes_swap(): log.debug('Swap panes') app.pane1, app.pane2 = app.pane2, app.pane1 app.resize() return RetCode.full_redisplay, None @public def panes_cycle_view(): if app.pane_active.mode == PaneMode.half: app.pane_active.change_mode(PaneMode.full) else: app.pane_active.change_mode(PaneMode.half) return RetCode.full_redisplay, None @public def refresh(): for tab in app.pane1.tabs + app.pane2.tabs: tab.reload() tab.refresh() return RetCode.full_redisplay, None @public def redraw_screen(): app.clear_screen() return RetCode.full_redisplay, None @public def dotfiles_toggle(): app.cfg.options.show_dotfiles = not app.cfg.options.show_dotfiles for tab in app.pane1.tabs + app.pane2.tabs: tab.fs.cfg.show_dotfiles = app.cfg.options.show_dotfiles tab.refresh() return RetCode.full_redisplay, None @public def filters_edit(): tab = app.pane_active.tab_active text = DialogEntry('Edit filter', 'Type globs, separated by commas, for the files you want to hide', tab.fs.cfg.filters, history=app.history['glob'][:], is_files=False).run() if text is None: # not "if not text"!!! because we need text='' to clear filters return RetCode.nothing, None tab.fs.cfg.filters = text tab.refresh() if text: app.history.append('glob', text) return RetCode.half_redisplay, None @public def sort_files(): sorttypes = {'o': SortType.none, 'O': SortType.none, 'n': SortType.byName, 'N': SortType.byName, 'e': SortType.byName, 'E': SortType.byName, 's': SortType.bySize, 'S': SortType.bySize, 'd': SortType.byMTime, 'D': SortType.byMTime} while True: key = DialogGetKey('Sorting mode', 'N(o)ne, by (n)ame, by (e)xtension, by (s)ize, by (d)ate,\nuppercase to reverse order, Ctrl-C to quit') if key == -1: # Ctrl-C break elif chr(key) in sorttypes.keys(): app.cfg.options.sort_type = sorttypes[chr(key)] app.cfg.options.sort_reverse = key= MAX_TABS: DialogError('Cannot create more tabs') else: curtab = app.pane_active.tab_active app.pane_active.insert_new_tab(curtab.fs.path_str, curtab) return RetCode.full_redisplay, None @public def tab_close(): if len(app.pane_active.tabs) == 1: DialogError('Cannot close last tab') else: app.pane_active.close_tab(app.pane_active.tab_active) return RetCode.full_redisplay, None @public def tab_left(): tab = app.pane_active.tab_active idx = app.pane_active.tabs.index(tab) if idx == 0: return RetCode.nothing, None app.pane_active.tab_active = app.pane_active.tabs[idx-1] return RetCode.full_redisplay, None @public def tab_right(): tab = app.pane_active.tab_active idx = app.pane_active.tabs.index(tab) if idx==len(app.pane_active.tabs)-1 or idx==MAX_TABS-1: return RetCode.nothing, None app.pane_active.tab_active = app.pane_active.tabs[idx+1] return RetCode.full_redisplay, None ######################################################################## ##### Selections @public def select(): tab = app.pane_active.tab_active if tab.i != 0: # pardir it = tab.fs[tab.i] if it is not None: try: tab.selected.index(it) except ValueError: tab.selected.append(it) else: tab.selected.remove(it) app.pane_active.tab_active.i += 1 return RetCode.fix_limits, None return RetCode.nothing, None @public def select_glob(): tab = app.pane_active.tab_active text = DialogEntry('Select group', 'Type pattern', '*', history=app.history['glob'][:], is_files=False).run() if not text: return RetCode.nothing, None for fname in glob(join(tab.dirname, text)): f = tab.fs.lookup(basename(fname)) if f and f not in tab.selected: tab.selected.append(f) if text != '*': app.history.append('glob', text) return RetCode.half_redisplay, None @public def deselect_glob(): tab = app.pane_active.tab_active text = DialogEntry('Deselect group', 'Type pattern', '*', history=app.history['glob'][:], is_files=False).run() if not text: return RetCode.nothing, None for fname in glob(join(tab.dirname, text)): f = tab.fs.lookup(basename(fname)) if f and f in tab.selected: tab.selected.remove(f) if text != '*': app.history.append('glob', text) return RetCode.half_redisplay, None @public def select_invert(): tab = app.pane_active.tab_active selected_old = tab.selected[:] tab.selected = [f for f in tab.fs if f not in selected_old and f.name != '..'] return RetCode.half_redisplay, None ######################################################################## ##### Files @public def rename_file(): tab = app.pane_active.tab_active filename = tab.current_filename if filename == pardir: return RetCode.nothing, None newname = DialogEntry('Rename', 'Rename \'{}\' to'.format(filename), filename, history=app.history['file'][:], is_files=True).run() if not newname: return RetCode.nothing, None src = tab.current_filename_full dest = newname if newname[0] == sep else join(tab.dirname, newname) if src == dest: DialogError('Cannot rename \'{}\'\nSource and destination are the same file'.format(filename)) return RetCode.full_redisplay, None if dirname(dest) != tab.dirname: DialogError('Cannot rename to different directory') return RetCode.full_redisplay, None if exists(dest) and app.cfg.confirmations.overwrite: if not DialogConfirm('Rename file', 'Overwrite \'{}\'?'.format(newname), 0): return RetCode.full_redisplay, None log.debug('Rename file: {} -> {}'.format(src, dest)) err = utils.rename_file(src, dest) if err: log.warning('Cannot rename: \'{}\' to \'{}\': {}'.format(filename, newname, str(err))) DialogError('Cannot rename: \'{}\' to \'{}\'\n{}'.format(filename, newname, str(err))) return RetCode.full_redisplay, None else: app.history.append('file', newname) return refresh() def __copymove_helper(act): tab = app.pane_active.tab_active files = [f.pfile for f in tab.selected_or_current] if len(files) == 0: return None, None, None if len(files) == 1: subtitle = '{} \'{}\' to:'.format(act, basename(files[0])) else: subtitle = '{} {} items to:'.format(act, len(files)) destdir = DialogEntry('{} file(s)'.format(act), subtitle, app.pane_inactive.tab_active.dirname+sep, history=app.history['path'][:], is_files=True).run() if not destdir: return None, None, None destdir = utils.get_norm_path(destdir, tab.dirname) if not isdir(destdir) and len(files) > 1: DialogError('Cannot {0} files\nTried to {0} many items to one file name'.format(act.lower())) return None, None, None log.debug('{} file(s) to \'{}\''.format(act, destdir)) app.history.append('path', destdir) basepath = tab.dirname if tab.dirname[-1]==sep else tab.dirname+sep return destdir, basepath, files @public def copy_file(): destdir, basepath, files = __copymove_helper('Copy') if not destdir: return RetCode.full_redisplay, None es = utils.PathContents(files, basepath) args = [(f, s, e, basepath, destdir) for f, s, e in es.entries] st, rets, errs = utils.ProcessFuncCopyLoop('Copy file(s)', utils.copy_file, args, es.tsize, app.cfg.confirmations.overwrite).run() app.pane_active.tab_active.selected = [] if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: if isinstance(err, LFMFileSkipped): log.warning('Copy file(s). Not overwritten: {}'.format(str(err))) else: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def move_file(): destdir, basepath, files = __copymove_helper('Move') if not destdir: return RetCode.full_redisplay, None es = utils.PathContents(files, basepath) args = [(f, s, e, basepath, destdir) for f, s, e in es.entries] st, rets, errs = utils.ProcessFuncCopyLoop('Move file(s)', utils.copy_file, args, es.tsize, app.cfg.confirmations.overwrite).run() if st == ProcCode.stopped: return refresh() not_overwritten = list() for a, res, err in zip(args, rets, errs): if err: if isinstance(err, LFMFileSkipped): log.warning('Move file(s). Not overwritten: {}'.format(str(err))) not_overwritten.append(str(err)) else: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) es.remove_files(not_overwritten) args = [(f, s, e, basepath) for f, s, e in es.entries_rev] # reverse! st, rets, errs = utils.ProcessFuncDeleteLoop('Move file(s)', utils.delete_file, args, es.tsize, False).run() app.pane_active.tab_active.selected = [] if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def move_file2(): # alternative version using shtutil.move instead of copy & delete. Faster but less control destdir, basepath, files = __copymove_helper('Move') if not destdir: return RetCode.full_redisplay, None args, size = list(), 0 for f in files: try: s = getsize(f) except OSError as err: s = 0 size += s args.append((f, s, None, basepath, destdir)) st, rets, errs = utils.ProcessFuncCopyLoop('Move file(s)', utils.move_file, args, size, app.cfg.confirmations.overwrite).run() app.pane_active.tab_active.selected = [] if st == ProcCode.stopped: return refresh() for a, ret, err in zip(args, rets, errs): if err is not None: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def delete_file(): tab = app.pane_active.tab_active files = [f.pfile for f in tab.selected_or_current] if len(files) == 0: return RetCode.nothing, None log.debug('Delete file(s)') es = utils.PathContents(files, tab.dirname+sep) args = [(f, s, e, tab.dirname+sep) for f, s, e in es.entries_rev] # reverse! st, rets, errs = utils.ProcessFuncDeleteLoop('Delete file(s)', utils.delete_file, args, es.tsize, app.cfg.confirmations.delete).run() app.pane_active.tab_active.selected = [] if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: if isinstance(err, LFMFileSkipped): log.warning('Delete file(s). Not deleted: {}'.format(str(err))) else: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def exec_on_file(): cmd = DialogEntry('Execute command on file(s)', 'Enter command', '', history=app.history['exec'][:], is_files=False).run() if not cmd: return RetCode.nothing, None for fsentry in app.pane_active.tab_active.selected_or_current: log.debug('Exec on file: {} "{}"'.format(cmd, fsentry.pfile)) utils.run_on_current_file(cmd, fsentry.pfile) log.debug('Exec on file: {}'.format(cmd)) app.pane_active.tab_active.selected = [] app.history.append('exec', cmd) return refresh() @public def view_file(): log.debug('View file: {}'.format(app.pane_active.tab_active.current_filename_full)) utils.run_on_current_file(app.cfg.programs['pager'], app.pane_active.tab_active.current_filename_full) return refresh() @public def edit_file(): log.debug('Edit file: {}'.format(app.pane_active.tab_active.current_filename_full)) utils.run_on_current_file(app.cfg.programs['editor'], app.pane_active.tab_active.current_filename_full) return refresh() @public def make_dir(): newdir = DialogEntry('Make directory', 'Type directory name', '', history=app.history['file'][:], is_files=True).run() if not newdir: return RetCode.nothing, None log.debug('Make directory: {}'.format(join(app.pane_active.tab_active.dirname, newdir))) err = utils.make_dir(join(app.pane_active.tab_active.dirname, newdir)) if err: log.warning('Cannot make directory: {}'.format(str(err))) DialogError('Cannot make directory\n{}'.format(str(err))) return RetCode.full_redisplay, None else: app.history.append('file', newdir) return refresh() @public def touch_file(): filename = DialogEntry('Touch file', 'Type file name', '', history=app.history['file'][:], is_files=True).run() if not filename: return RetCode.nothing, None log.debug('Touch file: {}'.format(join(app.pane_active.tab_active.dirname, filename))) err = utils.touch_file(join(app.pane_active.tab_active.dirname, filename)) if err: log.warning('Cannot touch file: {}'.format(str(err))) DialogError('Cannot touch file\n{}'.format(str(err))) return RetCode.full_redisplay, None else: app.history.append('file', filename) return refresh() @public def link_create(): thistab, othertab = app.pane_active.tab_active, app.pane_inactive.tab_active otherfile = utils.get_relpath(join(othertab.dirname, othertab.current_filename), thistab.dirname) ans = DialogDoubleEntry('Create link', 'Link name', 'Pointing to', '', otherfile, history1=app.history['file'][:], history2=app.history['path'][:], is_files=True).run() if not ans: return RetCode.nothing, None newlink, pointto = ans if newlink == '': DialogError('Cannot create link\nYou must specify the name for the new link') return RetCode.full_redisplay, None if pointto == '': DialogError('Cannot create link\nYou must specify the file to link') return RetCode.full_redisplay, None log.debug('Create link: {} -> {}'.format(join(thistab.dirname, newlink), pointto)) err = utils.link_create(join(thistab.dirname, newlink), pointto) if err: log.warning('Cannot create link: {}'.format(str(err))) DialogError('Cannot create link\n{}'.format(str(err))) return RetCode.full_redisplay, None else: app.history.append('file', newlink) return refresh() @public def link_edit(): linkname = app.pane_active.tab_active.current_filename_full if not islink(linkname): return RetCode.nothing, None newpointto = DialogEntry('Edit link', 'Link points to', readlink(linkname), history=app.history['path'][:], is_files=True).run() if not newpointto: return RetCode.nothing, None if newpointto == readlink(linkname): return RetCode.nothing, None log.debug('Edit link: {} -> {}'.format(linkname, newpointto)) err = utils.link_edit(linkname, newpointto) if err: log.warning('Cannot edit link: {}'.format(str(err))) DialogError('Cannot edit link\n{}'.format(str(err))) return RetCode.full_redisplay, None else: app.history.append('path', newpointto) return refresh() @public def backup_file(): ext = app.cfg.misc.backup_extension args = [(f.pfile, ext) for f in app.pane_active.tab_active.selected_or_current] if len(args) == 0: return RetCode.nothing, None log.debug('Backup file(s)') st, rets, errs = utils.ProcessFuncLoop('Create backup file(s)', utils.backup_file, args).run() app.pane_active.tab_active.selected = [] if st == ProcCode.stopped: return refresh() for a, ret, err in zip(args, rets, errs): if err is not None: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def change_perms(): tab = app.pane_active.tab_active files = [f for f in tab.selected_or_current] if len(files) == 0: return RetCode.nothing, None log.debug('Change file permissions') app.pane_active.tab_active.selected = [] for_all = False for i, f in enumerate(files): if not for_all: perms, recursive, for_all = DialogPerms(f.name, f.mode, i+1, len(files)).run() if perms == -1: # stop break elif perms == 0: # ignore file continue cmd = 'chmod {} 0{:o} "{}"'.format('-R' if recursive else '', utils.str2perms(perms), f.pfile) st, res, err = utils.ProcessCommand('Change file(s) permission', cmd, cmd, path=tab.dirname).run() if st == -100: # stopped by user return RetCode.full_redisplay, None if err: log.warning('Cannot "{}": {}'.format(cmd, str(err).replace('\n', ' '))) DialogError('Cannot "{}"\n{}'.format(cmd, str(err))) return refresh() @public def change_owner(): tab = app.pane_active.tab_active files = [f for f in tab.selected_or_current] if len(files) == 0: return RetCode.nothing, None log.debug('Change file owner/group') app.pane_active.tab_active.selected = [] for_all = False for i, f in enumerate(files): if not for_all: owner, group, recursive, for_all = DialogOwner(f.name, f.owner_str, f.group_str, utils.get_owners(), utils.get_groups(), i+1, len(files)).run() if owner == -1: # stop break elif owner == 0: # ignore file continue rec = '-R' if recursive else '' cmd = 'chown {} {} "{}"'.format(rec, owner, f.pfile) st, res, err = utils.ProcessCommand('Change file(s) owner', cmd, cmd, path=tab.dirname).run() if st == -100: # stopped by user return RetCode.full_redisplay, None if err: log.warning('Cannot "{}": {}'.format(cmd, str(err).replace('\n', ' '))) DialogError('Cannot "{}"\n{}'.format(cmd, str(err))) cmd = 'chgrp {} {} "{}"'.format(rec, group, f.pfile) st, res, err = utils.ProcessCommand('Change file(s) group', cmd, cmd, path=tab.dirname).run() if st == -100: # stopped by user return RetCode.full_redisplay, None if err: log.warning('Cannot "{}": {}'.format(cmd, str(err).replace('\n', ' '))) DialogError('Cannot "{}"\n{}'.format(cmd, str(err))) return refresh() @public def diff_file_with_backup(): log.debug('Diff file with backup') ext = app.cfg.misc.backup_extension filename = app.pane_active.tab_active.current_filename_full if filename.endswith(ext): file_old, file_new = filename, filename[:-len(ext)] else: file_old, file_new = filename+ext, filename if not exists(file_old): DialogError('Cannot diff file\nBackup file does not exist') return RetCode.full_redisplay, None if not exists(file_new): DialogError('Cannot diff file\nOnly backup file exists') return RetCode.full_redisplay, None if not isfile(file_old) or not isfile(file_new): DialogError('Cannot diff file\nWe can only diff regular files') return RetCode.full_redisplay, None diff = utils.get_file_diff(file_old, file_new, app.cfg.misc.diff_type) if diff == '': DialogError('Files are identical') return RetCode.full_redisplay, None tmpfile = join(mkdtemp(), basename(file_old) + ' DIFF ' + basename(file_new)) with open(tmpfile, 'w') as f: f.write(diff) utils.run_on_current_file(app.cfg.programs['pager'], tmpfile) utils.delete_bulk(tmpfile, True) return RetCode.full_redisplay, None @public def show_file_info(): log.debug('Show file information') tab = app.pane_active.tab_active f = tab.current lst = [] user = environ['USER'] username = utils.get_user_fullname(user) so, host, ver, tmp, arch = uname() color = 'view_green_on_black' lst.append(('{} v{} executed by {}'.format(LFM_NAME, VERSION, username), color)) lst.append(('<{}@{}> on {} {} [{}]'.format(user, host, so, ver, arch), color)) lst.append(('', color)) color = 'view_red_on_black' fileinfo = utils.get_file_info(f.pfile) lst.append(('{}: {} ({})'.format(FILETYPES[f.type][1], f.name, fileinfo), color)) lst.append(('Path: {}'.format(tab.fs.path_str), color)) lst.append(('Size: {} bytes'.format(f.size), color)) lst.append(('Mode: {} ({:o})'.format(f.mode_str, f.mode), color)) lst.append(('Links: {}'.format(f.stat.st_nlink), color)) lst.append(('User ID: {} ({}) / Group ID: {} ({})'.format(f.owner_str, f.owner, f.group_str, f.group), color)) lst.append(('Last access: {}'.format(utils.time2str_full(f.stat.st_atime)), color)) lst.append(('Last modification: {}'.format(utils.time2str_full(f.mtime)), color)) lst.append(('Last change: {}'.format(utils.time2str_full(f.stat.st_ctime)), color)) dev = f.stat.st_dev lst.append(('Location: {}, {} / Inode: #{:X} ({:X}h:{:X}h)'.format( (dev>>8) & 0x00FF, dev & 0x00FF, f.stat.st_ino, dev, f.stat.st_ino), color)) filename = tab.fs.path_str if tab.fs.vfs else f.pfile mountpoint, device, fstype = utils.get_mountpoint_for_file(filename) lst.append(('File system: {} on {} ({})'.format(device, mountpoint, fstype), color)) InternalView('Information about \'{}\''.format(f.name), lst).run() return refresh() @public def show_filesystems_info(): log.debug('Show filesystems information') try: buf = utils.get_filesystems_info() except OSError as err: DialogError('Cannot show filesystems info:\n{}'.format(err)) return RetCode.full_redisplay, None lbuf = buf.strip().split('\n') hdr = lbuf[0].strip() lst = [(hdr, 'view_red_on_black'), ('-'*len(hdr), 'view_red_on_black')] for l in lbuf[1:]: lst.append((l.strip(), 'view_white_on_black')) InternalView('Show filesystems info', lst).run() return refresh() ######################################################################## ##### Un/compress def do_uncompress_dir(destdir): args = [(f.pfile, destdir) for f in app.pane_active.tab_active.selected_or_current] if len(args) == 0: return RetCode.nothing, None log.debug('Uncompress file(s) to \'{}\''.format(destdir)) app.pane_active.tab_active.selected = [] st, rets, errs = utils.ProcessFuncLoop('Uncompress file(s)', utils.uncompress_dir, args).run() if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def uncompress_dir(): return do_uncompress_dir(app.pane_active.tab_active.dirname) @public def uncompress_dir_other_pane(): return do_uncompress_dir(app.pane_inactive.tab_active.dirname) @public def compress_dir(): dirs = [d.pfile for d in app.pane_active.tab_active.selected_or_current] # if d.is_dir if len(dirs) == 0: return RetCode.nothing, None compress_fmts = {'g': 'tgz', 'b': 'tbz2', 'x': 'txz', 'l': 'tlz', '4': 'tlz4', 't': 'tar', 'z': 'zip', 'r': 'rar', '7': '7z'} while True: ch = DialogGetKey('Compress directory to...', '.tar.(g)z, .tar.(b)z2, .tar.(x)z, .tar.(l)z,\n.tar.lz(4), .(t)ar, .(z)ip, .(r)ar, .(7)z\n\n' ' Ctrl-C to quit') if ch == -1: # Ctrl-C return RetCode.full_redisplay, None elif chr(ch) in compress_fmts.keys(): typ = compress_fmts[chr(ch)] break args = [(d, typ) for d in dirs] log.debug('Compress dir(s)') app.pane_active.tab_active.selected = [] st, rets, errs = utils.ProcessFuncLoop('Compress dir(s)', utils.compress_dir, args).run() if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() @public def compress_uncompress_file(): files = [f.pfile for f in app.pane_active.tab_active.selected_or_current] # if f.is_dir if len(files) == 0: return RetCode.nothing, None compress_fmts = {'g': 'gz', 'b': 'bz2', 'x': 'xz', 'l': 'lz', '4': 'lz4'} while True: ch = DialogGetKey('Un/Compress file(s)', '(g)zip, (b)zip2, (x)z, (l)z, lz(4)\n\n' ' Ctrl-C to quit') if ch == -1: # Ctrl-C return RetCode.full_redisplay, None elif chr(ch) in compress_fmts.keys(): typ = compress_fmts[chr(ch)] break args = [(f, typ) for f in files] log.debug('Compress or Uncompress file(s)') app.pane_active.tab_active.selected = [] st, rets, errs = utils.ProcessFuncLoop('Compress dir(s)', utils.compress_uncompress_file, args).run() if st == ProcCode.stopped: return refresh() for a, res, err in zip(args, rets, errs): if err: log.warning('ERROR: {}'.format(str(err).replace('\n', ' '))) return refresh() ######################################################################## ##### General @public def find_grep(): ans = DialogDoubleEntry('Find files', 'Filename', 'Content', '*', '', history1=app.history['find'][:], history2=app.history['grep'][:], is_files=False).run() if ans is None or not ans[0]: return RetCode.nothing, None fs, pat = ans tab = app.pane_active.tab_active path = tab.dirname if tab.dirname[-1]==sep else tab.dirname+sep if pat: log.debug('Find "{}" files with "{}" in {}'.format(fs, pat, path)) # 1. find . -type f -iname "*.py" -exec grep -EHni PATTERN {} \; # the slowest, 10x # 2. find . -type f -iname "*py" -print0 | xargs --null -0 grep -EHni PATTERN # maybe the best choice # 3. grep -EHni PATTERN `find . -type f -iname "*.py"` # don't like the ` # 4. grep -REHni PATTERN --include "*.py" . # the fastest, but maybe they wouldn't work on some old UNIX because of -R, --include cmd = '{} -R{}Hn{} "{}" --include "{}"'.format(SYSPROGS['grep'], 'E' if app.cfg.options.grep_regex else '', 'i' if app.cfg.options.grep_ignorecase else '', pat, fs) st, res, err = utils.ProcessCommand('Find files', 'Searching for "{}" in files with "{}" in their name'.format(pat, fs), cmd, path=path).run() if st == -100: # stopped by user return RetCode.full_redisplay, None if err: log.warning('Cannot grep "{}" in "{}" files: {}'.format(pat, fs, str(err).replace('\n', ' '))) DialogError('Cannot grep "{}" in "{}" files\n{}'.format(pat, fs, str(err))) return RetCode.full_redisplay, None app.history.append('find', fs) app.history.append('grep', pat) if not res.strip(): DialogError('Did not find "{}" in any file with "{}" in name'.format(pat, fs)) return RetCode.full_redisplay, None entries = sorted([f.strip() for f in res.split('\n') if f.strip()]) title, entry = 'Find "{}" pattern in "{}" files'.format(pat, fs), '' else: log.debug('Find "{}" files in {}'.format(fs, path)) cmd = '{} "{}" -{}name "{}" -print'.format(SYSPROGS['find'], path, 'i' if app.cfg.options.find_ignorecase else '', fs) st, res, err = utils.ProcessCommand('Find files', 'Searching for files with "{}" in their name'.format(fs), cmd, path=path).run() if st == -100: # stopped by user return RetCode.full_redisplay, None if err: log.warning('Cannot find "{}" files: {}'.format(fs, str(err).replace('\n', ' '))) DialogError('Cannot find "{}" files\n{}'.format(fs, str(err))) return RetCode.full_redisplay, None app.history.append('find', fs) if not res.strip(): DialogError('Did not find any file with "{}" in name'.format(fs)) return RetCode.full_redisplay, None entries = sorted([f.strip().replace(path, '') for f in res.split('\n') if f.strip() and f!=path]) title, entry = 'Find "{}" files'.format(fs), '' # common while True: ans, entry = DialogFindGrep(title, entries, entry).run() if pat: try: filename, lineno, _ = entry.split(':', 2) except ValueError: # entry = "-1 - Binary file .hg/store/data/lfm/utils.py.i matches" filename = entry if filename.startswith('Binary file'): filename = filename[11:] if filename.endswith('matches'): filename = filename[:-7] filename, lineno = filename.strip(), 0 else: filename = entry pfilename = join(path, filename) if ans == 0: # goto file tab.goto_folder(join(tab.fs.path_str, dirname(filename))) tab.i = tab.fs.pos(basename(filename)) tab.fix_limits() return RetCode.full_redisplay, None elif ans == 1: # panelize files = list() for f in entries: if f.startswith('Binary file'): f = f[11:] if f.endswith('matches'): f = f[:-7] f = f.split(':', 1)[0].strip() # FIXME: won't work if filename contains ':' chars if f not in files: files.append(f) tab.goto_folder(tab.fs.path_str, files=files) return RetCode.full_redisplay, None elif ans == 2: # view cmd = app.cfg.programs['pager'] cmd = '{} +{}'.format(cmd, lineno) if pat else cmd utils.run_on_current_file(cmd, pfilename) elif ans == 3: # edit cmd = app.cfg.programs['editor'] cmd = '{} +{}'.format(cmd, lineno) if pat else cmd utils.run_on_current_file(cmd, pfilename) elif ans == 4: # do something on file cmd = DialogEntry('Execute command on file(s)', 'Enter command', '', history=app.history['exec'][:], is_files=False).run() if not cmd: continue app.history.append('exec', cmd) utils.run_on_current_file(cmd, pfilename) else: return refresh() @public def show_tree(): tab = app.pane_active.tab_active log.debug('Tree in "{}"'.format(tab.dirname)) res = TreeView(tab.dirname).run() if res != -1: tab.goto_folder(res) return RetCode.full_redisplay, None @public def main_menu(): menu = ['/ Find/grep file(s)', '# Show directories size', 's Sort files', 't Tree', 'f Show filesystems info', 'o Open shell', 'c Edit configuration', 'k Edit keys', 'e Edit theme', 'h Delete history'] ret = SelectItem('Main Menu', menu, min_height=True).run() if ret != -1: ch = ret[0] if ch == '/': return find_grep() elif ch == '#': return show_dirs_size() elif ch == 's': return sort_files() elif ch == 't': return show_tree() elif ch == 'f': return show_filesystems_info() elif ch == 'o': return open_shell() elif ch == 'c': utils.run_on_current_file(app.cfg.programs['editor'], CONFIG_FILE) from ui import init_config app.cfg = init_config(False) return refresh() elif ch == 'k': utils.run_on_current_file(app.cfg.programs['editor'], KEYS_FILE) app.init_keys() return refresh() elif ch == 'e': utils.run_on_current_file(app.cfg.programs['editor'], THEME_FILE) app.init_colors() return refresh() elif ch == 'h': try: app.history.delete() except Exception as e: DialogError('Cannot save history file\n{}'.format(str(e))) return RetCode.nothing, None return RetCode.nothing, None else: return RetCode.nothing, None @public def file_menu(): menu = ['@ Exec on file', 'i File info', 'p Change file permissions', 'o Change file owner and/or group', 'a Backup file(s)', 'd Diff file with backup', # 'l Folder comparation', # 'y Folder synchronization', 'z Compress/uncompress file(s)…', 'x Uncompress file', 'u Uncompress file in other panel', 'c Compress directory to format…'] ret = SelectItem('File Menu', menu, min_height=True).run() if ret != -1: ch = ret[0] if ch == '@': return exec_on_file() elif ch == 'i': return show_file_info() elif ch == 'p': return change_perms() elif ch == 'o': return change_owner() elif ch == 'a': return backup_file() elif ch == 'd': return diff_file_with_backup() elif ch == 'l': pass elif ch == 'y': pass elif ch == 'z': return compress_uncompress_file() elif ch == 'x': return uncompress_dir() elif ch == 'u': return uncompress_dir_other_pane() elif ch == 'c': return compress_dir() return RetCode.nothing, None else: return RetCode.nothing, None @public def help_menu(): menu = ['r Readme', 'n News', 't Todo', 'l License', 'k Key bindings'] ret = SelectItem('Help Menu', menu, min_height=True).run() if ret != -1: if ret[0] == 'k': from preferences import dump_keys_to_file dump_keys_to_file(app.keys) filename = DUMP_KEYS_FILE utils.run_on_current_file(app.cfg.programs['pager'], filename) else: docfile = {'r': 'README', 'n': 'NEWS', 't': 'TODO', 'l': 'COPYING'}.get(ret[0]) try: filename = pkg_resources.resource_filename('lfm', 'doc/' + docfile) utils.run_on_current_file(app.cfg.programs['pager'], filename) except NotImplementedError: buf = str(pkg_resources.resource_string('lfm', 'doc/' + docfile), 'UTF-8') InternalView(docfile, [(l, 'view_white_on_black') for l in buf.splitlines()], center=False).run() return refresh() else: return RetCode.nothing, None @public def toggle_powercli(): app.cli.toggle() app.display_statusbar_or_powercli() return refresh() @public def open_shell(): utils.run_shell(app.cfg.programs['shell'], app.pane_active.tab_active.dirname) return refresh() @public def quit_chdir(): return do_quit(chdir=True) @public def quit_nochdir(): return do_quit(chdir=False) def do_quit(chdir=True): if app.cfg.confirmations.quit: ch = DialogConfirm('Last File Manager', 'Quit Last File Manager?', 1) if ch == 0: return RetCode.full_redisplay, None fs = app.pane_active.tab_active.fs path = dirname(fs.base) if fs.vfs else fs.pdir for tab in app.pane1.tabs + app.pane2.tabs: tab.close() return RetCode.quit_chdir if chdir else RetCode.quit_nochdir, path ######################################################################## lfm-3.1/lfm/utils.py0000644000175000001440000010231213066244055013453 0ustar inigousers# -*- coding: utf-8 -*- import os import sys import pwd import grp import difflib import stat import errno import pkg_resources from ctypes import CDLL from datetime import datetime from collections import OrderedDict from shutil import copytree, copy2, copystat, move, rmtree from signal import SIGCONT, SIGKILL, SIGSTOP from subprocess import check_output, Popen, PIPE, STDOUT from multiprocessing import Process, Queue, Pipe from os.path import basename, dirname, exists, expanduser, expandvars, getsize, \ isabs, isdir, isfile, islink, join, normpath, relpath from string import whitespace, punctuation from time import asctime, localtime, sleep, strftime, time, tzname from common import * ######################################################################## ##### Module variables use_wide_chars = False ######################################################################## ###### LFM def get_lfm_data_file_contents(filename): return str(pkg_resources.resource_string('lfm', 'etc/' + filename), 'UTF-8') ######################################################################## ##### Decorators # Decorator for public callable actions from functools import wraps from inspect import getmembers, isfunction def public(f): @wraps(f) def wrapper(*a, **k): return f(*a, **k) setattr(wrapper, 'public', True) return wrapper def get_public_actions(): import actions fns = getmembers(actions, isfunction) return [name for name, fn in fns if hasattr(fn, 'public')] def is_public_api(fn): return hasattr(fn, 'public') # Decorator for catching OSError def catch_os_exception(f): @wraps(f) def wrapper(*a, **k): try: return f(*a, **k) except OSError as err: return err return wrapper ######################################################################## ##### ConfigParser with Comments and Header from configparser import ConfigParser class ConfigParserWithComments(ConfigParser): def __init__(self, header): self._header = header self.optionxform = str super(ConfigParser, self).__init__() def write(self, fp): if self._header: fp.write('{}\n\n'.format(self._header)) if self._defaults: fp.write('[{}]\n'.format(ConfigParser.DEFAULTSECT)) for (key, value) in self._defaults.items(): self._write_item(fp, key, value) fp.write('\n') for section in self._sections: fp.write('[{}]\n'.format(section)) for (key, value) in self._sections[section].items(): self._write_item(fp, key, value) fp.write('\n') def _write_item(self, fp, key, value): if key == '#' and value: for l in value.split('\n'): fp.write('# {}\n'.format(l)) else: fp.write('{}: {}\n'.format(key, str(value).replace('\n', '\n\t'))) ######################################################################## ##### PathContents class PathContents: def __init__(self, fs, basepath): self.basepath = basepath self.__entries = dict() for f in fs: try: if islink(f): self.__entries[f] = (f, 0, '') elif isdir(f): self.__entries[f] = (f, getsize(f), '') self.__fill_contents(f) else: self.__entries[f] = (f, getsize(f), '') except OSError as err: self.__entries[f] = (f, 0, self.__format_err(f, err)) self.__entries = sorted(self.__entries.values()) self.length = len(fs) self.tlength = len(self.__entries) self.tsize = sum([f[1] for f in self.__entries]) or 1 def __fill_contents(self, path): for root, dirs, files in os.walk(path, topdown=False, onerror=self.__on_error): for f in dirs + files: fullpath = join(root, f) try: if islink(fullpath): self.__entries[fullpath] = (fullpath, 0, '') else: self.__entries[fullpath] = (fullpath, getsize(fullpath), '') except OSError as err: self.__entries[fullpath] = (fullpath, 0, self.__format_err(fullpath, err)) def __on_error(self, err): fullpath = join(self.basepath, err.filename) self.__entries[fullpath] = (fullpath, 0, self.__format_err(fullpath, err)) def __format_err(self, filename, err): return Exception('[Errno {}] {}: \'{}\''.format(err.errno, err.strerror, filename.replace(self.basepath, ''))) def __repr__(self): return 'PathContents[Base:"{}" with {} entries (Total: {} items, {:.2f} KB)]' \ .format(self.basepath, self.length, self.tlength, self.tsize/1024) def remove_files(self, fs): new, size = list(), 0 for f, s, e in self.__entries: if f not in fs: new.append((f, s, e)) size += s self.__entries = new self.length = self.length # not really, but not important either self.tlength = len(self.__entries) self.tsize = size @property def entries(self): return sorted(self.__entries, reverse=False) @property def entries_rev(self): return sorted(self.__entries, reverse=True) ######################################################################## ##### DirsTree class DirsTree: def __init__(self, path, dotfiles): self._dotfiles = dotfiles self.build(path) def __get_graph(self, path): """return a OrderedDict with tree structure""" d, expanded = dict(), None while path: if path==os.sep and os.sep in d: break d[path] = (get_dirs(path, self._dotfiles), expanded) expanded = basename(path) path = dirname(path) return OrderedDict(sorted(d.items(), key=lambda t: t[0])) def __get_node(self, i, td, parent): """expand branch. Each node has (name, depth, fullname))""" dirs, expanded_node = td[list(td.keys())[i]] if not expanded_node: return list() lst = list() for d in dirs: lst.append((d, i, join(parent, d))) if d == expanded_node: lst2 = self.__get_node(i+1, td, join(parent, d)) if lst2 is not None: lst.extend(lst2) return lst def __getitem__(self, i): return self._data[i] def __len__(self): return len(self._data) def build(self, path): """build list with tree structure""" td = self.__get_graph(path) self._data = [(os.sep, -1, os.sep)] self._data.extend(self.__get_node(0, td, os.sep)) self.path = path def regenerate_from_pos(self, newpos): """regenerate tree when changing to a new directory and return it""" newpath = self._data[newpos][2] self.build(newpath) return newpath @property def pos(self): """return position of current dir""" for i in range(len(self._data)): if self.path == self._data[i][2]: return i else: return -1 @property def cur_depth(self): return self.get_depth(self.pos) def get_depth(self, pos): return self._data[pos][1] @property def is_first_sibling(self): return self.cur_depth != self.get_depth(self.pos-1) @property def first_sibling_pos(self): newpos = self.pos while True: if newpos-1 < 0 or self.cur_depth != self.get_depth(newpos-1): break newpos -= 1 return newpos @property def is_last_sibling(self): return self.cur_depth != self.get_depth(self.pos+1) @property def last_sibling_pos(self): newpos = self.pos while True: if newpos+1 == len(self) or self.cur_depth != self.get_depth(newpos+1): break newpos += 1 return newpos @property def parent_pos(self): for i in range(self.pos-1, -1, -1): if self.get_depth(i) == self.cur_depth-1: break return i @property def has_children_dirs(self): return len(get_dirs(self.path, self._dotfiles)) > 0 def to_child(self): newpath = None child_dirs = get_dirs(self.path, self._dotfiles) if len(child_dirs) > 0: newpath = join(self.path, child_dirs[0]) self.build(newpath) return newpath def show_tree(self, a=0, z=-1): """show an ascii representation of the tree. Not used in lfm""" if z>len(self._data) or z==-1: z = len(self._data) for i in range(a, z): name, depth, fullname = self._data[i] if fullname == self.path: name += ' <=====' if name == os.sep: print(' ' + name) else: print(' | ' * depth + ' +- ' + name) ######################################################################## ##### Paths def get_realpath(tab): if tab.fs[tab.i].is_link: try: return '-> ' + os.readlink(join(tab.dirname, tab.current_filename)) except OSError: return tab.fs.path_str + '/' + tab.current_filename else: return tab.fs.path_str + '/' + tab.current_filename def get_relpath(path, base): return relpath(path, base) if path.startswith(base) else path def get_norm_path(path, basedir): path = expandvars(expanduser(path)) path = path if isabs(path) else join(basedir, path) return normpath(path) def get_dir_size(dirname): try: buf = check_output(['du', '-sb', dirname], stderr=STDOUT, universal_newlines=True) return int(buf.split()[0]) except: return -1 def get_dirs(path, dotfiles): try: if dotfiles: ds = [d for d in os.listdir(path) if isdir(join(path, d))] else: ds = [d for d in os.listdir(path) if d[0]!='.' and isdir(join(path, d))] except OSError: return list() return sorted(ds) ######################################################################## ##### Files def get_filetype(pfile): """Returns the type of a file as FileType""" lmode = os.lstat(pfile).st_mode if stat.S_ISDIR(lmode): return FileType.dir if stat.S_ISLNK(lmode): try: mode = os.stat(pfile)[stat.ST_MODE] except OSError: return FileType.nlink else: return FileType.link2dir if stat.S_ISDIR(mode) else FileType.link if stat.S_ISCHR(lmode): return FileType.cdev if stat.S_ISBLK(lmode): return FileType.bdev if stat.S_ISFIFO(lmode): return FileType.fifo if stat.S_ISSOCK(lmode): return FileType.socket if stat.S_ISREG(lmode) and (lmode & 0o111): return FileType.exe else: return FileType.reg # if no other type, regular file def get_file_info(filename): try: return check_output(['file', '-b', filename], stderr=STDOUT, universal_newlines=True).strip() except: return 'no type information' def backup_file(src, backup_ext): dest = src + backup_ext if exists(dest): raise FileExistsError('Cannot backup file:\n"{}" already exists'.format(dest)) try: return copy_bulk(src, dest) except OSError as err: raise Exception('Cannot backup file:\n{}'.format(err)) def get_file_diff(file_old, file_new, diff_type): try: d0 = datetime.fromtimestamp(os.stat(file_old).st_mtime) date_old = d0.strftime(' %Y-%m-%d %H:%M:%S.%f ') + tzname[0] d1 = datetime.fromtimestamp(os.stat(file_new).st_mtime) date_new = d1.strftime(' %Y-%m-%d %H:%M:%S.%f ') + tzname[0] # with open(file_old).readlines() as buf0, open(file_new).readlines() as buf1: with open(file_old) as f0, open(file_new) as f1: buf0, buf1 = f0.readlines(), f1.readlines() if diff_type == 'context': diff = difflib.context_diff(buf0, buf1, file_old, file_new, date_old, date_new) elif diff_type == 'unified': diff = difflib.unified_diff(buf0, buf1, file_old, file_new, date_old, date_new) elif diff_type == 'ndiff': diff = difflib.ndiff(buf0, buf1) return ''.join(diff) except: return '' def copy_file(filename, basepath, destdir, overwrite=False): partial = filename.replace(basepath, '') dest = join(destdir, partial) if isdir(destdir) else destdir try: dest_exists = exists(dest) if dest_exists and not overwrite: raise LFMFileExistsError(dest) if islink(filename) or isfile(filename): if dest_exists: os.unlink(dest) copy2(filename, dest, follow_symlinks=False) elif isdir(filename): if dest_exists: try: os.rmdir(dest) except OSError as err: if err.errno != errno.ENOTEMPTY: # Directory not empty raise try: os.mkdir(dest) except OSError as err: if err.errno != errno.EEXIST: # File exists raise try: st = os.lstat(src) os.chown(dest, st[stat.ST_UID], st[stat.ST_GID]) copystat(src, dest, follow_symlinks=False) except: pass else: raise Exception('Cannot copy file!\nCannot copy special file: \'{}\''.format(partial)) except OSError as err: raise Exception('Cannot copy file!\n[Errno {}] {}: \'{}\''.format(err.errno, err.strerror, partial)) def move_file(filename, basepath, destdir, overwrite=False): # alternative method instead of copy & delete. Faster but less control partial = filename.replace(basepath, '') dest = join(destdir, partial) if isdir(destdir) else destdir try: if exists(dest) and not overwrite: raise LFMFileExistsError(dest) move(filename, dest) except OSError as err: raise Exception('Cannot move file!\n[Errno {}] {}: \'{}\''.format(err.errno, err.strerror, partial)) def delete_file(filename, basepath): try: if islink(filename): os.unlink(filename) elif isdir(filename): os.rmdir(filename) else: os.unlink(filename) except OSError as err: raise Exception('Cannot delete file!\n[Errno {}] {}: \'{}\''.format(err.errno, err.strerror, filename.replace(basepath, ''))) @catch_os_exception def make_dir(newdir): os.makedirs(newdir) @catch_os_exception def touch_file(filename): with open(filename, 'a'): os.utime(filename) @catch_os_exception def rename_file(src, dest): os.rename(src, dest) @catch_os_exception def link_create(linkname, pointto): os.symlink(pointto, linkname) @catch_os_exception def link_edit(linkname, pointto): os.unlink(linkname) os.symlink(pointto, linkname) ##### copy, overwrite, delete def copy_bulk(src, dest): if isdir(src): copytree(src, dest, symlinks=True) elif isfile(src): copy2(src, dest) def overwrite_vfsfile(src, dest): copystat(dest, src) # copy attrs move(src, dest) def delete_bulk(path, ignore_errors=False): if isdir(path): rmtree(path, ignore_errors=ignore_errors) elif isfile(path): if ignore_errors: try: os.unlink(path) except OSError: pass else: os.unlink(path) ######################################################################## ##### Un/compress from compress import get_compressed_file_engine, packagers_by_type, PackagerTAR def uncompress_dir(filename, destdir): if not isfile(filename): raise Exception('It\'s not a file: {}'.format(basename(filename))) c = get_compressed_file_engine(filename) if c is None: raise Exception('Cannot uncompress this file type: {}'.format(basename(filename))) res, err = run_in_cli(c.cmd_uncompress, destdir) if err: raise Exception(err) return res def compress_dir(dir, typ): if not isdir(dir): raise Exception('It\'s not a directory: {}'.format(basename(dir))) c = packagers_by_type[typ](dir) if c is None: raise Exception('Cannot compress: {}'.format(basename(dir))) res, err = run_in_cli(c.cmd_compress, dirname(dir)) if err: raise Exception(err) return res def compress_uncompress_file(filename, typ): if not isfile(filename): raise Exception('It\'s not a file: {}'.format(basename(filename))) c = get_compressed_file_engine(filename) if c is None or isinstance(c, PackagerTAR): cmd = packagers_by_type[typ](filename).cmd_compress elif c.type == typ: cmd = c.cmd_uncompress else: raise Exception('Cannot un/compress \'{}\' with type {}'.format(basename(filename), typ)) res, err = run_in_cli(cmd, dirname(filename)) if err: raise Exception(err) return res ######################################################################## ##### FileSystems def get_filesystems_info(): return check_output(['df', '-h'], stderr=STDOUT, universal_newlines=True) def get_mount_points(): """return system mount points as list of (mountpoint, device, fstype). Compatible with linux and solaris""" buf = check_output(['mount'], stderr=STDOUT, universal_newlines=True).strip() lst = [(e.split()[2], e.split()[0], e.split()[4]) for e in buf.split('\n')] return sorted(lst, reverse=True) def get_mountpoint_for_file(filename): try: for m, d, t in get_mount_points(): if filename.find(m) != -1: return (m, d, t) else: raise except: return ('/', '', '') ######################################################################## ##### Users & Groups def get_user_fullname(user): try: return pwd.getpwnam(user)[4] except KeyError: return '' def get_owners(): """get a list with the users defined in the system""" return sorted([e[0] for e in pwd.getpwall()]) def get_groups(): """get a list with the groups defined in the system""" return sorted([e[0] for e in grp.getgrall()]) ######################################################################## ##### Binary programs def get_binary_programs(): return sorted({f for p in os.getenv('PATH').split(':') if isdir(p) for f in os.listdir(p) if isfile(join(p, f))}) ######################################################################## ##### String formatting def size2str(size): """Converts a file size into a string""" if size >= 1000000000: return str(size//1048576) + 'M' # 1024*1024 elif size >= 10000000: return str(size//1024) + 'K' else: return str(size) def num2str(num): # Thanks to "Fatal" in #pys60 num = str(num) return (len(num) < 4) and num or (num2str(num[:-3])+","+num[-3:]) def time2str(t, short=True): """Converts a file time into a string""" if -15552000 < (time() - t) < 15552000: # date < 6 months from now, past or future fmt = '%d %b %H:%M' if short else '%a %b %d %H:%M' else: fmt = '%d %b %Y' if short else '%a %d %b %Y' return strftime(fmt, localtime(t)) def time2str_full(t): return asctime(localtime(t)) def perms2str(perms): """Converts a file permisions into a string""" return stat.filemode(perms)[1:].lower() def type2str(ftype): """Converts a file type into a string""" return FILETYPES[ftype][0] def rdev2str(rdev): """Converts a device file numbers into a string""" return ('%d,%d' % rdev).rjust(7) def owner2str(uid): try: return pwd.getpwuid(uid).pw_name except KeyError: return str(uid) def group2str(gid): try: return grp.getgrgid(gid).gr_name except KeyError: return str(gid) def str2perms(perms): ps = 0 for i, p in enumerate(reversed(perms)): if p == 'x': ps += 1 * 8**(i//3) elif p == 'w': ps += 2 * 8**(i//3) elif p == 'r': ps += 4 * 8**(i//3) elif p == 't' and i == 0: ps += 1 * 8**3 elif p == 's': if i == 6: ps += 4 * 8**3 elif i == 3: ps += 2 * 8**3 return ps ######################################################################## ##### Support for wide chars def length(text): return wchar_len(text) if use_wide_chars else len(text) def max_length(entries): return max(map(wchar_len if use_wide_chars else len, entries)) def text2wrap(*args, **kw): return text2wrap_wchar(*args, **kw) if use_wide_chars else text2wrap_normal(*args, **kw) def text2wrap_normal(text, max_width, ljust=True, start_pct=.66, sep='~', fill=True): """Returns a displayable string from an attribute""" length = len(text) if length <= max_width: if ljust: return text.ljust(max_width) if fill else text else: return text.ljust(max_width) if ljust else text.rjust(max_width) else: until = int(max_width*start_pct) return text[:until] + sep + text[-(max_width-until-1):] def text2wrap_wchar(text, max_width, ljust=True, start_pct=.66, sep='~', fill=True): """Returns a displayable string from an attribute""" length = wchar_len(text) if length <= max_width: if ljust: return wchar_ljust(text, max_width) if fill else text else: return wchar_ljust(text, max_width) if ljust else wchar_rjust(text, max_width) else: return wchar_fill(text, max_width, start_pct, sep) def wchar_len(text): libc = CDLL('libc.so.6') # assert libc.wcswidth('世界')==4 return len(text) if len(text)>127 else libc.wcswidth(text) # BUG: wcswidth doesn't accept string with 128+ chars def wchar_ljust(text, max_width): return text + ' '*(max_width-wchar_len(text)) def wchar_rjust(text, max_width): return ' '*(max_width-wchar_len(text)) + text def wchar_fill(text, max_width, pct, sep): pos = int(max_width*pct) buf1 = buf2 = '' for c in text: if wchar_len(buf1) + wchar_len(c) > pos: break buf1 += c buf1 += sep len_buf1 = wchar_len(buf1) for c in text[::-1]: if len_buf1 + wchar_len(c) + wchar_len(buf2) > max_width: break buf2 += c return buf1+buf2[::-1] ######################################################################## ##### Navigate through paths stepchars = whitespace + punctuation def prev_step(text, pos): pos = max(0, pos-2) while pos > 0 and text[pos] not in stepchars: pos -=1 return pos+1 if pos > 0 else 0 def next_step(text, pos): pos += 1 l = len(text) while pos < l and text[pos] not in stepchars: pos +=1 return pos+1 if pos < l else l ######################################################################## ##### Running commands in shell import curses def escape_str(buf): if buf.find('"') != -1: return '\'{}\''.format(buf.replace('"', '\\"')) else: return '"{}"'.format(buf) def escape_command(cmd, filename, background): filename = filename.replace('$', '\$') return '{} {}{}'.format(cmd, escape_str(filename), ' >/dev/null 2>&1 &' if background else '') def run_on_current_file(program, filename, background=False): curses.endwin() os.system(escape_command(program, filename, background)) curses.curs_set(0) def run_in_background(cmd, path): cmd = 'cd {} && {} >/dev/null 2>&1 &'.format(escape_str(path), cmd) curses.endwin() os.system(cmd) curses.curs_set(0) def run_shell(shell, path): curses.endwin() os.system('cd "{}" && {}'.format(path.replace('"', '\\"'), shell)) curses.curs_set(0) def run_in_cli(cmd, path): return Popen(cmd, shell=True, cwd=path, stdout=PIPE, stderr=PIPE, universal_newlines=True).communicate() ###################################################################### ##### Process classes from ui_widgets import CursorAnimation, DialogConfirm, DialogConfirmAll, DialogConfirmAllNone, \ DialogError, DialogMessagePanel, DialogProgress1Panel, DialogProgress2Panel ##### ProcessCommand class ProcessCommand: def __init__(self, title, subtitle, cmd, path=None): self.title = title self.subtitle = subtitle self.cmd = cmd self.path = path self.proc = None self.status = None self.dialog = DialogMessagePanel(self.title, self.subtitle) self.animation = CursorAnimation() def check_stop(self): if self.dialog.check_key() == 0x03: self.proc.send_signal(SIGSTOP) self.dialog.hide() if DialogConfirm('Stop process', '{} {}'.format(self.title, self.subtitle), 0) == 1: self.proc.kill() return True else: self.dialog.show() self.proc.send_signal(SIGCONT) return False def run(self): self.dialog.show() results, errors = None, None self.proc = Popen(self.cmd, shell=True, cwd=self.path, stdout=PIPE, stderr=PIPE, universal_newlines=True) if self.proc.poll(): # process can finish too fast while True: self.animation.next() self.status = self.proc.poll() if self.status is not None: break if self.check_stop(): self.status = -100 break sleep(0.05) self.dialog.hide() results, errors = self.proc.communicate() return self.status, results, errors ##### ProcessFuncLoop class ProcessFuncLoop: def __init__(self, title, fn, lst): self.title = title self.fn = fn self.lst = lst self.i, self.n = 0, len(lst) self.proc = None self.animation = CursorAnimation() def check_stop(self): if self.dialog.check_key() == 0x03: os.kill(self.proc.pid, SIGSTOP) self.dialog.hide() if DialogConfirm('Stop process', 'Stop "{}"?'.format(self.title), 0) == 1: os.kill(self.proc.pid, SIGKILL) return True else: self.dialog.show() os.kill(self.proc.pid, SIGCONT) return False def run_func(self, fn, qin, qout, qerr, conn): while not qin.empty(): if qin.empty(): break try: ret = self.do_run(fn, qin, conn) except Exception as err: qout.put_nowait(None) qerr.put_nowait(err) if not isinstance(err, LFMFileSkipped): conn.send((ProcCode.error, str(err))) else: qout.put_nowait(ret) qerr.put_nowait(None) # sleep(0.001) conn.send((ProcCode.end, )) sleep(0.25) def run(self): self.prepare() self.dialog.show() qin, qout, qerr = Queue(maxsize=self.n), Queue(maxsize=self.n), Queue(maxsize=self.n) for a in self.lst: qin.put(a) conn_parent, conn_child = Pipe() self.proc = Process(target=self.run_func, args=(self.fn, qin, qout, qerr, conn_child)) self.proc.start() while True: if conn_parent.poll(): buf = conn_parent.recv() if buf[0] == ProcCode.end: conn_parent.send(ProcCodeConfirm.stop) status = ProcCode.end rets, errs = list(), list() while not qout.empty(): rets.append(qout.get_nowait()) while not qerr.empty(): errs.append(qerr.get_nowait()) break elif buf[0] == ProcCode.error: self.dialog.hide() msg = buf[1] if buf[1].startswith('Cannot') else 'Cannot {}!\n{}'.format(self.title.lower(), buf[1]) DialogError(msg) self.dialog.show() elif buf[0] == ProcCode.next: self.display_next(*buf[1:]) ans = self.confirm_pre() conn_parent.send(ans) if ans == ProcCodeConfirm.stop: status, rets, errs = ProcCode.stopped, None, None break elif buf[0] == ProcCode.confirm: ans = self.confirm_post(buf[1]) conn_parent.send(ans) if ans == ProcCodeConfirm.stop: status, rets, errs = ProcCode.stopped, None, None break if self.check_stop(): status, rets, errs = ProcCode.stopped, None, None break self.animation.next() # sleep(0.05) self.dialog.hide() try: os.kill(self.proc.pid, SIGKILL) except ProcessLookupError: pass return status, rets, errs def prepare(self): self.dialog = DialogProgress1Panel(self.title) def display_next(self, text, *args): self.i += 1 self.dialog.update(text, self.i, self.n) def confirm_pre(self): return ProcCodeConfirm.ok def confirm_post(self, *args): return ProcCodeConfirm.ok def do_run(self, fn, qin, conn): args = qin.get_nowait() conn.send((ProcCode.next, args[0])) _ = conn.recv() return fn(*args) ##### ProcessFuncDeleteLoop class ProcessFuncDeleteLoop(ProcessFuncLoop): def __init__(self, title, fn, lst, tot_size, confirm): super(ProcessFuncDeleteLoop, self).__init__(title, fn, lst) self.tot_size = tot_size self.confirm = confirm def prepare(self): self.dialog = DialogProgress2Panel(self.title) self.acc_size = 0 def display_next(self, text, size, *args): self.i += 1 self.acc_size += size self.cur_elm = text self.dialog.update(text, self.i, self.n, self.acc_size, self.tot_size) def confirm_pre(self): if self.confirm: self.dialog.hide() ans = DialogConfirmAll(self.title, 'Delete \'{}\'?'.format(self.cur_elm), default=1) if ans == 1: ret = ProcCodeConfirm.ok elif ans == 2: self.confirm = False ret = ProcCodeConfirm.ok elif ans == 0: ret = ProcCodeConfirm.skip else: # ans == -1 ret = ProcCodeConfirm.stop self.dialog.show() return ret else: return ProcCodeConfirm.ok def do_run(self, fn, qin, conn): filename, size, err, basepath = qin.get_nowait() conn.send((ProcCode.next, filename.replace(basepath, ''), size)) ans = conn.recv() if err: raise err if ans == ProcCodeConfirm.ok: return fn(filename, basepath) elif ans == ProcCodeConfirm.skip: raise LFMFileSkipped(filename) else: return None ##### ProcessFuncCopyLoop class ProcessFuncCopyLoop(ProcessFuncLoop): def __init__(self, title, fn, lst, tot_size, confirm_overwrite): super(ProcessFuncCopyLoop, self).__init__(title, fn, lst) self.tot_size = tot_size self.overwrite = None if confirm_overwrite else ProcCodeConfirm.ok def prepare(self): self.dialog = DialogProgress2Panel(self.title) self.acc_size = 0 def display_next(self, text, size, *args): self.i += 1 self.acc_size += size self.cur_elm = text self.dialog.update(text, self.i, self.n, self.acc_size, self.tot_size) def confirm_post(self, filename): if self.overwrite is None: self.dialog.hide() ans = DialogConfirmAllNone(self.title, 'Overwrite \'{}\'?'.format(filename), default=1) if ans == 1: ret = ProcCodeConfirm.ok elif ans == 2: ret = self.overwrite = ProcCodeConfirm.ok elif ans == 0: ret = ProcCodeConfirm.skip elif ans == -2: ret = self.overwrite = ProcCodeConfirm.skip else: # ans == -1 ret = ProcCodeConfirm.stop self.dialog.show() return ret else: return self.overwrite def do_run(self, fn, qin, conn): filename, size, err, basepath, destdir = qin.get_nowait() conn.send((ProcCode.next, filename.replace(basepath, ''), size)) ans = conn.recv() if err: raise err if ans == ProcCodeConfirm.ok: try: return fn(filename, basepath, destdir) except LFMFileExistsError as err: conn.send((ProcCode.confirm, err)) ans = conn.recv() if ans == ProcCodeConfirm.ok: return fn(filename, basepath, destdir, overwrite=True) elif ans == ProcCodeConfirm.skip: raise LFMFileSkipped(filename) else: return None else: return None ######################################################################## lfm-3.1/lfm/lfm.py0000755000175000001440000001136413123762510013075 0ustar inigousers#!/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2001-17 Iñigo Serna # Time-stamp: <2017-06-25 18:31:04 inigo> # # 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 . """lfm v3.0 - (C) 2001-17, by Iñigo Serna 'Last File Manager' is a powerful file manager for UNIX console. It has a curses interface and it's written in Python version 3.4+. Released under GNU Public License, read COPYING file for more details. """ __author__ = 'Iñigo Serna' __revision__ = '3.0' import os from os.path import join, exists import sys import argparse import logging from common import * from ui import run_app ######################################################################## ###################################################################### ##### Main def lfm_exit(ret_code, ret_path='.'): with open('/tmp/lfm-%s.path' % (os.getppid()), 'w') as f: f.write(ret_path) sys.exit(ret_code) def helper_delete_item(filename, text): try: os.unlink(filename) except OSError as err: print('lfm - ERROR: {}\n{}'.format(text, err)) else: print('lfm: {}!'.format(text)) sys.exit(0) def lfm_start(): parser = argparse.ArgumentParser(description=__doc__.split('\n')[0], formatter_class=argparse.RawDescriptionHelpFormatter, epilog='\n'.join(__doc__.split('\n')[2:])) parser.add_argument('-d', '--debug', help='Enable debug level in log file', action='store_true') parser.add_argument('-w', '--use-wide-chars', help='Enable support for wide chars', action='store_true') parser.add_argument('--restore-config', help='Restore default configuration', action='store_true') parser.add_argument('--restore-keys', help='Restore default key bindings', action='store_true') parser.add_argument('--restore-theme', help='Restore default theme', action='store_true') parser.add_argument('--delete-history', help='Delete history', action='store_true') parser.add_argument('path1', nargs='?', default='.', help='Path to show in left pane (default: ".")') parser.add_argument('path2', nargs='?', default='.', help='Path to show in right pane (default: ".")') args = parser.parse_args() if not exists(CONFIG_DIR): os.makedirs(CONFIG_DIR) logging.basicConfig(level=logging.DEBUG if args.debug else DEBUG_LEVEL, # format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', format='%(asctime)s %(name)s(%(module)12s:%(lineno)4d) %(levelname).1s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', filename=join(CONFIG_DIR, 'lfm.log'), filemode='w') if args.restore_config: helper_delete_item(CONFIG_FILE, 'restore default configuration') if args.restore_keys: helper_delete_item(KEYS_FILE, 'restore default key bindings') if args.restore_theme: helper_delete_item(THEME_FILE, 'restore default theme') if args.delete_history: helper_delete_item(HISTORY_FILE, 'delete history') try: pwd = os.getcwd() os.chdir(args.path1) os.chdir(args.path2) os.chdir(pwd) except OSError as err: print('lfm - ERROR: cannot initialize path\n{}'.format(err)) sys.exit(-1) logging.info('Start lfm') try: path = run_app(args.path1, args.path2, args.use_wide_chars) except FileNotFoundError as err: print('ERROR: Cannot copy default theme or keys file to user configuration folder {}\n{}\nQuitting'.format(CONFIG_DIR, err)) sys.exit(-1) except BaseException as err: import traceback tb_str = traceback.format_exc() logging.critical('ERROR: ' + str(err)) logging.critical(tb_str) print('ERROR:', str(err)) print(tb_str) path = None raise logging.info('End lfm, returns: "{}"'.format(path)) if path is not None: lfm_exit(0, path) else: lfm_exit(0) ######################################################################## if __name__ == '__main__': lfm_start() ######################################################################## lfm-3.1/lfm/preferences.py0000644000175000001440000004170513066234651014625 0ustar inigousers# -*- coding: utf-8 -*- from os.path import exists from shutil import copyfile from configparser import ConfigParser from collections import OrderedDict import pickle from utils import get_lfm_data_file_contents, ConfigParserWithComments, get_public_actions from key_defs import key_str2bin, key_bin2str from common import * ######################################################################## ##### Default options and preferences CONFIGFILE_HEADER = '{0} {1} Configuration File v3.x {0}'.format('#'*10, LFM_NAME) DEF_OPTIONS = {'save_configuration_at_exit': True, 'save_history_at_exit': True, 'show_output_after_exec': True, 'use_wide_chars': False, 'rebuild_vfs': False, 'detach_terminal_at_exec': True, 'show_dotfiles': True, 'sort_type': SortType.byName, 'sort_reverse': False, 'sort_mix_dirs': False, 'sort_mix_cases': True, 'automatic_file_encoding_conversion': False, # ask 'find_ignorecase': False, 'grep_ignorecase': True, 'grep_regex': True} OPTIONS_TEXT = 'automatic_file_encoding_conversion: {}\nsort_type: {}'.format("never = -1, ask = 0, always = 1", ', '.join([str(s) for s in SortType])) DEF_CONFIRMATIONS = {'delete': True, 'overwrite': True, 'quit': True, 'ask_rebuild_vfs': True} DEF_MISC = {'backup_extension': '.bak', 'diff_type': 'unified'} MISC_TEXT = 'diff_type: context, unified, ndiff' DEF_PROGRAMS = {'shell': 'bash', 'pager': 'less', 'editor': 'vi', 'web': 'firefox', 'audio': 'vlc', 'video': 'vlc', 'graphics': 'eog', 'pdf': 'evince', 'ebook': 'FBReader'} FILES_EXT = {'archive': ['gz', 'bz2', 'xz', 'lz', 'lz4', 'tar', 'tgz', 'tbz2', 'txz', 'tlz', 'tlz4', 'Z', 'zip', 'rar', '7z', 'arj', 'cab', 'lzh', 'lha', 'zoo', 'arc', 'ark', 'rpm', 'deb'], 'audio': ['au', 'flac', 'mid', 'midi', 'mp2', 'mp3', 'mpg', 'ogg', 'wma', 'xm'], 'data': ['dta', 'nc', 'dbf', 'mdn', 'db', 'mdb', 'dat', 'fox', 'dbx', 'mdx', 'sql', 'mssql', 'msql', 'ssql', 'pgsql', 'cdx', 'dbi', 'sqlite'], 'devel': ['c', 'h', 'cc', 'hh', 'cpp', 'hpp', 'py', 'pyw', 'hs', 'lua', 'pl', 'pm', 'inc', 'rb', 'asm', 'pas', 'f', 'f90', 'pov', 'm', 'pas', 'cgi', 'php', 'phps', 'tcl', 'tk', 'js', 'java', 'jav', 'jasm', 'vala', 'glade', 'ui', 'diff', 'patch', 'css', 'sh', 'bash', 'awk', 'm4', 'el', 'st', 'mak', 'sl', 'ada', 'caml', 'ml', 'mli', 'mly', 'mll', 'mlp', 'prg'], 'document': ['txt', 'text', 'rtf', 'odt', 'odc', 'odp', 'abw', 'gnumeric', 'sxw', 'sxc', 'sxp', 'sdw', 'sdc', 'sdp', 'djvu', 'dvi', 'bib', 'tex', 'doc', 'xls', 'ppt', 'pps', 'docx', 'xlsx', 'pptx', 'xml', 'xsd', 'xslt', 'sgml', 'dtd', 'mail', 'msg', 'letter', 'ics', 'vcs', 'vcard', 'lsm', 'po', 'man', '1', 'info'], 'ebook': ['azw', 'azw3', 'chm', 'epub', 'fb2', 'imp', 'lit', 'mobi', 'prc'], 'graphics': ['jpg', 'jpeg', 'gif', 'png', 'tif', 'tiff', 'pcx', 'bmp', 'xpm', 'xbm', 'eps', 'pic', 'rle', 'ico', 'wmf', 'omf', 'ai', 'cdr', 'xcf', 'dwb', 'dwg', 'dxf', 'svg', 'dia'], 'pdf': ['pdf', 'ps'], 'temp': ['tmp', '$$$', '~', 'bak'], 'web': ['html', 'shtml', 'htm'], 'video': ['acc', 'avi', 'asf', 'flv', 'mkv', 'mov', 'mol', 'mpl', 'med', 'mp4', 'mpg', 'mpeg', 'ogv', 'swf', 'ogv', 'wmv']} POWERCLI_FAVS = ['mv "$f" "{$f.replace(\'\', \'\')}"', 'less "$f" $', 'find "$p" -name "*" -print0 | xargs --null -0 grep -EHcni "TODO|WARNING|FIXME|BUG"', 'find "$p" -name "*" -print0 | xargs --null -0 grep -EHcni "TODO|WARNING|FIXME|BUG" >output.txt &', 'cp $s "$o"', '', '', '', '', ''] ######################################################################## ##### Config class SectContainer: def __init__(self, d): for k, v in d.items(): setattr(self, k, v) def prepare_to_save(self, comment=''): # convert True->1, False->0 except non booleans values NO_BOOL = ('sort_type', 'backup_extension', 'diff_type') d = dict((k, (v if k in NO_BOOL else (1 if v else 0))) for k, v in self.__dict__.items()) if comment: d['#'] = comment return OrderedDict(sorted(d.items(), key=lambda k: k[0])) class Config: """Configuration class for lfm""" def __init__(self): # fill with default values self.options = SectContainer(DEF_OPTIONS) self.confirmations = SectContainer(DEF_CONFIRMATIONS) self.misc = SectContainer(DEF_MISC) self.programs = DEF_PROGRAMS self.files_ext = FILES_EXT self.bookmarks = dict([(b, '/') for b in BOOKMARKS_KEYS]) self.powercli_favs = POWERCLI_FAVS def load(self): if not exists(CONFIG_FILE): log.warning('Configuration file "{}" does not exist, using default values'.format(CONFIG_FILE)) self.save() return try: with open(CONFIG_FILE) as f: hdr = f.readline()[:-1] if hdr and hdr != CONFIGFILE_HEADER: log.error('Configuration file "{}" looks corrupted, using default values'.format(CONFIG_FILE)) return except: log.critical('Can\'t read configuration file "{}", quitting'.format(CONFIG_FILE)) return log.debug('Load configuration file "{}"'.format(CONFIG_FILE)) cp = ConfigParser() cp.read(CONFIG_FILE) if cp.has_section('Options'): for k, v in cp.items('Options'): if k in DEF_OPTIONS: try: exec('self.options.{} = {}'.format(k, v if k=='sort_type' else int(v)!=0)) except: log.warning('CONFIGURATION FILE: Invalid value "{}" for "{}" in Options section'.format(v, k)) else: log.warning('CONFIGURATION FILE: Unknown "{}" in section Options, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Options" section, using defaults') if cp.has_section('Confirmations'): for k, v in cp.items('Confirmations'): if k in DEF_CONFIRMATIONS: try: exec('self.confirmations.{} = {}'.format(k, int(v)!=0)) except: log.warning('CONFIGURATION FILE: Invalid value "{}" for "{}" in Confirmations section'.format(v, k)) else: log.warning('CONFIGURATION FILE: Unknown "{}" in section Confirmations, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Confirmations" section, using defaults') if cp.has_section('Misc'): for k, v in cp.items('Misc'): if k in DEF_MISC: try: if k == 'diff_type' and v not in ('context', 'unified', 'ndiff'): raise ValueError exec('self.misc.{} = "{}"'.format(k, v)) except: log.warning('CONFIGURATION FILE: Invalid value "{}" for "{}" in Misc section'.format(v, k)) else: log.warning('CONFIGURATION FILE: Unknown "{}" in section Misc, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Misc" section, using defaults') if cp.has_section('Programs'): for k, v in cp.items('Programs'): if k in DEF_PROGRAMS: # Don't check if program exists self.programs[k] = v.strip() else: log.warning('CONFIGURATION FILE: Unknown "{}" in section Programs, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Programs" section, using defaults') if cp.has_section('Files'): for k, v in cp.items('Files'): if k in FILES_EXT: self.files_ext[k] = list(map(str.lower, [e.strip() for e in v.split(',')])) else: log.warning('CONFIGURATION FILE: Unknown "{}" key in section Files, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Files" section, filling with defaults') if cp.has_section('Bookmarks'): for k, v in cp.items('Bookmarks'): if k in BOOKMARKS_KEYS: # Don't check if exists # if not exists(v.split()) or not isdir(v.split()): # continue self.bookmarks[k] = v.strip() else: log.warning('CONFIGURATION FILE: Unknown "{}" key in section Bookmarks, ignoring'.format(k)) else: log.warning('CONFIGURATION FILE: No "Bookmarks" section, filling with defaults') if cp.has_section('PowerCLI Favs'): for i, (_, v) in enumerate(cp.items('PowerCLI Favs')): self.powercli_favs[i] = v.strip() else: log.warning('CONFIGURATION FILE: No "PowerCLI Favs" section, filling with defaults') def save(self): log.debug('Save configuration file "{}"'.format(CONFIG_FILE)) cfg = ConfigParserWithComments(CONFIGFILE_HEADER) cfg['Options'] = self.options.prepare_to_save(OPTIONS_TEXT) cfg['Confirmations'] = self.confirmations.prepare_to_save() cfg['Misc'] = self.misc.prepare_to_save(MISC_TEXT) cfg['Programs'] = OrderedDict(sorted(self.programs.items(), key=lambda k: k[0])) d = dict((k, ', '.join(sorted(v))) for k, v in self.files_ext.items()) cfg['Files'] = OrderedDict(sorted(d.items(), key=lambda k: k[0])) cfg['Bookmarks'] = OrderedDict(sorted(self.bookmarks.items(), key=lambda k: k[0])) cfg['PowerCLI Favs'] = OrderedDict(dict(((i, c) for i, c in enumerate(self.powercli_favs)))) try: with open(CONFIG_FILE, 'w') as cfgfile: cfg.write(cfgfile) except: log.error('Couldn\'t write configuration file "{}"'.format(CONFIG_FILE)) raise ######################################################################## ##### Color Theme def load_colortheme(): log.debug('Parse ColorTheme file') if not exists(THEME_FILE): log.warning('ColorTheme file does not exist, copying default') copy_default_colortheme_file() cp = ConfigParser() cp.read(THEME_FILE) if not cp.has_section('Colors'): log.warning('ColorTheme file corrupted, copying default') copy_default_colortheme_file() # parse file colors = dict() rels = [] for it, color_desc in cp.items('Colors'): it = it.strip().lower() if it not in COLOR_ITEMS: log.warning('Color item invalid: {}'.format(it)) continue if color_desc[0] == '=': toit = color_desc[1:] if toit not in COLOR_ITEMS: log.warning('Color item pointed to an invalid item: {}: {}'.format(it, toit)) continue rels.append((it, toit)) continue (fg, bg) = map(str.lower, color_desc.split(maxsplit=1)) if fg not in VALID_COLORS or bg not in VALID_COLORS: log.warning('Color item contain invalid colors: {}: {}'.format(it, color_desc)) continue colors[it] = (fg, bg) # fill ='s for it, toit in rels: if toit not in colors: log.warning('Color item pointed to undefined item: {}: {}'.format(it, toit)) continue colors[it] = colors[toit] # check if missing items for it in set(COLOR_ITEMS) - set(colors.keys()): log.warning('Missing Color item {}, added as white on black'.format(it)) colors[it] = ('white', 'black') return colors def copy_default_colortheme_file(): try: with open(THEME_FILE, 'w') as f: default_colortheme = get_lfm_data_file_contents('lfm-default.theme') f.write(default_colortheme) except: log.error('Can\'t copy default ColorTheme file: {}'.format(THEME_FILE)) raise ######################################################################## ##### Keys def load_keys(): log.debug('Parse Keys file') if not exists(KEYS_FILE): log.warning('Keys file does not exist, copying default') copy_default_keys_file() cp = ConfigParser() cp.read(KEYS_FILE) if not cp.has_section('Main'): log.warning('Keys file corrupted, copying default') copy_default_keys_file() # parse file public_api = get_public_actions() actions = dict() for it, key_desc in cp.items('Main'): it = it.strip().lower() if it not in public_api: log.warning('KEYS: Action item invalid in section Main: {}'.format(it)) continue try: actions[it] = [key_str2bin(k) for k in key_desc.split()] except: log.warning('KEYS: Action "{}" contains invalid key definition in section Main: "{}"'.format(it, key_desc)) # check if missing items for it in set(public_api) - set(actions.keys()): log.warning('KEYS: Missing Action item {}'.format(it)) # transpose dict: {action: keys} => {keys: action} # and check if same key used for different actions # keys = dict([(k, a) for a, ks in actions.items() for k in ks]) keys = dict() for a, ks in actions.items(): for k in ks: if k in keys.keys(): log.warning('KEYS: Can\'t use key "{}" in action "{}", alredy used in "{}"'.format(key_bin2str(k), a, keys[k])) continue else: keys[k] = a return keys def copy_default_keys_file(): try: with open(KEYS_FILE, 'w') as f: default_keys = get_lfm_data_file_contents('lfm-default.keys') f.write(default_keys) except: log.error('Can\'t copy default Keys file: {}'.format(KEYS_FILE)) raise def dump_keys_to_file(keys): log.info('Dump keys into file') with open(DUMP_KEYS_FILE, 'w') as f: f.write('#'*30 + ' LFM dump keys ' + '#'*30 + '\n\n') f.write('#'*20 + ' Key -> Action ' + '#'*20 + '\n') lines = ['{:10} -> {}'.format(key_bin2str(k), a) for k, a in keys.items()] f.write('\n'.join(sorted(lines))) f.write('\n\n' + '#'*20 + ' Action -> Key ' + '#'*20 + '\n') actions = dict() for k, a in keys.items(): if a in actions: actions[a].append(key_bin2str(k)) else: actions[a] = [key_bin2str(k)] lines = ['{:24} -> {}'.format(a, ', '.join(sorted(ks))) for a, ks in actions.items()] f.write('\n'.join(sorted(lines))) missing_actions = set(get_public_actions()) - set(actions) if len(missing_actions) > 0: f.write('\n\nActions with no keys:\n') for a in sorted(missing_actions): f.write('. {}\n'.format(a)) else: f.write('\n\nAll actions have key binding\n') f.write('\n' + '#'*70) ######################################################################## ##### History class History(dict): def __init__(self): self._data = HISTORY_BLANK def __getitem__(self, key): return self._data[key] def __setitem__(self, key, val): self._data[key] = val def delete(self): log.info('Delete history') self._data = HISTORY_BLANK self.save() def load(self): log.info('Load history file from "{}"'.format(HISTORY_FILE)) with open(HISTORY_FILE, 'rb') as f: self._data = pickle.load(f) def save(self): log.info('Save history file to "{}"'.format(HISTORY_FILE)) with open(HISTORY_FILE, 'wb') as f: pickle.dump(self._data, f, -1) def append(self, section, new_entry): assert section in self._data.keys() if new_entry == '': return if new_entry in self._data[section]: self._data[section].remove(new_entry) self._data[section].append(new_entry) self._data[section] = self._data[section][-HISTORY_MAX:] ######################################################################## lfm-3.1/lfm/common.py0000644000175000001440000000703013123762515013604 0ustar inigousers# -*- coding: utf-8 -*- import logging import os.path from enum import Enum, IntEnum from string import digits, ascii_lowercase ######################################################################## ##### General AUTHOR = 'Iñigo Serna' VERSION = '3.0' DATE = '2001-17' LFM_NAME = 'lfm - Last File Manager' CONFIG_DIR = os.path.abspath(os.path.expanduser('~/.config/lfm')) CONFIG_FILE = os.path.join(CONFIG_DIR, 'lfm.ini') THEME_FILE = os.path.join(CONFIG_DIR, 'lfm.theme') KEYS_FILE = os.path.join(CONFIG_DIR, 'lfm.keys') HISTORY_FILE = os.path.join(CONFIG_DIR, 'lfm.history') DUMP_KEYS_FILE = os.path.join(CONFIG_DIR, 'keys.dump') DEBUG_LEVEL = logging.INFO # DEBUG, WARNING log = logging.getLogger('lfm') MAX_TABS = 4 VFS_STRING = '#vfs://' BOOKMARKS_KEYS = digits + ascii_lowercase MIN_COLUMNS = 66 HISTORY_MAX = 100 HISTORY_BLANK = {'file': [], 'path': [], 'glob': [], 'find': [], 'grep': [], 'exec': [], 'cli': []} SYSPROGS = {'tar': 'tar', 'bzip2': 'bzip2', 'gzip': 'gzip', 'zip': 'zip', 'unzip': 'unzip', 'rar': 'rar', '7z': '7z', 'xz': 'xz', 'lzip': 'lzip', 'lz4': 'lz4', 'grep': 'grep', 'find': 'find', 'which': 'which', 'xargs': 'xargs'} FileType = IntEnum('FileType', 'dir link2dir link nlink cdev bdev fifo socket exe reg unknown') FILETYPES = [('x', 'Placeholder'), ('/', 'Directory'), ('~', 'Link to Directory'), ('@', 'Link'), ('!', 'No Link'), ('-', 'Char Device'), ('+', 'Block Device'), ('|', 'Fifo'), ('#', 'Socket'), ('*', 'Executable'), (' ', 'File'), ('?', 'Unknown')] PaneMode = Enum('PaneMode', 'full half hidden info contents') SortType = Enum('SortType', 'none byName byExt byPath bySize byMTime') KeyModifier = Enum('KeyModifier', 'none control alt') RetCode = Enum('RetCode', 'nothing quit_chdir quit_nochdir fix_limits full_redisplay half_redisplay') ProcCode = Enum('ProcCode', 'end stopped error next confirm') ProcCodeConfirm = Enum('ProcCodeConfirm', 'stop ok skip') class LFMFileExistsError(Exception): pass class LFMFileSkipped(Exception): pass class LFMTerminalTooNarrow(Exception): pass ######################################################################## ##### Color Theme COLOR_ITEMS = [ # general interface 'header', 'tab_active', 'tab_inactive', 'pane_active', 'pane_inactive', 'pane_header_path', 'pane_header_titles', 'statusbar', 'powercli_prompt', 'powercli_text', 'selected_files', 'cursor', 'cursor_selected', # files. Must match the FILES_EXT from configuration! 'files_dir', 'files_exe', 'files_reg', 'files_archive', 'files_audio', 'files_data', 'files_devel', 'files_document', 'files_ebook', 'files_graphics', 'files_pdf', 'files_temp', 'files_video', 'files_web', # dialogs 'dialog', 'dialog_title', 'button_active', 'button_inactive', 'dialog_error', 'dialog_error_title', 'dialog_error_text', 'dialog_perms', 'selectitem', 'selectitem_title', 'selectitem_cursor', 'entryline', 'progressbar_fg', 'progressbar_bg', 'view_white_on_black', 'view_red_on_black', 'view_blue_on_black', 'view_green_on_black' ] VALID_COLORS = ['white', 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white*', 'black*', 'red*', 'green*', 'yellow*', 'blue*', 'magenta*', 'cyan*'] ######################################################################## lfm-3.1/lfm/key_defs.py0000644000175000001440000001077212733204621014107 0ustar inigousers# -*- coding: utf-8 -*- import curses from string import ascii_letters, digits, punctuation from common import * ######################################################################## VALID_KEYS_1 = ascii_letters + digits + punctuation VALID_KEYS_2 = "up down left right pageup pagedown home end del ins backspace enter spc esc tab btab".split() VALID_KEYS_3 = ['f%d' % i for i in range(1, 25)] tbl_kstr_kcode = [('up', curses.KEY_UP), ('down', curses.KEY_DOWN), ('left', curses.KEY_LEFT), ('right', curses.KEY_RIGHT), ('pageup', curses.KEY_PPAGE), ('pagedown', curses.KEY_NPAGE), ('home', curses.KEY_HOME), ('end', curses.KEY_END), ('del', curses.KEY_DC), ('ins', curses.KEY_IC), ('backspace', curses.KEY_BACKSPACE), ('enter', 10), ('spc', 32), ('esc', 27), ('tab', 9), ('backtab', curses.KEY_BTAB), ('S+up', curses.KEY_SR), ('S+down', curses.KEY_SF), ('S+left', curses.KEY_SLEFT), ('S+right', curses.KEY_SRIGHT), ('S+del', curses.KEY_SDC)] kmap_str2code = dict(tbl_kstr_kcode) kmap_code2str = dict([(it[1], it[0]) for it in tbl_kstr_kcode]) # A-up: kUP3, A-S-up: kUP4, C-up: kUP5, C-S-up: kUP6, C-A-up: kUP7 ... C-A-pagedown: kNXT7 # kUP3: 0x235, kUP4: 0x236 ... kDN3: 0x20c ... kLFT3: 0x220 ... kRIT3: 0x22c ... # kDC3: 0x206 ... kIC3: 0x21b ... kPRV: 0x22a ... kNXT: 0x22a tbl_mods = {'A-': 3, 'A-S-': 4, 'S-A-': 4, 'C-': 5, 'C-S-': 6, 'S-C-': 6, 'C-A-': 7, 'A-C-': 7} #tbl_keysspecial = {'up': ('kUP', 0x235), 'down': ('kDN', 0x20c), 'left': ('kLFT', 0x220), # 'right': ('kRIT', 0x22f), 'del': ('kDC', 0x206), 'ins': ('kIC', 0x21b), # 'pageup': ('kPRV', 0x22a), 'pagedown': ('kNXT', 0x225)} tbl_keysspecial = {'up': ('kUP', 0x236), 'down': ('kDN', 0x20d), 'left': ('kLFT', 0x221), 'right': ('kRIT', 0x230), 'del': ('kDC', 0x207), 'ins': ('kIC', 0x21c), 'pageup': ('kPRV', 0x22b), 'pagedown': ('kNXT', 0x226)} # tbl_keysspecial = {'up': ('kUP', 0x237), 'down': ('kDN', 0x20e), 'left': ('kLFT', 0x222), # 'right': ('kRIT', 0x231), 'del': ('kDC', 0x208), 'ins': ('kIC', 0x21d), # 'pageup': ('kPRV', 0x22c), 'pagedown': ('kNXT', 0x227)} tbl_aliases = [] for mod in tbl_mods: for k in tbl_keysspecial: pair = mod+k, tbl_keysspecial[k][1]+tbl_mods[mod]-3 tbl_aliases.append(pair) # S-F1: F13 ... S-F12: F24 for i in range(0, 11): pair = 'S-F{}'.format(i+1), curses.KEY_F1+12+i tbl_aliases.append(pair) kmap_aliases_str2code = dict(tbl_aliases) kmap_aliases_code2str = dict([(it[1], it[0]) for it in tbl_aliases]) ######################################################################## def get_keycode(kstr): if len(kstr) == 1: if kstr in VALID_KEYS_1: return ord(kstr) else: raise ValueError kstr = kstr.lower() if kstr in kmap_aliases_str2code: return kmap_aliases_str2code[kstr] if kstr in VALID_KEYS_2: return kmap_str2code[kstr] if kstr in VALID_KEYS_3: return curses.KEY_F1 + int(kstr[1:]) - 1 else: raise ValueError def key_str2bin(k): if k in kmap_aliases_str2code: return (KeyModifier.none, kmap_aliases_str2code[k]) if k.startswith('C-'): km = KeyModifier.control k = k[2:] if len(k) == 1: return (KeyModifier.none, ord(k.lower())-ord('a')+1) elif k.startswith('A-'): km = KeyModifier.alt k = k[2:] else: km = KeyModifier.none return (km, get_keycode(k)) def get_keystr(kcode): if kcode in kmap_code2str: return kmap_code2str[kcode] if kcode in kmap_aliases_code2str: return kmap_aliases_code2str[kcode] if kcode >= curses.KEY_F1 and kcode <= curses.KEY_F24: return 'f%d' % (kcode-curses.KEY_F1+1, ) if kcode>=32 and kcode<128: return chr(kcode) return str(kcode) def key_bin2str(k): km, key_code = k if key_code < 0x20: if key_code in kmap_code2str: return kmap_code2str[key_code] return 'C-' + chr(ord('a')-1+key_code) if km == KeyModifier.alt: km = 'A-' else: km = '' return km + get_keystr(key_code) ######################################################################## lfm-3.1/lfm/compress.py0000644000175000001440000002074413066244175014161 0ustar inigousers# -*- coding: utf-8 -*- from os.path import dirname, basename, join, isfile, isdir from utils import delete_bulk from common import * ###################################################################### def get_compressed_file_engine(filename): for p in packagers: # Note: tbz2 must be before bz2 for e in p.exts: if filename.endswith(e): return p(filename) else: return None def get_compressed_file_type(filename): c = get_compressed_file_engine(filename) return c.type if c else None def check_compressed_vfs(filename): c = get_compressed_file_engine(filename) return c.can_vfs if c else False class PackagerBase: def __init__(self, filename): self.path = filename self.dirname = dirname(self.path) self.filename = basename(self.path) @property def cmd_uncompress(self): return self.uncompress_cmd % self.path @property def cmd_compress(self): newfile = self.filename + self.exts[0] if isfile(self.path): if self.type in ('bz2', 'gz', 'xz', 'lz', 'lz4'): return self.compress_cmd % self.filename elif self.type in ('tbz2', 'tgz', 'txz', 'tlz', 'tlz4, ''tar'): return # Don't use tar, it's a file else: return self.compress_cmd % (self.filename, newfile) elif isdir(self.path): if self.type in ('bz2', 'gz', 'xz', 'lz', 'lz4'): return # Don't compress without tar, it's a dir if self.need_tar: return self.compress_cmd % (self.filename, newfile) else: return self.compress_cmd % (newfile, self.filename) def cmd_compress2(self, src, dest): if not dest.endswith(self.exts[0]): dest += self.exts[0] if self.need_tar: return self.compress2_cmd % (src, dest) else: return self.compress2_cmd % (dest, src) def delete_temp(self, path, from_compress, is_tmp=False): if is_tmp: tmpfile = path else: if from_compress: for e in self.exts: if self.filename.endswith(e): dirname = self.filename[:-len(e)] break else: return tmpfile = join(path, dirname) else: # from uncompress tmpfile = join(path, self.filename+self.exts[0]) delete_bulk(str(tmpfile), ignore_errors=True) class PackagerTBZ2(PackagerBase): type = 'tbz2' exts = ('.tar.bz2', '.tbz2') need_tar = True can_vfs = True uncompress_prog = compress_prog = SYSPROGS['bzip2'] uncompress_cmd = uncompress_prog + ' -d \"%s\" -c | ' + SYSPROGS['tar'] + ' xfi -' compress_cmd = SYSPROGS['tar'] + ' cf - \"%s\" | ' + compress_prog + ' > \"%s\"' compress2_cmd = SYSPROGS['tar'] + ' cf - %s | ' + compress_prog + ' > \"%s\"' class PackagerBZ2(PackagerBase): type = 'bz2' exts = ('.bz2', ) need_tar = False can_vfs = False uncompress_prog = compress_prog = SYSPROGS['bzip2'] uncompress_cmd = uncompress_prog + ' -d \"%s\"' compress_cmd = compress_prog + ' \"%s\"' compress2_cmd = compress_prog + ' %s' class PackagerTGZ(PackagerBase): type = 'tgz' exts = ('.tar.gz', '.tgz', '.tar.Z') need_tar = True can_vfs = True uncompress_prog = compress_prog = SYSPROGS['gzip'] uncompress_cmd = uncompress_prog + ' -d \"%s\" -c | ' + SYSPROGS['tar'] + ' xfi -' compress_cmd = SYSPROGS['tar'] + ' cf - \"%s\" | ' + compress_prog + ' > \"%s\"' compress2_cmd = SYSPROGS['tar'] + ' cf - %s | ' + compress_prog + ' > \"%s\"' class PackagerGZ(PackagerBase): type = 'gz' exts = ('.gz', ) need_tar = False can_vfs = False uncompress_prog = compress_prog = SYSPROGS['gzip'] uncompress_cmd = uncompress_prog + ' -d \"%s\"' compress_cmd = compress_prog + ' \"%s\"' compress2_cmd = compress_prog + ' %s' class PackagerTXZ(PackagerBase): type = 'txz' exts = ('.tar.xz', '.txz') need_tar = True can_vfs = True uncompress_prog = compress_prog = SYSPROGS['xz'] uncompress_cmd = uncompress_prog + ' -d \"%s\" -c | ' + SYSPROGS['tar'] + ' xfi -' compress_cmd = SYSPROGS['tar'] + ' cf - \"%s\" | ' + compress_prog + ' > \"%s\"' compress2_cmd = SYSPROGS['tar'] + ' cf - %s | ' + compress_prog + ' > \"%s\"' class PackagerXZ(PackagerBase): type = 'xz' exts = ('.xz', ) need_tar = False can_vfs = False uncompress_prog = compress_prog = SYSPROGS['xz'] uncompress_cmd = uncompress_prog + ' -d \"%s\"' compress_cmd = compress_prog + ' \"%s\"' compress2_cmd = compress_prog + ' %s' class PackagerTLZ(PackagerBase): type = 'tlz' exts = ('.tar.lz', '.tlz') need_tar = True can_vfs = True uncompress_prog = compress_prog = SYSPROGS['lzip'] uncompress_cmd = uncompress_prog + ' -d \"%s\" -c | ' + SYSPROGS['tar'] + ' xfi -' compress_cmd = SYSPROGS['tar'] + ' cf - \"%s\" | ' + compress_prog + ' > \"%s\"' compress2_cmd = SYSPROGS['tar'] + ' cf - %s | ' + compress_prog + ' > \"%s\"' class PackagerLZ(PackagerBase): type = 'lz' exts = ('.lz', ) need_tar = False can_vfs = False uncompress_prog = compress_prog = SYSPROGS['lzip'] uncompress_cmd = uncompress_prog + ' -d \"%s\"' compress_cmd = compress_prog + ' \"%s\"' compress2_cmd = compress_prog + ' %s' class PackagerTLZ4(PackagerBase): type = 'tlz4' exts = ('.tar.lz4', '.tlz4') need_tar = True can_vfs = True uncompress_prog = compress_prog = SYSPROGS['lz4'] uncompress_cmd = uncompress_prog + ' -q -d \"%s\" -c | ' + SYSPROGS['tar'] + ' xfi -' compress_cmd = SYSPROGS['tar'] + ' cf - \"%s\" | ' + compress_prog + ' -9 -q > \"%s\"' compress2_cmd = SYSPROGS['tar'] + ' cf - %s | ' + compress_prog + ' -9 -q > \"%s\"' class PackagerLZ4(PackagerBase): type = 'lz4' exts = ('.lz4', ) need_tar = False can_vfs = False uncompress_prog = compress_prog = SYSPROGS['lz4'] uncompress_cmd = uncompress_prog + ' --rm -m -q -d \"%s\"' compress_cmd = compress_prog + ' --rm -m -q \"%s\"' compress2_cmd = compress_prog + ' --rm -m -q %s' class PackagerTAR(PackagerBase): type = 'tar' exts = ('.tar', ) need_tar = False can_vfs = True uncompress_prog = compress_prog = SYSPROGS['tar'] uncompress_cmd = uncompress_prog + ' xf \"%s\"' compress_cmd = compress_prog + ' cf \"%s\" \"%s\"' compress2_cmd = compress_prog + ' cf \"%s\" %s' class PackagerZIP(PackagerBase): type = 'zip' exts = ('.zip', '.jar', '.apk') need_tar = False can_vfs = True uncompress_prog = SYSPROGS['unzip'] uncompress_cmd = uncompress_prog + ' -o -q \"%s\"' compress_prog = SYSPROGS['zip'] compress_cmd = compress_prog + ' -qr \"%s\" \"%s\"' compress2_cmd = compress_prog + ' -qr \"%s\" %s' class PackagerRAR(PackagerBase): type = 'rar' exts = ('.rar', ) need_tar = False can_vfs = True uncompress_prog = compress_prog = SYSPROGS['rar'] uncompress_cmd = uncompress_prog + ' x \"%s\"' compress_cmd = compress_prog + ' a \"%s\" \"%s\"' compress2_cmd = compress_prog + ' a \"%s\" %s' class Packager7Z(PackagerBase): type = '7z' exts = ('.7z', ) need_tar = False can_vfs = True uncompress_prog = SYSPROGS['7z'] uncompress_cmd = uncompress_prog + ' x \"%s\"' compress_prog = SYSPROGS['7z'] compress_cmd = compress_prog + ' a \"%s\" \"%s\"' compress2_cmd = compress_prog + ' a \"%s\" %s' ###################################################################### packagers = (PackagerTBZ2, PackagerBZ2, PackagerTGZ, PackagerGZ, PackagerTXZ, PackagerXZ, PackagerTLZ, PackagerLZ, PackagerTLZ4, PackagerLZ4, PackagerTAR, PackagerZIP, PackagerRAR, Packager7Z) packagers_by_type = {'tbz2': PackagerTBZ2, 'bz2': PackagerBZ2, 'tgz': PackagerTGZ, 'gz': PackagerGZ, 'txz': PackagerTXZ, 'xz': PackagerXZ, 'tlz': PackagerTLZ, 'lz': PackagerLZ, 'tlz4': PackagerTLZ4, 'lz4': PackagerLZ4, 'tar': PackagerTAR, 'zip': PackagerZIP, 'rar': PackagerRAR, '7z': Packager7Z} ###################################################################### lfm-3.1/lfm/__init__.py0000644000175000001440000000010612441301057014040 0ustar inigousersimport sys import os.path sys.path.append(os.path.dirname(__file__)) lfm-3.1/lfm/ui_widgets.py0000644000175000001440000017604312610763672014477 0ustar inigousers# -*- coding: utf-8 -*- import curses import curses.panel from os import sep, listdir from os.path import basename, dirname, exists, expanduser, isdir, isabs, join from utils import length, max_length, text2wrap, prev_step, next_step, perms2str, get_binary_programs, DirsTree from common import * ######################################################################## ##### Module variables app = None history = {} ######################################################################## ##### Scrollbar def display_scrollbar(win, y0, x0, h, n, i, a): """Display a scrollbar""" if n <= h: return win.vline(y0, x0, curses.ACS_VLINE, h) ss = max(h*h//n, 1) y = min(max((i//h)*h*h//n, 0), h-ss) win.vline(y0+y, x0, curses.ACS_CKBOARD, ss) if a != 0: win.vline(y0, x0, '^', 1) if (ss == 1) and (y == 0): win.vline(y0+1, x0, curses.ACS_CKBOARD, 1) if n > a + h: win.vline(y0+h-1, x0, 'v', 1) if (ss == 1) and (y == h-1): win.vline(y0+h-2, x0, curses.ACS_CKBOARD, 1) ###################################################################### ##### Dialogs def DialogMessage(title, subtitle): """Show a message. No wait, no keys""" title, subtitle = title[:app.w-14], subtitle[:app.w-14] h, w = 5, min(max(len(title), len(subtitle), 22)+6, app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) win.addstr(2, 2, subtitle) win.addstr(h-1, (w-22)//2, ' Press Ctrl-C to stop ') win.refresh() def DialogError(text): """Show an error message and waits for a key""" lines = text.split('\n') lth = max(max_length(lines), 31) h, w = min(len(lines)+4, app.h-2), min(lth+4, app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog_error']) win.erase() win.box() win.addstr(0, (w-7)//2, ' Error ', app.CLR['dialog_error_title']) win.addstr(h-1, (w-27)//2, ' Press any key to continue ') for i, l in enumerate(lines): win.addstr(i+2, 2, l, app.CLR['dialog_error_text']) win.refresh() while not win.getch(): pass pwin.hide() curses.panel.update_panels() def DialogGetKey(title, question): """Show a message and return key pressed""" question = question.replace('\t', ' ' * 4) lines = question.split('\n') h = min(len(lines)+4, app.h-2) w = min(max_length(lines)+4, app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) for i, l in enumerate(lines): win.addstr(i+2, 2, l) win.refresh() win.keypad(1) while True: ch = win.getch() if ch in (0x03, 0x1B): # Ctrl-C, ESC ch = -1 break elif 0x01 <= ch <= 0xFF: break else: curses.beep() pwin.hide() curses.panel.update_panels() return ch def DialogConfirm(title, question, default=0): """Show a yes/no question, returning 1/0""" BTN_SELECTED, BTN_NO_SELECTED = app.CLR['button_active'], app.CLR['button_inactive'] h, w = 5, min(max(34, len(question)+5), app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) win.addstr(1, 2, question) win.refresh() row, col = (app.h-h)//2 + 3, (app.w-w)//2 col1, col2 = col + w//5 + 1, col + w*4//5 - 6 win.keypad(1) answer = default while True: if answer == 1: attr_yes, attr_no = BTN_SELECTED, BTN_NO_SELECTED else: attr_yes, attr_no = BTN_NO_SELECTED, BTN_SELECTED btn = curses.newpad(1, 8) btn.addstr(0, 0, '[ Yes ]', attr_yes) btn.refresh(0, 0, row, col1, row+1, col1+6) btn = curses.newpad(1, 7) btn.addstr(0, 0, '[ No ]', attr_no) btn.refresh(0, 0, row, col2, row+1, col2+5) ch = win.getch() if ch in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_LEFT, curses.KEY_RIGHT, 9, curses.KEY_BTAB): answer = not answer elif ch in (ord('Y'), ord('y')): answer = 1 break elif ch in (ord('N'), ord('n')): answer = 0 break elif ch in (0x03, 0x1B): # Ctrl-C, ESC answer = 0 break elif ch in (10, 13): # enter break else: curses.beep() pwin.hide() curses.panel.update_panels() return answer def DialogConfirmAll(title, question, default=0): """Show a yes/all/no/stop question, returning 1/2/0/-1""" BTN_SELECTED, BTN_NO_SELECTED = app.CLR['button_active'], app.CLR['button_inactive'] h, w = 5, min(max(48, len(question)+5), app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) win.addstr(1, 2, question) win.refresh() row, col = (app.h-h)//2 + 3, (app.w-w)//2 x = (w-28) // 5 col1 = col + x + 1 col2 = col1 + 7 + x col3 = col2 + 7 + x col4 = col3 + 6 + x win.keypad(1) answer = default order = [1, 2, 0, -1] while True: attr_yes = attr_all = attr_no = attr_skipall = BTN_NO_SELECTED if answer == 1: attr_yes = BTN_SELECTED elif answer == 2: attr_all = BTN_SELECTED elif answer == 0: attr_no = BTN_SELECTED else: # answer == -1: attr_skipall = BTN_SELECTED btn = curses.newpad(1, 8) btn.addstr(0, 0, '[ Yes ]', attr_yes) btn.refresh(0, 0, row, col1, row+1, col1+6) btn = curses.newpad(1, 8) btn.addstr(0, 0, '[ All ]', attr_all) btn.refresh(0, 0, row, col2, row+1, col2+6) btn = curses.newpad(1, 7) btn.addstr(0, 0, '[ No ]', attr_no) btn.refresh(0, 0, row, col3, row+1, col3+5) btn = curses.newpad(1, 15) btn.addstr(0, 0, '[ Stop ]', attr_skipall) btn.refresh(0, 0, row, col4, row+1, col4+7) ch = win.getch() if ch in (curses.KEY_UP, curses.KEY_LEFT, curses.KEY_BTAB): try: answer = order[order.index(answer) - 1] except IndexError: answer = order[len(order)] elif ch in (curses.KEY_DOWN, curses.KEY_RIGHT, 9): try: answer = order[order.index(answer) + 1] except IndexError: answer = order[0] elif ch in (ord('Y'), ord('y')): answer = 1 break elif ch in (ord('A'), ord('a')): answer = 2 break elif ch in (ord('N'), ord('n')): answer = 0 break elif ch in (ord('S'), ord('s'), 0x03, 0x1B): # Ctrl-C, ESC answer = -1 break elif ch in (10, 13): # enter break else: curses.beep() pwin.hide() curses.panel.update_panels() return answer def DialogConfirmAllNone(title, question, default=0): """Show a yes/all/no/none/stop question, returning 1/2/0/-2/-1""" BTN_SELECTED, BTN_NO_SELECTED = app.CLR['button_active'], app.CLR['button_inactive'] h, w = 5, min(max(50, len(question)+5), app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) pwin = curses.panel.new_panel(win) pwin.top() except curses.error: raise win.keypad(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) win.addstr(1, 2, question) win.refresh() row, col = (app.h-h)//2 + 3, (app.w-w)//2 x = (w-36) // 6 col1 = col + x + 1 col2 = col1 + 7 + x col3 = col2 + 7 + x col4 = col3 + 6 + x col5 = col4 + 8 + x win.keypad(1) answer = default order = [1, 2, 0, -2, -1] while True: attr_yes = attr_all = attr_no = attr_none = attr_skipall = BTN_NO_SELECTED if answer == 1: attr_yes = BTN_SELECTED elif answer == 2: attr_all = BTN_SELECTED elif answer == 0: attr_no = BTN_SELECTED elif answer == -2: attr_none = BTN_SELECTED else: # answer == -1: attr_skipall = BTN_SELECTED btn = curses.newpad(1, 8) btn.addstr(0, 0, '[ Yes ]', attr_yes) btn.refresh(0, 0, row, col1, row+1, col1+6) btn = curses.newpad(1, 8) btn.addstr(0, 0, '[ All ]', attr_all) btn.refresh(0, 0, row, col2, row+1, col2+6) btn = curses.newpad(1, 7) btn.addstr(0, 0, '[ No ]', attr_no) btn.refresh(0, 0, row, col3, row+1, col3+5) btn = curses.newpad(1, 9) btn.addstr(0, 0, '[ NOne ]', attr_none) btn.refresh(0, 0, row, col4, row+1, col4+7) btn = curses.newpad(1, 9) btn.addstr(0, 0, '[ Stop ]', attr_skipall) btn.refresh(0, 0, row, col5, row+1, col5+7) ch = win.getch() if ch in (curses.KEY_UP, curses.KEY_LEFT, curses.KEY_BTAB): try: answer = order[order.index(answer) - 1] except IndexError: answer = order[len(order)] elif ch in (curses.KEY_DOWN, curses.KEY_RIGHT, 9): try: answer = order[order.index(answer) + 1] except IndexError: answer = order[0] elif ch in (ord('Y'), ord('y')): answer = 1 break elif ch in (ord('A'), ord('a')): answer = 2 break elif ch in (ord('N'), ord('n')): answer = 0 break elif ch in (ord('O'), ord('o')): answer = -2 break elif ch in (ord('S'), ord('s'), 0x03, 0x1B): # Ctrl-C, ESC answer = -1 break elif ch in (10, 13): # enter break else: curses.beep() pwin.hide() curses.panel.update_panels() return answer ######################################################################## ##### CursorAnimation class CursorAnimation: """A small progress animation show on top-right corner of screen""" anim_chars = ('|', '/', '-', '\\') def __init__(self): self.step = 0 self.win = curses.newpad(1, 2) self.win.bkgd(app.CLR['dialog_title']) def next(self): self.win.erase() self.win.addch(CursorAnimation.anim_chars[self.step%4]) self.win.refresh(0, 0, 0, app.w-3, 1, app.w-2) self.step = 0 if self.step>3 else self.step+1 ######################################################################## ##### DialogMessagePane class DialogMessagePanel: """A dialog to show a message. No wait""" def __init__(self, title, subtitle): title, subtitle = title[:app.w-14], subtitle[:app.w-14] h, w = 5, min(max(len(title), len(subtitle), 22)+6, app.w-2) try: win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) self.pwin = curses.panel.new_panel(win) self.pwin.top() except curses.error: raise win.keypad(1) win.nodelay(1) win.bkgd(app.CLR['dialog']) win.erase() win.box() win.addstr(0, (w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) win.addstr(2, 2, subtitle) win.addstr(h-1, (w-22)//2, ' Press Ctrl-C to stop ') win.refresh() def show(self): self.pwin.show() curses.panel.update_panels() def hide(self): self.pwin.hide() curses.panel.update_panels() def check_key(self): return self.pwin.window().getch() ######################################################################## ##### DialogProgressXPanel class DialogProgress1Panel: """A dialog with 1 progress bar""" def __init__(self, title): title = title[:app.w-14] h, self.w = 7, min(max(len(title), 60)+6, app.w-2) try: self.win = curses.newwin(h, self.w, (app.h-h)//2, (app.w-self.w)//2) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.nodelay(1) self.win.bkgd(app.CLR['dialog']) self.win.erase() self.win.box() self.win.addstr(0, (self.w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) self.win.addstr(2, 2, 'File:') self.win.addstr(4, 2, ' '*(self.w-4-7), app.CLR['progressbar_bg']) self.win.addstr(4, self.w-8, '[ 0%]') self.win.addstr(h-1, (self.w-22)//2, ' Press Ctrl-C to stop ') self.win.refresh() def show(self): self.pwin.show() curses.panel.update_panels() def hide(self): self.pwin.hide() curses.panel.update_panels() app.display() def check_key(self): return self.pwin.window().getch() def update(self, text, i, n): buf = '{}/{}'.format(i, n) self.win.addstr(2, 9, text2wrap(text, self.w-11-11, fill=True), app.CLR['dialog_title']) self.win.addstr(2, self.w-2-len(buf), buf) self.win.addstr(4, 2, ' '*int((self.w-4-7)*i/n), app.CLR['progressbar_fg']) self.win.addstr(4, self.w-8, '[{:3}%]'.format(100*i//n)) class DialogProgress2Panel(DialogProgress1Panel): """A dialog with 2 progress bar""" def __init__(self, title): title = title[:app.w-14] h, self.w = 8, min(max(len(title), 60)+6, app.w-2) try: self.win = curses.newwin(h, self.w, (app.h-h)//2, (app.w-self.w)//2) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.nodelay(1) self.win.bkgd(app.CLR['dialog']) self.win.erase() self.win.box() self.win.addstr(0, (self.w-len(title)-2)//2, ' %s ' % title, app.CLR['dialog_title']) self.win.addstr(2, 2, 'File:') self.win.addstr(4, 2, 'Bytes') self.win.addstr(4, 9, ' '*(self.w-4-14), app.CLR['progressbar_bg']) self.win.addstr(4, self.w-8, '[ 0%]') self.win.addstr(5, 2, 'Count') self.win.addstr(5, 9, ' '*(self.w-4-14), app.CLR['progressbar_bg']) self.win.addstr(5, self.w-8, '[ 0%]') self.win.addstr(h-1, (self.w-22)//2, ' Press Ctrl-C to stop ') self.win.refresh() def update(self, text, i, n, sp, st): buf = '{}/{}'.format(i, n) self.win.addstr(2, 9, text2wrap(text, self.w-11-11, fill=True), app.CLR['dialog_title']) self.win.addstr(2, self.w-2-len(buf), buf) self.win.addstr(4, 9, ' '*int((self.w-4-14)*sp/st), app.CLR['progressbar_fg']) self.win.addstr(4, self.w-8, '[{:3}%]'.format(100*sp//st)) self.win.addstr(5, 9, ' '*int((self.w-4-14)*i/n), app.CLR['progressbar_fg']) self.win.addstr(5, self.w-8, '[{:3}%]'.format(100*i//n)) ######################################################################## ##### SelectItem class SelectItem: """A dialog to select an item in a list""" def __init__(self, title, entries, y0=-1, x0=-1, entry_i='', quick_key=True, min_height=False): self.quick_key = quick_key if y0==-1 and x0==-1: # (y0,x0) is the position, if == -1 => center self.h = min(app.h-4, len(entries) if min_height else 10) + 2 self.w = min(app.w-12, max(max_length(entries), len(title), 32)) + 8 y0, x0 = (app.h-self.h) // 2, (app.w-self.w) // 2 else: self.h = min(app.h-y0-2, 10) + 2 # len(entries) => no, fixed height self.w = max(min(app.w-2*x0-4-10, max_length(entries)), len(title), 20) + 8 try: self.win = curses.newwin(self.h, self.w, y0, x0) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.bkgd(app.CLR['selectitem']) self.entries = entries self.nels = len(entries) try: self.entry_i = self.entries.index(entry_i) except: self.entry_i = 0 self.title = title def show(self): self.win.erase() self.win.box() h0, w0 = self.h-2, self.w-4 if self.title != '': self.win.addstr(0, (self.w-len(self.title)-2)//2, ' %s ' % self.title, app.CLR['selectitem_title']) entry_a = self.entry_i//h0 * h0 for i in range(h0): if entry_a+i >= self.nels: break line = text2wrap(self.entries[entry_a+i], w0) if entry_a+i == self.entry_i: self.win.addstr(i+1, 2, line, app.CLR['selectitem_cursor']) else: self.win.addstr(i+1, 2, line) self.win.refresh() display_scrollbar(self.win, 1, self.w-1, h0, self.nels, self.entry_i, entry_a) def manage_keys(self): initials = {ord(e[0]) for e in self.entries} while True: self.show() ch = self.win.getch() if self.quick_key: if ch in initials: for e in self.entries: if ch == ord(e[0]): return self.entries[self.entries.index(e)] if ch in (0x03, 0x1B, ord('q'), ord('Q')): # Ctrl-C, ESC return -1 elif ch in (curses.KEY_UP, ord('k'), ord('K')): self.entry_i = max(0, self.entry_i-1) elif ch in (curses.KEY_DOWN, ord('j'), ord('J')): self.entry_i = min(self.entry_i+1, self.nels-1) elif ch in (curses.KEY_PPAGE, curses.KEY_BACKSPACE, 0x08, 0x02): self.entry_i = max(0, self.entry_i-(self.h-2)) elif ch in (curses.KEY_NPAGE, ord(' '), 0x06): self.entry_i = min(self.entry_i+(self.h-2), self.nels-1) elif ch in (curses.KEY_HOME, 0x01): self.entry_i = 0 elif ch in (curses.KEY_END, 0x05): self.entry_i = self.nels - 1 elif ch == 0x0C: # Ctrl-L entry_a = int(self.entry_i//(self.h-2)) * (self.h-2) self.entry_i = entry_a + (self.h-2)//2 elif ch == 0x13: # Ctrl-S ch2 = self.win.getkey() for e in self.entries[self.entry_i:]: if e.find(ch2) == 0: self.entry_i = self.entries.index(e) break elif ch in (0x0A, 0x0D): # enter return self.entries[self.entry_i] else: curses.beep() def run(self): selected = self.manage_keys() self.pwin.hide() curses.panel.update_panels() app.display() return selected ###################################################################### ##### DialogFindGrep class DialogFindGrep: """A dialog similar to SelectItem""" def __init__(self, title, entries, entry_i=''): self.h = min(app.h-8, len(entries), 12) + 5 self.w = min(app.w-6, 100, max(max_length(entries), 60, len(title))) + 4 y0, x0 = (app.h-self.h) // 2, (app.w-self.w) // 2 try: self.win = curses.newwin(self.h, self.w, y0, x0) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.bkgd(app.CLR['selectitem']) self.entries = entries self.nels = len(entries) try: self.entry_i = self.entries.index(entry_i) except: self.entry_i = 0 self.title = title self.btn_active = 0 def show(self): BTN_SELECTED, BTN_NO_SELECTED = app.CLR['selectitem_cursor'], app.CLR['selectitem'] self.win.erase() self.win.box() h0, w0 = self.h-4, self.w-4 if self.title != '': self.win.addstr(0, (self.w-len(self.title)-2)//2, ' %s ' % self.title, app.CLR['selectitem_title']) entry_a = self.entry_i//h0 * h0 for i in range(h0): if entry_a+i >= self.nels: break line = text2wrap(self.entries[entry_a+i], w0, start_pct=0.9) if entry_a+i == self.entry_i: self.win.addstr(i+1, 2, line, app.CLR['selectitem_cursor']) else: self.win.addstr(i+1, 2, line) display_scrollbar(self.win, 1, self.w-1, h0, self.nels, self.entry_i, entry_a) self.win.hline(self.h-3, 1, curses.ACS_HLINE, self.w-2) self.win.hline(self.h-3, 0, curses.ACS_LTEE, 1) self.win.hline(self.h-3, self.w-1, curses.ACS_RTEE, 1) self.win.addstr(self.h-2, 3, '[ Go ] [ Panelize ] [ View ] [ Edit ] [ Do ] [ Quit ]', BTN_NO_SELECTED) attrs = [BTN_NO_SELECTED] * 6 attrs[self.btn_active] = BTN_SELECTED self.win.addstr(self.h-2, 3, '[ Go ]', attrs[0]) self.win.addstr(self.h-2, 11, '[ PAnelize ]', attrs[1]) self.win.addstr(self.h-2, 25, '[ View ]', attrs[2]) self.win.addstr(self.h-2, 35, '[ Edit ]', attrs[3]) self.win.addstr(self.h-2, 45, '[ Do ]', attrs[4]) self.win.addstr(self.h-2, 53, '[ Quit ]', attrs[5]) self.win.refresh() def manage_keys(self): while True: self.show() ch = self.win.getch() if ch in (0x03, 0x1B, ord('q'), ord('Q')): # Ctrl-C, ESC return -1, self.entries[self.entry_i] elif ch in (curses.KEY_UP, ord('k'), ord('K')): self.entry_i = max(0, self.entry_i-1) elif ch in (curses.KEY_DOWN, ord('j'), ord('J')): self.entry_i = min(self.entry_i+1, self.nels-1) elif ch in (curses.KEY_PPAGE, curses.KEY_BACKSPACE, 0x08, 0x02): self.entry_i = max(0, self.entry_i-(self.h-2)) elif ch in (curses.KEY_NPAGE, ord(' '), 0x06): self.entry_i = min(self.entry_i+(self.h-2), self.nels-1) elif ch in (curses.KEY_HOME, 0x01): self.entry_i = 0 elif ch in (curses.KEY_END, 0x05): self.entry_i = self.nels - 1 elif ch == 0x0C: # Ctrl-L entry_a = int(self.entry_i//(self.h-2)) * (self.h-2) self.entry_i = entry_a + (self.h-2)//2 elif ch == 0x13: # Ctrl-S ch2 = self.win.getkey() for e in self.entries[self.entry_i:]: if e.find(ch2) == 0: self.entry_i = self.entries.index(e) break elif ch in (curses.KEY_LEFT, curses.KEY_BTAB): self.btn_active = 5 if self.btn_active==0 else self.btn_active-1 elif ch in (curses.KEY_RIGHT, 0x09): # tab self.btn_active = 0 if self.btn_active==5 else self.btn_active+1 elif ch in (0x0A, 0x0D): # enter return -1 if self.btn_active==5 else self.btn_active, self.entries[self.entry_i] elif ch in (ord('a'), ord('A')): return 1, self.entries[self.entry_i] elif ch in (curses.KEY_F3, ord('v'), ord('V')): return 2, self.entries[self.entry_i] elif ch in (curses.KEY_F4, ord('e'), ord('E')): return 3, self.entries[self.entry_i] elif ch in (ord('@'), ord('d'), ord('D')): return 4, self.entries[self.entry_i] else: curses.beep() def run(self): selected = self.manage_keys() self.pwin.hide() curses.panel.update_panels() app.display() return selected ###################################################################### ##### Helper widgets class Yes_No_Buttons: """Yes/No buttons""" def __init__(self, w, h, d): self.row = (app.h-h)//2 + 4 + d col = (app.w-w)//2 self.col1, self.col2 = col + w//5 + 1, col + w*4//5 - 6 self.active = 0 def show(self): BTN_SELECTED = app.CLR['button_active'] BTN_NO_SELECTED = app.CLR['button_inactive'] if self.active == 0: attr1, attr2 = BTN_NO_SELECTED, BTN_NO_SELECTED elif self.active == 1: attr1, attr2 = BTN_SELECTED, BTN_NO_SELECTED else: attr1, attr2 = BTN_NO_SELECTED, BTN_SELECTED btn = curses.newpad(1, 8) btn.addstr(0, 0, '[]', attr1) btn.refresh(0, 0, self.row, self.col1, self.row + 1, self.col1 + 6) btn = curses.newpad(1, 7) btn.addstr(0, 0, '[ No ]', attr2) btn.refresh(0, 0, self.row, self.col2, self.row + 1, self.col2 + 5) def manage_keys(self): tmp = curses.newpad(1, 1) tmp.keypad(1) while True: ch = tmp.getch() if ch in (0x03, 0x1B): # Ctrl-C, ESC return -1 elif ch in (ord('\t'), curses.KEY_BTAB): return ch elif ch in (10, 13): # enter if self.active == 1: return 10 else: return -1 else: curses.beep() ###################################################################### ##### EntryLine class EntryLine: """An entry line to enter a dir, a file, a pattern, etc""" def __init__(self, par_widget, w, y0, x0, text='', history=None, is_files=True, cli=False): try: self.win = curses.newwin(1, w+1, y0, x0) except curses.error: raise self.par_widget = par_widget self.color = app.CLR['powercli_text'] if cli else app.CLR['entryline'] self.win.attrset(self.color) self.win.keypad(1) self.win.nodelay(0) self.x0, self.y0, self.w = x0, y0, w self.text, self.origtext, self.pos, self.ins = text, text, len(text), True self.history, self.history_i = history, -1 if history is None else len(history) self.cli = cli self.is_files = True if self.cli else is_files def show(self): text, pos, w, ltext = self.text, self.pos, self.w, len(self.text) if pos < w: relpos = pos textstr = text[:w] if ltext>w else text.ljust(w) else: if pos > ltext - (w-1): relpos = w-1 - (ltext-pos) textstr = text[ltext-w+1:] + ' ' else: relpos = pos - pos//w*w textstr = text[pos//w*w:pos//w*w+w] # self.win.refresh() # needed to avoid a problem with blank paths self.win.bkgd(app.CLR['dialog']) self.win.erase() self.win.addstr(textstr[:w], self.color) self.win.move(0, relpos) self.win.refresh() def __select_item(self, entries, pos0, title=''): if not entries: curses.beep() return elif len(entries) == 1: return entries.pop() else: x = self.x0+pos0 if self.x0+pos0<3*app.w//4-4 else self.x0+2 if self.cli: y = app.h-14 # SelectItem has min 12 height, else: app.h//2-2 else: y = self.y0 curses.curs_set(0) selected = SelectItem(title, entries, y+1, x-2, quick_key=False).run() app.display() if not self.cli: self.par_widget.show() curses.curs_set(1) return selected def __get_list_completion(self, text): tab_path = app.pane_active.tab_active.fs.pdir path = expanduser(text) path = path if isabs(path) else join(tab_path, path) try: if text.endswith(os.sep) and isdir(path): basedir, fs = path, listdir(path) else: basedir, start = dirname(path), basename(path) fs = [f for f in listdir(basedir) if f.startswith(start)] except OSError: return basedir, list() # sort files with dirs first d1, d2 = list(), list() for f in fs: if isdir(join(basedir, f)): d1.append(f+sep) else: d2.append(f) d1.sort() d1.extend(sorted(d2)) return basedir, d1 def manage_keys(self): while True: self.show() wch = self.win.get_wch() if isinstance(wch, str) and ord(wch)>=32 and ord(wch)!=127: if self.ins: self.text = self.text[:self.pos] + wch + self.text[self.pos:] else: self.text = self.text[:self.pos] + wch + self.text[self.pos+1:] self.pos += 1 else: # int! wch = ord(wch) if isinstance(wch, str) else wch if wch in (3, 21): # C-c, ESC return -1 if wch == 24 and self.cli: # C-x return -2 elif wch in (9, curses.KEY_BTAB) and not self.cli: # tab, S-tab return wch elif wch in (10, 13): # enter return 10 # movement elif wch in (curses.KEY_HOME, 1): # home, C-a self.pos = 0 elif wch in (curses.KEY_END, 5): # end, C-e self.pos = len(self.text) elif wch in (curses.KEY_LEFT, 2): # left, C-b if self.pos > 0: self.pos -= 1 elif wch in (curses.KEY_RIGHT, 6): # right, C-f if self.pos < len(self.text): self.pos += 1 elif wch in (16, 0x222, 0x223, 0x224): # C-p, C-left self.pos = prev_step(self.text, self.pos) elif wch in (14, 0x231, 0x232, 0x233): # C-n, C-right self.pos = next_step(self.text, self.pos) # deletion elif wch in (127, curses.KEY_BACKSPACE): # Backspace if len(self.text) > 0 and self.pos > 0: self.text = self.text[:self.pos-1] + self.text[self.pos:] self.pos -= 1 elif wch in (17, ): # C-q, C-Backspace pos = prev_step(self.text, self.pos) self.text = self.text[:pos] + self.text[self.pos:] self.pos = pos elif wch == curses.KEY_DC: # Del if self.pos < len(self.text): self.text = self.text[:self.pos] + self.text[self.pos+1:] elif wch in (18, 0x208, 0x209): # C-r, C-Del pos = next_step(self.text, self.pos) if pos>0 and self.text[pos-1] in '.([{<"\'': pos = max(0, pos-1) if pos+1 0: if self.history_i == len(self.history): self.history.append(self.text) self.history_i -= 1 self.text = self.history[self.history_i] self.pos = len(self.text) elif wch == curses.KEY_DOWN: # down if self.history is not None and self.history_i < len(self.history)-1: self.history_i += 1 self.text = self.history[self.history_i] self.pos = len(self.text) elif wch == 7: # C-g if self.cli: BOOKMARKS_STR, HISTORY_STR = '----- Stored: -----', '----- History: -----' entries = [BOOKMARKS_STR] entries.extend([c for c in app.cfg.powercli_favs if c.strip()]) entries.append(HISTORY_STR) entries.extend([c for c in self.history if c.strip()]) selected = self.__select_item(entries, 0, 'History') if selected not in (None, -1, '', BOOKMARKS_STR, HISTORY_STR): self.text, self.pos = selected, len(selected) else: if self.history is not None and len(self.history) > 0: selected = self.__select_item(self.history, 0, 'History') if selected not in (None, -1): self.text, self.pos = selected, len(selected) # other elif wch == curses.KEY_IC: # insert self.ins = not self.ins else: curses.beep() ###################################################################### ##### DialogEntry class DialogEntry: """An entry dialog to enter a dir, a file, a pattern…""" def __init__(self, title, help, text='', history=None, is_files=True): h, w = 6, max(len(help)+5, app.w//2) try: self.win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) self.entry = EntryLine(self, w-4, (app.h-h)//2+2, (app.w-w+4)//2, text, history=history, is_files=is_files, cli=False) self.btns = Yes_No_Buttons(w, h, 0) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.w, self.title, self.help = w, title, help self.win.keypad(1) self.win.bkgd(app.CLR['dialog']) self.active_widget = self.entry def show(self): self.win.erase() self.win.box() self.win.addstr(0, (self.w-len(self.title)-2)//2, ' %s ' % self.title, app.CLR['dialog_title']) self.win.addstr(1, 2, '%s:' % self.help) self.win.refresh() self.entry.show() self.btns.show() def run(self): self.show() curses.curs_set(1) quit = False while not quit: self.btns.show() ans = self.active_widget.manage_keys() if ans == -1: # Ctrl-C quit = True answer = None elif ans == ord('\t'): # tab if self.active_widget == self.entry: self.active_widget = self.btns self.btns.active = 1 curses.curs_set(0) answer = self.entry.text elif self.active_widget == self.btns and self.btns.active == 1: self.btns.active = 2 curses.curs_set(0) answer = None else: self.active_widget = self.entry self.btns.active = 0 curses.curs_set(1) elif ans == curses.KEY_BTAB: # S+tab if self.active_widget == self.entry: self.active_widget = self.btns self.btns.active = 2 curses.curs_set(0) answer = None elif self.active_widget == self.btns and self.btns.active == 1: self.active_widget = self.entry self.btns.active = 0 curses.curs_set(1) else: self.active_widget = self.btns self.btns.active = 1 curses.curs_set(0) answer = self.entry.text elif ans == 10: # return values quit = True answer = self.entry.text.strip() curses.curs_set(0) self.pwin.hide() curses.panel.update_panels() app.display() return answer class DialogDoubleEntry: """A dialog with 2 entries""" def __init__(self, title, help1, help2, text1, text2='', history1=None, history2=None, is_files=True): h, w = 9, max(len(help2)+5, app.w//2) try: self.win = curses.newwin(h, w, (app.h-h)//2, (app.w-w)//2) self.entry1 = EntryLine(self, w-4, (app.h-h)//2+2, (app.w-w+4)//2, text1, history=history1, is_files=is_files, cli=False) self.entry2 = EntryLine(self, w-4, (app.h-h)//2+5, (app.w-w+4)//2, text2, history=history2, is_files=is_files, cli=False) self.btns = Yes_No_Buttons(w, h, 3) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.w, self.title, self.help1, self.help2 = w, title, help1, help2 self.win.keypad(1) self.win.bkgd(app.CLR['dialog']) self.active_widget = self.entry1 def show(self): self.win.erase() self.win.box() self.win.addstr(0, (self.w-len(self.title)-2)//2, ' %s ' % self.title, app.CLR['dialog_title']) self.win.addstr(1, 2, '%s:' % self.help1) self.win.addstr(4, 2, '%s:' % self.help2) self.win.refresh() self.entry1.show() self.entry2.show() self.btns.show() def run(self): self.show() curses.curs_set(1) quit = False while not quit: self.btns.show() ans = self.active_widget.manage_keys() if ans == -1: # Ctrl-C quit = True answer = None elif ans == ord('\t'): # tab if self.active_widget == self.entry1: self.active_widget = self.entry2 elif self.active_widget == self.entry2: self.active_widget = self.btns self.btns.active = 1 curses.curs_set(0) answer = self.entry1.text, self.entry2.text elif self.active_widget == self.btns and self.btns.active == 1: self.btns.active = 2 answer = None else: self.active_widget = self.entry1 self.btns.active = 0 curses.curs_set(1) elif ans == curses.KEY_BTAB: # S+tab if self.active_widget == self.entry1: self.active_widget = self.btns self.btns.active = 2 curses.curs_set(0) answer = None elif self.active_widget == self.entry2: self.active_widget = self.entry1 elif self.active_widget == self.btns and self.btns.active == 1: self.active_widget = self.entry2 self.btns.active = 0 curses.curs_set(1) answer = self.entry1.text, self.entry2.text else: self.btns.active = 1 answer = self.entry1.text, self.entry2.text elif ans == 10: # return values quit = True answer = self.entry1.text.strip(), self.entry2.text.strip() curses.curs_set(0) self.pwin.hide() curses.panel.update_panels() app.display() return answer ######################################################################## ##### DialogPerms & DialogOwner class DialogPerms: """Dialog to change file permissions""" def __init__(self, filename, perms, i=0, n=0): self.h, self.w = 7+4, 42+4 x0, y0 = int((app.w-self.w)/2), int((app.h-self.h)/2) try: self.win = curses.newwin(self.h, self.w, y0, x0) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.bkgd(app.CLR['dialog']) self.file = filename self.perms_old = perms2str(perms) self.perms = [l for l in self.perms_old] self.recursive = False self.i, self.n, self.entry_i = i, n, 0 def show_btns(self): attr_sel, attr_no = app.CLR['button_active'], app.CLR['button_inactive'] self.win.addstr(self.h-2, self.w-21, '[]', attr_sel if self.entry_i==11 else attr_no) self.win.addstr(self.h-2, self.w-13, '[ Cancel ]', attr_sel if self.entry_i==12 else attr_no) if self.n > 1: self.win.addstr(self.h-2, 3, '[ All ]', attr_sel if self.entry_i==13 else attr_no) self.win.addstr(self.h-2, 12, '[ Ignore ]', attr_sel if self.entry_i==14 else attr_no) def show(self): self.win.erase() self.win.box() attr, attr_sel, attr_no = app.CLR['dialog_title'], app.CLR['dialog_perms'], app.CLR['dialog'] title = 'Change file(s) permissions' self.win.addstr(0, int((self.w-len(title)-2)/2), ' {} '.format(title), attr) if self.n > 1: buf = '{}/{}'.format(self.i, self.n) lbuf = len(buf) self.win.addstr(2, self.w-2-len(buf), buf) else: lbuf = 0 self.win.addstr(2, 2, 'File:') self.win.addstr(2, 8, text2wrap(self.file, self.w-11-lbuf, fill=True), attr) self.win.addstr(4, 2, ' owner group other recursive') self.win.addstr(5, 2, 'new: [---] [---] [---] [ ]') self.win.addstr(6, 2, 'old: [---] [---] [---] [ ]') perms = ''.join(self.perms) self.win.addstr(5, 9, perms[0:3], attr_sel if self.entry_i==0 else attr_no) self.win.addstr(5, 16, perms[3:6], attr_sel if self.entry_i==1 else attr_no) self.win.addstr(5, 23, perms[6:9], attr_sel if self.entry_i==2 else attr_no) self.win.addstr(5, 39, 'X' if self.recursive else ' ', attr_sel if self.entry_i==3 else attr_no) self.win.addstr(6, 9, self.perms_old[0:3]) self.win.addstr(6, 16, self.perms_old[3:6]) self.win.addstr(6, 23, self.perms_old[6:9]) self.show_btns() self.win.refresh() def manage_keys(self): order = [0, 1, 2, 3, 13, 14, 11, 12] if self.n>1 else [0, 1, 2, 3, 11, 12] nels = len(order) while True: self.show() ch = self.win.getch() if ch in (0x03, 0x1B, ord('q'), ord('Q')): return -1, False, False elif ch in (ord('\t'), 0x09, curses.KEY_DOWN, curses.KEY_RIGHT): i = order.index(self.entry_i) self.entry_i = order[0 if i==nels-1 else i+1] elif ch in (curses.KEY_UP, curses.KEY_LEFT, curses.KEY_BTAB): i = order.index(self.entry_i) self.entry_i = order[nels-1 if i==0 else i-1] elif ch in (ord('r'), ord('R')): if 0 <= self.entry_i <= 2: d = self.entry_i * 3 self.perms[d] = 'r' if self.perms[d]=='-' else '-' elif ch in (ord('w'), ord('W')): if 0 <= self.entry_i <= 2: d = 1 + self.entry_i * 3 self.perms[d] = 'w' if self.perms[d]=='-' else '-' elif ch in (ord('x'), ord('X')): if 0 <= self.entry_i <= 2: d = 2 + self.entry_i * 3 self.perms[d] = 'x' if self.perms[d]=='-' else '-' elif ch in (ord('t'), ord('T')): if self.entry_i == 2: self.perms[8] = 't' if self.perms[8]=='-' else '-' elif ch in (ord('s'), ord('S')): if 0 <= self.entry_i <= 1: d = 2 + self.entry_i * 3 self.perms[d] = 's' if self.perms[d]=='-' else '-' elif ch in (ord(' '), 0x0A, 0x0D): if self.entry_i == 3: self.recursive = not self.recursive elif self.entry_i == 12: return -1, False, False elif self.n>1 and self.entry_i==13: return self.perms, self.recursive, True elif self.n>1 and self.entry_i==14: return 0, False, False else: return self.perms, self.recursive, False elif ch in (ord('i'), ord('I')): # and self.n>1 return 0, False, False elif ch in (ord('a'), ord('A')): # and self.n>1 return self.perms, self.recursive, True else: curses.beep() def run(self): selected = self.manage_keys() self.pwin.hide() curses.panel.update_panels() return selected class DialogOwner: """Dialog to change file owner/group""" def __init__(self, filename, owner, group, owners, groups, i=0, n=0): self.h, self.w = 7+4, 45+4 x0, y0 = int((app.w-self.w)/2), int((app.h-self.h)/2) try: self.win = curses.newwin(self.h, self.w, y0, x0) self.pwin = curses.panel.new_panel(self.win) self.pwin.top() except curses.error: raise self.win.keypad(1) self.win.bkgd(app.CLR['dialog']) self.file = filename self.owner = self.owner_old = owner self.group = self.group_old = group self.owners, self.groups = owners, groups self.recursive = True self.i, self.n, self.entry_i = i, n, 0 def show_btns(self): attr_sel, attr_no = app.CLR['button_active'], app.CLR['button_inactive'] self.win.addstr(self.h-2, self.w-21, '[]', attr_sel if self.entry_i==11 else attr_no) self.win.addstr(self.h-2, self.w-13, '[ Cancel ]', attr_sel if self.entry_i==12 else attr_no) if self.n > 1: self.win.addstr(self.h-2, 3, '[ All ]', attr_sel if self.entry_i==13 else attr_no) self.win.addstr(self.h-2, 12, '[ Ignore ]', attr_sel if self.entry_i==14 else attr_no) def __fmt_text(self, text, n=10, ch='-'): l = len(text) return text[:n] if l>n else text+ch*(n-l) def show(self): self.win.erase() self.win.box() attr, attr_sel, attr_no = app.CLR['dialog_title'], app.CLR['dialog_perms'], app.CLR['dialog'] title = 'Change file(s) owner/group' self.win.addstr(0, int((self.w-len(title)-2)/2), ' {} '.format(title), attr) if self.n > 1: buf = '{}/{}'.format(self.i, self.n) lbuf = len(buf) self.win.addstr(2, self.w-2-len(buf), buf) else: lbuf = 0 self.win.addstr(2, 2, 'File:') self.win.addstr(2, 8, text2wrap(self.file, self.w-11-lbuf, fill=True), attr) self.win.addstr(4, 2, ' owner group recursive') self.win.addstr(5, 2, 'new: [----------] [----------] [ ]') self.win.addstr(6, 2, 'old: [----------] [----------] [ ]') self.win.addstr(5, 9, self.__fmt_text(self.owner), attr_sel if self.entry_i==0 else attr_no) self.win.addstr(5, 23, self.__fmt_text(self.group), attr_sel if self.entry_i==1 else attr_no) self.win.addstr(5, 42, 'X' if self.recursive else ' ', attr_sel if self.entry_i==3 else attr_no) self.win.addstr(6, 9, self.owner_old) self.win.addstr(6, 23, self.group_old) self.show_btns() self.win.refresh() def manage_keys(self): y, x = self.pwin.window().getbegyx() order = [0, 1, 3, 13, 14, 11, 12] if self.n>1 else [0, 1, 3, 11, 12] nels = len(order) while True: self.show() ch = self.win.getch() if ch in (0x03, 0x1B, ord('q'), ord('Q')): return -1, -1, False, False elif ch in (ord('\t'), 0x09, curses.KEY_DOWN, curses.KEY_RIGHT): i = order.index(self.entry_i) self.entry_i = order[0 if i==nels-1 else i+1] elif ch in (curses.KEY_UP, curses.KEY_LEFT, curses.KEY_BTAB): i = order.index(self.entry_i) self.entry_i = order[nels-1 if i==0 else i-1] elif ch in (ord(' '), 0x0A, 0x0D): if self.entry_i == 3: self.recursive = not self.recursive elif self.entry_i == 0: ret = SelectItem('Select new owner', self.owners, y+6, x+7, self.owner).run() if ret != -1: self.owner = ret elif self.entry_i == 1: ret = SelectItem('Select new group', self.groups, y+6, x+21, self.group).run() if ret != -1: self.group = ret elif self.entry_i == 12: return -1, -1, False, False elif self.n>1 and self.entry_i==13: return self.owner, self.group, self.recursive, True elif self.n>1 and self.entry_i==14: return 0, 0, False, False else: return self.owner, self.group, self.recursive, False elif ch in (ord('i'), ord('I')): # and self.n>1 return 0, 0, False, False elif ch in (ord('a'), ord('A')): # and self.n>1 return self.owner, self.group, self.recursive, True else: curses.beep() def run(self): selected = self.manage_keys() self.pwin.hide() curses.panel.update_panels() return selected ######################################################################## ##### TreeView class TreeView: """TreeView class""" def __init__(self, path=sep): if not exists(path) or not isdir(path): raise ValueError('Path does not exist or is not dir: "{}"'.format(path)) if path[-1] == sep and path != sep: path = path[:-1] self.path = path self.tree = DirsTree(path, app.cfg.options.show_dotfiles) self.__init_ui() def __init_ui(self): """initialize curses stuff""" self.__calculate_dims() try: self.win = curses.newwin(*self.dims) except curses.error: raise self.win.keypad(1) if curses.has_colors(): self.win.bkgd(app.CLR['files_reg']) def __calculate_dims(self): if app.pane_active.mode == PaneMode.half: win = app.pane_inactive.win elif app.pane_active.mode == PaneMode.full: win = app.pane_active.win h, w = win.getmaxyx() y0, x0 = win.getbegyx() self.dims = h, w, y0, x0 def display(self): h, n = app.h-4, len(self.tree) j, a, z = 0, self.tree.pos//h * h, (self.tree.pos//h+1) * h self.win.erase() self.win.attrset(app.CLR['pane_active']) self.win.box() display_scrollbar(self.win, 1, self.dims[1]-1, h, n, a, a) self.win.addstr(0, 2, ' Tree ', app.CLR['pane_header_path']) self.win.attrset(app.CLR['files_reg']) if z > n: a, z = max(n-h, 0), n for i in range(a, z): j += 1 name, depth, fullname = self.tree[i] if name == sep: self.win.addstr(j, 1, ' ') else: self.win.move(j, 1) for kk in range(depth): self.win.addstr(' ') self.win.addch(curses.ACS_VLINE) # \u2502 self.win.addstr(' ') self.win.addstr(' ') if i == n-1: self.win.addch(curses.ACS_LLCORNER) # \u2514 elif depth > self.tree.get_depth(i+1): self.win.addch(curses.ACS_LLCORNER) # \u2514 else: self.win.addch(curses.ACS_LTEE) # \u251C self.win.addch(curses.ACS_HLINE) # \u2500 self.win.addstr(' ') w, wd = app.w//2-2, 3*depth+4 if fullname == self.path: self.win.addstr(text2wrap(name, w-wd-3, fill=False), app.CLR['cursor']) if self.tree.has_children_dirs: self.win.addstr(' \u27A9') # ' ->' else: self.win.addstr(text2wrap(name, w-wd, fill=False)) app.statusbar.show_message('Path: {}'.format(self.path)) def run(self): while True: self.display() chext = 0 ch = self.win.getch() # to avoid extra chars input if ch == 0x1B: chext = 1 ch = self.win.getch() ch = self.win.getch() if ch in (ord('k'), ord('K'), curses.KEY_UP): if self.tree.pos == 0 or self.tree.is_first_sibling: continue newpos = self.tree.pos - 1 elif ch in (ord('j'), ord('j'), curses.KEY_DOWN): if self.tree.pos == len(self.tree)-1 or self.tree.is_last_sibling: continue newpos = self.tree.pos + 1 elif ch in (curses.KEY_PPAGE, curses.KEY_BACKSPACE, 0x02): if self.tree.pos-(app.h-4) >= 0: if self.tree.cur_depth == self.tree.get_depth(self.tree.pos-(app.h-4)): newpos = self.tree.pos-(app.h-4) else: newpos = self.tree.first_sibling_pos else: newpos = self.tree.first_sibling_pos elif ch in (curses.KEY_NPAGE, ord(' '), 0x06): # Ctrl-F if self.tree.pos+(app.h-4) <= len(self.tree)-1: if self.tree.cur_depth == self.tree.get_depth(self.tree.pos+(app.h-4)): newpos = self.tree.pos+(app.h-4) else: newpos = self.tree.last_sibling_pos else: newpos = self.tree.last_sibling_pos elif (ch in (curses.KEY_HOME, 0x01)) or (chext == 1) and (ch == 72): newpos = 1 elif (ch in (curses.KEY_END, 0x05)) or (chext == 1) and (ch == 70): newpos = len(self.tree) - 1 elif ch == curses.KEY_LEFT: if self.tree.pos == 0: continue newpos = self.tree.parent_pos elif ch == curses.KEY_RIGHT: new_path = self.tree.to_child() if new_path is not None: self.path = new_path continue elif ch in (10, 13): return self.path elif ch == curses.KEY_RESIZE: curses.doupdate() app.resize() self.__calculate_dims() self.win.resize(self.dims[0], self.dims[1]) self.win.mvwin(self.dims[2], self.dims[3]) continue elif ch in (ord('q'), ord('Q'), curses.KEY_F10, 0x03): # Ctrl-C return -1 else: continue # update self.path = self.tree.regenerate_from_pos(newpos) ######################################################################## ##### InternalView class InternalView: """View information on full screen""" def __init__(self, title, lbuf, center=True): self.title = title self.prepare_lines(lbuf, center) self.init_curses() def prepare_lines(self, lbuf, center): self.lbuf = [(text2wrap(l, app.w-2).strip(), c) for l, c in lbuf] self.nlines = len(lbuf) self.large = self.nlines > app.h-2 if center: col_max = max_length([l for l, _ in self.lbuf]) self.x0, self.y0 = (app.w-col_max)//2, 0 if self.large else (app.h-2-self.nlines)//2 else: self.x0, self.y0 = 1, 0 if self.large else 1 self.y = 0 def init_curses(self): try: win_title = curses.newwin(1, app.w, 0, 0) self.win_body = curses.newwin(app.h-2, app.w, 1, 0) win_status = curses.newwin(1, app.w, app.h-1, 0) except curses.error: raise win_title.bkgd(app.CLR['header']) self.win_body.bkgd(app.CLR['view_white_on_black']) win_status.bkgd(app.CLR['statusbar']) self.win_body.keypad(1) win_title.erase() win_status.erase() title = text2wrap(self.title, app.w-1).strip() win_title.addstr(0, (app.w-length(title))//2, title) status = '' if self.large else 'Press any key to continue' win_status.addstr(0, (app.w-len(status))//2, status) win_title.refresh() win_status.refresh() def show(self): self.win_body.erase() for i, (l, c) in enumerate(self.lbuf[self.y:self.y+app.h-2]): self.win_body.addstr(self.y0+i, self.x0, l, app.CLR[c]) self.win_body.refresh() def run(self): if self.large: while True: self.show() ch = self.win_body.getch() if ch in (ord('k'), ord('K'), curses.KEY_UP): self.y = max(self.y-1, 0) if ch in (ord('j'), ord('J'), curses.KEY_DOWN): self. y = min(self.y+1, self.nlines-1) elif ch in (curses.KEY_HOME, 0x01): self.y = 0 elif ch in (curses.KEY_END, 0x05): self.y = self.nlines - 1 elif ch in (curses.KEY_PPAGE, 0x08, 0x02, curses.KEY_BACKSPACE): self.y = max(self.y-app.h+2, 0) elif ch in (curses.KEY_NPAGE, ord(' '), 0x06): self.y = min(self.y+app.h-2, self.nlines-1) elif ch in (0x1B, ord('q'), ord('Q'), curses.KEY_F3, curses.KEY_F10): break else: self.show() while not self.win_body.getch(): pass ######################################################################## lfm-3.1/lfm/ui.py0000644000175000001440000006425413123724454012744 0ustar inigousers# -*- coding: utf-8 -*- import errno import curses from os import getuid, pardir from os.path import dirname, exists, join from datetime import datetime from preferences import Config, load_colortheme, load_keys, History from folders import new_folder, is_delete_oldfs from utils import num2str, length, text2wrap, get_realpath, run_in_background, run_shell, ProcessCommand from ui_widgets import display_scrollbar, DialogError, DialogConfirm, EntryLine, InternalView from key_defs import key_bin2str from actions import do from common import * ######################################################################## ##### Definitions and module variables colors_table = {'black': curses.COLOR_BLACK, 'blue': curses.COLOR_BLUE, 'cyan': curses.COLOR_CYAN, 'green': curses.COLOR_GREEN, 'magenta': curses.COLOR_MAGENTA, 'red': curses.COLOR_RED, 'white': curses.COLOR_WHITE, 'yellow': curses.COLOR_YELLOW} ##### Module variables app, cfg = None, None ######################################################################## ##### Main window class UI: def __init__(self, cfg, win, paths1, paths2): logging.debug('Create UI') self.cfg = cfg self.win = win self.w = self.h = 0 self.CLR = dict() self.init_curses() self.pane1 = Pane(self, paths1) self.pane2 = Pane(self, paths2) self.statusbar = StatusBar(self) self.cli = PowerCLI(self) self.focus_pane(self.pane1) self.resize() def init_curses(self): curses.cbreak() curses.raw() self.win.leaveok(1) self.win.keypad(1) curses.curs_set(0) self.init_colors() self.init_keys() self.init_history() # HACK: ugly hack to inject main app in that module import ui_widgets ui_widgets.app = self def init_colors(self): if curses.has_colors(): try: colors = load_colortheme() except FileNotFoundError: raise for i, col in enumerate(colors.keys()): fg, bg = colors[col] light = fg.endswith('*') fg = fg[:-1] if fg.endswith('*') else fg bg = bg[:-1] if bg.endswith('*') else bg color_fg, color_bg = colors_table[fg], colors_table[bg] curses.init_pair(i+1, color_fg, color_bg) self.CLR[col] = curses.color_pair(i+1) if light: self.CLR[col] = self.CLR[col] | curses.A_BOLD else: for col in COLOR_ITEMS: self.CLR[col] = curses.color_pair(0) def init_keys(self): try: self.keys = load_keys() except FileNotFoundError: raise def init_history(self): self.history = History() if self.cfg.options.save_history_at_exit: try: self.history.load() except Exception as e: return def focus_pane(self, pane): pane.focus = True otherpane = self.pane2 if pane==self.pane1 else self.pane1 otherpane.focus = False @property def pane_active(self): return self.pane1 if self.pane1.focus else self.pane2 @property def pane_inactive(self): return self.pane2 if self.pane1.focus else self.pane1 def resize(self): h, w = self.win.getmaxyx() logging.debug('Resize UI: w={}, h={}'.format(w, h)) self.h, self.w = h, w if w == 0 or h == 2: return if w < MIN_COLUMNS: raise LFMTerminalTooNarrow self.win.resize(self.h, self.w) if self.pane1.mode == PaneMode.full: self.pane1.resize(0, 0, h-1, w) elif self.pane1.mode == PaneMode.hidden: self.pane1.resize(0, 0, h-1, w) else: self.pane1.resize(0, 0, h-1, w//2) if self.pane2.mode == PaneMode.full: self.pane2.resize(0, 0, h-1, w) elif self.pane2.mode == PaneMode.hidden: self.pane2.resize(0, 0, h-1, w) else: self.pane2.resize(0, w//2, h-1, w//2) self.statusbar.resize(h-1, w) self.cli.resize(h-1, w) self.display() def clear_screen(self): logging.debug('Clear screen') self.pane1.clear() self.pane2.clear() self.statusbar.clear() curses.doupdate() def display(self): logging.debug('Display UI') self.pane1.display() self.pane2.display() self.display_statusbar_or_powercli() self.win.noutrefresh() curses.doupdate() def display_half(self): logging.debug('Display half UI') self.pane_active.display() self.display_statusbar_or_powercli() self.win.noutrefresh() curses.doupdate() def display_statusbar_or_powercli(self): if self.cli.visible: self.cli.display() else: self.statusbar.display() def run(self): self.display() while True: ret, extra = self.get_key() if ret == RetCode.quit_chdir: return extra elif ret == RetCode.quit_nochdir: return None elif ret == RetCode.fix_limits: self.pane_active.tab_active.fix_limits() self.display_half() elif ret == RetCode.full_redisplay: self.display() elif ret == RetCode.half_redisplay: self.display_half() def get_key(self): self.win.nodelay(False) km = KeyModifier.none key = self.win.getch() if key == 27: # Esc or Alt km = KeyModifier.alt self.win.nodelay(True) key = self.win.getch() if key == -1: # Esc km, key = KeyModifier.none, 27 if key == curses.KEY_RESIZE: self.resize() return RetCode.full_redisplay, None key_str = key_bin2str((km, key)) action = self.keys[(km, key)] if (km, key) in self.keys else None logging.debug('Key pressed: {0} [{1:#x}] => {2} => {3} -> {4}' .format(curses.keyname(key), key, str((km, key)), key_str, action if action else '')) return do(self, action) if action else (RetCode.nothing, None) ######################################################################## ##### Pane class Pane: def __init__(self, ui, paths): logging.debug('Create Pane') self.ui = ui self.mode = PaneMode.half self.focus = False self.tabs = [Tab(p) for p in paths[:MAX_TABS]] self.tab_active = self.tabs[0] # ui try: self.win_tabs = curses.newwin(1, 1, 0, 0) self.win = curses.newwin(10, 10, 0, 0) except curses.error: raise self.win.bkgd(self.ui.CLR['pane_inactive']) def resize(self, y0, x0, h, w): logging.debug('Resize Pane: x0={}, y0={}, w={}, h={}'.format(x0, y0, h, w)) self.x0, self.y0 = x0, y0 self.w, self.h = w, h self.fh = h-4 if self.mode==PaneMode.half else h-1 self.win_tabs.resize(2, w) self.win_tabs.mvwin(y0, x0) self.win.resize(h-1, w) self.win.mvwin(y0+1, x0) try: for tab in self.tabs: tab.fix_limits() except AttributeError: pass def clear(self): self.win.erase() self.win_tabs.erase() self.win.noutrefresh() self.win_tabs.noutrefresh() def display(self): logging.debug('Display Pane') if self.mode == PaneMode.hidden: return tab = self.tab_active CLR = self.ui.CLR # tabs self.win_tabs.erase() self.win_tabs.addstr(0, 0, ' '*self.w, CLR['header']) wtab = self.w // MAX_TABS for i, t in enumerate(self.tabs): pathname = '/' if t.fs.basename=='' else t.fs.basename buf = '[' + text2wrap(pathname, wtab-2, start_pct=.5) + ']' self.win_tabs.addstr(0, wtab*i, buf, CLR['tab_active' if t==tab else 'tab_inactive']) # contens: self.win.erase() if self.mode == PaneMode.half: self.__display_panehalf(tab, CLR) elif self.mode == PaneMode.full: self.__display_panefull(tab, CLR) # refresh self.win_tabs.noutrefresh() self.win.noutrefresh() def __display_panehalf(self, tab, CLR): if self.focus: attr, attr_path = CLR['pane_active'], CLR['pane_header_path'] else: attr, attr_path = CLR['pane_inactive'], CLR['pane_inactive'] self.win.attrset(attr) # box self.win.box() self.win.addstr(0, 2, text2wrap(tab.fs.path_str, self.w-5, start_pct=.33, fill=False), attr_path) col2 = self.w - 14 # sep between size and date: w - len(mtime) - 2x borders col1 = col2 - 8 # sep between filename and size: col2 - len(size) - 1x border self.win.addstr(1, 1, 'Name'.center(col1-2)[:col1-2], CLR['pane_header_titles']) self.win.addstr(1, col1+2, 'Size', CLR['pane_header_titles']) self.win.addstr(1, col2+5, 'Date', CLR['pane_header_titles']) if tab.fs.cfg.show_dotfiles: self.win.addstr(0, self.w-3, '·', attr) if tab.fs.cfg.filters: self.win.addstr(0, self.w-2, 'f', attr) # files fmt = [('type', 1), ('name', col1-2), ('sep', 1), ('size', 7), ('sep', 1), ('mtime', 12)] for i in range(self.h-4): if i+tab.a >= len(tab.fs): break f = tab.fs[tab.a+i] if self.focus and tab.i == tab.a+i: attr = 'cursor_selected' if f in tab.selected else 'cursor' else: attr = 'selected_files' if f in tab.selected else 'files_' + f.get_type_from_ext(self.ui.cfg.files_ext) self.win.addstr(2+i, 1, f.format(fmt), CLR[attr]) # vertical separators self.win.vline(1, col1, curses.ACS_VLINE, self.h-3) self.win.vline(1, col2, curses.ACS_VLINE, self.h-3) if self.focus: self.win.vline(tab.i-tab.a+2, col1, curses.ACS_VLINE, 1, CLR['cursor']) self.win.vline(tab.i-tab.a+2, col2, curses.ACS_VLINE, 1, CLR['cursor']) # scrollbar display_scrollbar(self.win, 2, self.w-1, self.h-4, len(tab.fs), tab.i, tab.a) def __display_panefull(self, tab, CLR): self.win.attrset(CLR['pane_inactive']) # files col = self.w - 64 # 1x border fmt = [('type', 1), ('mode', 9), ('sep', 2), ('owner', 10), ('sep', 2), ('group', 10), ('sep', 2), ('size', 7), ('sep', 2), ('mtime2', 16), ('sep', 2), ('name', col)] for i in range(self.h-1): if i+tab.a >= len(tab.fs): break f = tab.fs[tab.a+i] if tab.i == tab.a+i: attr = 'cursor_selected' if f in tab.selected else 'cursor' else: attr = 'selected_files' if f in tab.selected else 'files_' + f.get_type_from_ext(self.ui.cfg.files_ext) self.win.addstr(i, 0, f.format(fmt, sep=' '), CLR[attr]) # scrollbar display_scrollbar(self.win, 0, self.w-1, self.h-1, len(tab.fs), tab.i, tab.a) def change_mode(self, newmode): otherpane = self.ui.pane2 if self==self.ui.pane1 else self.ui.pane1 if self.mode == PaneMode.full: otherpane.mode = PaneMode.half if newmode == PaneMode.full: otherpane.mode = PaneMode.hidden self.mode = newmode self.ui.resize() def refresh(self): for tab in self.tabs: tab.refresh() def insert_new_tab(self, path, lefttab): newtab = Tab(path) self.tabs.insert(self.tabs.index(lefttab)+1, newtab) self.tab_active = newtab def close_tab(self, tab): idx = self.tabs.index(tab) tab.close() self.tabs.remove(tab) self.tab_active = self.tabs[idx-1] ######################################################################## ##### Tab class Tab: def __init__(self, path): self.fs = None self.history = [] self.goto_folder(path) def __check_rebuild(self): if self.fs.vfs: rebuild = 1 if app.cfg.options.rebuild_vfs is True else 0 if app.cfg.confirmations.ask_rebuild_vfs: rebuild = DialogConfirm('Rebuild vfs file', self.fs.base_filename, rebuild) return rebuild==1 else: return False def close(self): self.fs.exit(all_levels=True, rebuild=self.__check_rebuild()) def goto_folder(self, path, delete_vfs_tree=False, files=None): """Called when chdir to a new path""" oldpath = self.fs.path_str if self.fs else None oldfs = self.fs try: if files: # search vfs self.fs = new_folder(path, self.fs, files=files) else: rebuild = self.__check_rebuild() if is_delete_oldfs(path, self.fs) else False self.fs = new_folder(path, self.fs, rebuild_if_exit=rebuild) self.fs.cfg.filters = oldfs.cfg.filters if oldfs else '' except PermissionError as e: logging.warning('ERROR: cannot enter in {}: {}'.format(path, str(e))) app.display() DialogError('Cannot chdir {}\n{}'.format(path, str(e).split(':', 1)[0])) return except UserWarning as e: logging.warning('ERROR: cannot enter in {}: {}'.format(path, str(e))) app.display() DialogError('Cannot chdir {}\n{}'.format(path, str(e))) return except FileNotFoundError: logging.warning('ERROR: cannot enter in {}: invalid directory?'.format(path)) app.display() DialogError('Cannot chdir {}\nInvalid directory?'.format(path)) return if delete_vfs_tree: oldfs.exit(all_levels=True) self.a = self.i = 0 self.selected = [] self.refresh(first=True) if oldpath and VFS_STRING not in oldpath: if oldpath in self.history: self.history.remove(oldpath) self.history.append(oldpath) self.history = self.history[-HISTORY_MAX:] def reload(self): """Called when contents have changed""" try: self.fs.load() except OSError as err: if err.errno == errno.ENOENT: # dir deleted? pardir = self.fs.pdir while not exists(pardir): pardir = dirname(pardir) self.goto_folder(pardir) self.refresh() def refresh(self, first=False): """Called when config or filters have changed""" if not first: oldi = self.i oldf = self.fs[self.i].name oldselected = [f.name for f in self.selected] self.fs.cfg.fill_with_app(cfg) self.fs.refresh() self.n = len(self.fs) i = 0 if first else self.fs.pos(oldf) self.i = oldi if i==-1 else i self.a = divmod(self.i, self.n)[0] * self.n if not first: self.fix_limits() self.selected = list(filter(None, [self.fs.lookup(f) for f in oldselected])) def fix_limits(self): self.i = max(0, min(self.i, self.n-1)) self.a = int(self.i//app.pane_active.fh * app.pane_active.fh) def focus_file(self, filename): i = self.fs.pos(filename) if i != -1: self.i = i self.fix_limits() @property def dirname(self): return self.fs.pdir @property def current_filename(self): return self.fs[self.i].name @property def current_filename_full(self): return join(self.fs.pdir, self.fs[self.i].name) @property def current(self): return self.fs[self.i] @property def selected_or_current(self): if len(self.selected) == 0: cur = self.fs[self.i] return list() if cur.name==pardir else [cur] else: return self.selected @property def selected_or_current2(self): return self.selected if self.selected else [self.fs[self.i]] ######################################################################## ##### Statusbar class StatusBar: def __init__(self, ui): logging.debug('Create StatusBar') self.ui = ui try: self.win = curses.newwin(1, 10, 1, 0) except curses.error: raise self.win.bkgd(self.ui.CLR['statusbar']) def resize(self, y0, w): logging.debug('Resize StatusBar: y0={}, w={}'.format(y0, w)) self.y0, self.w = y0, w self.win.resize(1, w) self.win.mvwin(y0, 0) def clear(self): self.win.erase() self.win.noutrefresh() def display(self): logging.debug('Display StatusBar') self.win.erase() tab = self.ui.pane_active.tab_active if len(tab.selected) > 0: if self.w >= 45: size = sum([f.size for f in tab.selected]) self.win.addstr(' %s bytes in %d files' % (num2str(size), len(tab.selected))) else: if self.w >= 80: s = 'File: %4d of %-4d' % (tab.i+1, tab.n) if tab.fs.cfg.filters: s+= ' [%d filtered]' % tab.fs.nfiltered self.win.addstr(s) filename = text2wrap(get_realpath(tab), self.w-20-len(s), fill=False) self.win.addstr(0, len(s)+4, 'Path: ' + filename) if self.w > 10: try: self.win.addstr(0, self.w-8, 'F1=Help') except: pass self.win.refresh() def show_message(self, text): self.win.erase() self.win.addstr(0, 1, text2wrap(text, self.w-2, fill=False)) self.win.refresh() ######################################################################## ##### PowerCLI class PowerCLI: RUN_NORMAL, RUN_BACKGROUND, RUN_NEEDCURSESWIN = range(3) def __init__(self, ui): logging.debug('Create PowerCLI') self.ui = ui self.visible = self.running = False try: self.win = curses.newwin(1, 10, 1, 0) except curses.error: raise self.win.bkgd(self.ui.CLR['powercli_text']) self.entry, self.text, self.pos = None, '', 0 def toggle(self): if self.visible: self.visible = self.running = False else: self.visible, self.running = True, False def resize(self, y0, w): logging.debug('Resize PowerCLI: y0={}, w={}'.format(y0, w)) self.y0, self.w = y0, w self.win.resize(1, w) self.win.mvwin(y0, 0) def display(self): if self.running: return logging.debug('Display PowerCLI') tab = self.ui.pane_active.tab_active self.win.erase() path = text2wrap(tab.fs.path_str, self.ui.w//6, start_pct=0, fill=False) prompt = '[{}]{} '.format(path, '#' if getuid()==0 else '$') lprompt = length(prompt) self.win.addstr(0, 0, prompt, self.ui.CLR['powercli_prompt']) self.win.noutrefresh() curses.curs_set(1) self.running = True self.entry = EntryLine(self, self.w-lprompt, self.y0, lprompt, self.text, history=self.ui.history['cli'][:], is_files=True, cli=True) self.entry.pos = self.pos self.entry.show() ans = self.entry.manage_keys() if ans == -1: # Ctrl-C cmd, self.text, self.pos = None, '', 0 elif ans == -2: # Ctrl-X cmd, self.text, self.pos = None, self.entry.text, self.entry.pos elif ans == 10: # return cmd, self.text, self.pos = self.entry.text.strip(), '', 0 if cmd: self.execute(cmd) else: raise ValueError curses.curs_set(0) self.visible = False self.ui.display() def execute(self, cmd): self.ui.history.append('cli', cmd) logging.debug('PowerCLI Execute: |{}|'.format(cmd)) selected = [f for f in self.ui.pane_active.tab_active.selected_or_current2] if cmd[-1] == '&': mode, cmd = PowerCLI.RUN_BACKGROUND, cmd[:-1].strip() elif cmd[-1] == '$': mode, cmd = PowerCLI.RUN_NEEDCURSESWIN, cmd[:-1].strip() else: mode = PowerCLI.RUN_NORMAL for f in selected: try: cmd2 = self.__replace_cli(cmd, f) except Exception as err: log.warning('Cannot execute PowerCLI command: {}\n{}'.format(cmd2, str(err))) DialogError('Cannot execute PowerCLI command:\n {}\n\n{}'.format(cmd2, str(err))) else: if self.__run(cmd2, self.ui.pane_active.tab_active.dirname, mode) == -1: self.ui.display() if DialogConfirm('Error running PowerCLI', 'Do you want to stop now?') == 1: break self.ui.pane_active.tab_active.selected = [] def __replace_cli(self, cmd, f): # prepare variables tab = self.ui.pane_active.tab_active filename = f.name cur_directory = tab.dirname other_directory = self.ui.pane_inactive.tab_active.dirname fullpath = f.pfile filename_noext = f.name_noext ext = f.ext all_selected = [s.name for s in tab.selected] all_files = [elm for elm in tab.fs.get_filenames() if elm is not pardir] try: selection_idx = all_selected.index(filename)+1 except ValueError: selection_idx = 0 tm = datetime.fromtimestamp(f.mtime) ta = datetime.fromtimestamp(f.stat.st_atime) tc = datetime.fromtimestamp(f.stat.st_ctime) tnow = datetime.now() dm = tm.date() da = ta.date() dc = tc.date() dnow = tnow.date() # conversion table lcls = {'f': filename, 'v': filename, 'F': fullpath, 'E': filename_noext, 'e': ext, 'p': cur_directory, 'o': other_directory, 's': all_selected, 'a': all_files, 'i': selection_idx, 'dm': dm, 'da': da, 'dc': dc, 'dn': dnow, 'tm': tm, 'ta': ta, 'tc': tc, 'tn': tnow} for k, bmk in self.ui.cfg.bookmarks.items(): lcls['b{}'.format(k)] = bmk # and replace, first python code, and then variables cmd = self.__replace_python(cmd, lcls) cmd = self.__replace_variables(cmd, lcls) return cmd def __replace_python(self, cmd, lcls): lcls = dict([('__lfm_{}'.format(k), v) for k, v in lcls.items()]) # get chunks chunks, st = {}, 0 while True: i = cmd.find('{', st) if i == -1: break j = cmd.find('}', i+1) if j == -1: raise SyntaxError('{ at %d position has not ending }' % i) else: chunks[(i+1, j)] = cmd[i+1:j].replace('$', '__lfm_') st = j + 1 # evaluate if chunks == {}: return cmd buf, st = '', 0 for i, j in sorted(chunks.keys()): buf += cmd[st:i-1] try: translated = eval(chunks[(i, j)], {}, lcls) except Exception as err: raise SyntaxError(str(err).replace('__lfm_', '$')) buf += translated st = j+1 buf += cmd[st:] return buf def __replace_variables(self, cmd, lcls): for k, v in lcls.items(): if k in ('i', ): cmd = cmd.replace('${}'.format(k), str(v)) elif k in ('dm', 'da', 'dc', 'dn', 'tm', 'ta', 'tc', 'tn'): cmd = cmd.replace('${}'.format(k), str(v).split('.')[0]) elif k in ('s', 'a'): cmd = cmd.replace('${}'.format(k), ' '.join(['"{}"'.format(f) for f in v])) else: cmd = cmd.replace('${}'.format(k), v) return cmd def __run(self, cmd, path, mode): curses.curs_set(0) if mode == PowerCLI.RUN_NEEDCURSESWIN: run_shell(cmd, path) st, msg, err = 0, '', '' elif mode == PowerCLI.RUN_BACKGROUND: run_in_background(cmd, path) st, msg, err = 0, '', '' else: # PowerCLI.RUN_NORMAL st, msg, err = ProcessCommand('Executing PowerCLI', cmd, cmd, path).run() if err: log.warning('Error running PowerCLI command: {}\n{}'.format(cmd, str(err))) DialogError('Error running PowerCLI command:\n {}\n\n{}'.format(cmd, str(err))) if st != -100 and msg: if self.ui.cfg.options.show_output_after_exec: if DialogConfirm('Executing PowerCLI', 'Show output?', 1) == 1: lst = [(l, 'view_white_on_black') for l in msg.split('\n')] InternalView('Output of: {}'.format(cmd), lst, center=False).run() return st ######################################################################## ##### Main def init_config(use_wide_chars): global cfg cfg = Config() cfg.load() # HACK: ugly hack to inject option in that module import utils utils.use_wide_chars = True if use_wide_chars else cfg.options.use_wide_chars log.info('Support wide chars: {}'.format(utils.use_wide_chars)) return cfg def launch_ui(win, path1, path2, use_wide_chars): global app cfg = init_config(use_wide_chars) try: app = UI(cfg, win, [path1], [path2]) ret = app.run() except LFMTerminalTooNarrow as e: DialogError('Terminal too narrow to show contents.\nIt should have {} columns at mininum.'.format(MIN_COLUMNS)) return None if app.cfg.options.save_configuration_at_exit: try: app.cfg.save() except Exception as e: DialogError('Cannot save configuration file\n{}'.format(str(e))) if app.cfg.options.save_history_at_exit: try: app.history.save() except Exception as e: DialogError('Cannot save history file\n{}'.format(str(e))) return ret def run_app(path1, path2, use_wide_chars): return curses.wrapper(launch_ui, path1, path2, use_wide_chars) ######################################################################## lfm-3.1/lfm/lfm0000755000175000001440000000172113123762467012455 0ustar inigousers#!/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2001-17 Iñigo Serna # Time-stamp: <2017-06-25 18:30:47 inigo> # # 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 . from sys import exit, version_info from lfm.lfm import lfm_start ver = (version_info.major, version_info.minor) if ver < (3, 4): print('Python 3.4 or higher is required to run lfm.') exit(-1) lfm_start()