diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e3d38a --- /dev/null +++ b/.gitignore @@ -0,0 +1,102 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +modules.order +Module.symvers +Mkfile.old +dkms.conf +# http://www.gnu.org/software/automake + +Makefile.in +/ar-lib +/mdate-sh +/py-compile +/test-driver +/ylwrap + +# http://www.gnu.org/software/autoconf + +/autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.guess +/config.h.in +/config.sub +/configure +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 + +# https://www.gnu.org/software/libtool/ + +/ltmain.sh + +# http://www.gnu.org/software/texinfo + +/texinfo.tex + +# --vp +.deps/ + +*.sublime-workspace +/!* +/INSTALL +/Makefile +/clex-*.tar.gz +/clex.spec +/config.h +/config.log +/config.status +/src/Makefile +/src/clex +/src/help.inc +/src/kbd-test + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..6880570 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +The author of CLEX is Vlado Potisk. + +Many thanks to all the people who have taken the time to submit +suggestions for enhancements or problem reports. diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..c76356a --- /dev/null +++ b/ChangeLog @@ -0,0 +1,251 @@ +* * * CLEX Revision History * * * + +4.7 released on 15-AUG-2022 + + Important announcement: + * CLEX has moved to GitHub, all links were updated. The + new project home is https://github.com/xitop/clex + + Problems fixed: + * Fixed a build issue on MacOS. It was related to wide + characters in the ncurses library. Patch provided by + a maintainer from MacPorts. + + +4.6.patch10 released on 30-SEP-2020 + + Problems fixed: + * Under certain rare circumstances (more than 384 different + directories visited) the displayed directory names did not + match the names stored in the panel. + + New or improved functionality: + * The sort panel actions are described more accurately. + * Pressing ctrl-C now leaves the config panel. + + +4.6.patch9 released on 08-JUN-2018 + + New or improved functionality: + * Support for GPM mouse and other mice was added + when compiled with NCURSES version 6.0 or newer. + + +4.6.patch8 released on 05-MAY-2018 + + Problems fixed: + * Some typos were corrected. Thanks to Tobias Frost + for sending a patch. + + New or improved functionality: + * The 'configure' script was modified so that CLEX compiles + without warnings with recent gcc and glibc versions. + + +4.6.patch7 released on 23-JUN-2017 + + Problems fixed: + * Non-ASCII - but printable - Unicode characters are now + allowed in the xterm window title. + * A backtick character inside of double quotes is now + properly quoted. + * Attempts to insert an Alt-key combination into the + editing line (i.e. Ctrl-V Alt-X) invoked the function + bound to that Alt-key combination. This is now fixed. + + New or improved functionality: + * Mouse clicks on the top (bottom) frame scroll the panel + one page up (down) respectively. Panel filter control + with a mouse was removed in order not to interfere with + the new page down function. + * The file rename function does not replace spaces + with underscores. + * The Unicode non-breaking space (NBSP) is marked as a special + character. Shells do not treat this character as a separator. + Commands containing NBSP written by mistake usually fail + and the error is not easy to find. + * User and group names up to 16 characters are displayed + without truncating in the file panel. (The limit was + 9 characters) + * User names are never truncated in the user panel. + * The RPM spec file is now included in the source code + tarball. Previously this file has to be downloaded + separately or built from a provided template. This extra + step is now unnecessary and an RPM package can be built + simply with: rpmbuild -tb clex.X.Y.Z.tar.gz + + +4.6.patch6 released on 31-AUG-2013 + + Problems fixed: + * Several wide character buffer sizes were computed in + incorrect units. No buffer overflows were actually + occurring, but such code is not usable if compiled with + protection against overflows, e.g. with gcc and + FORTIFY_SOURCE=2. Problem noted and a fix proposed by + Rudolf Polzer. + * A bug in the file I/O error reporting code of the + directory compare function was found by Rudolf Polzer. + + New or improved functionality: + * New setting in the sort panel: hidden files can be + excluded from the file list. + + +4.6.patch5 released on 19-JUL-2011 + + Problems fixed: + * Some keys did not work in the log panel's filter. + + +4.6.4 released on 21-MAY-2011 + + Problems fixed: + * Name completion did not expand a single tilde as a home + directory. + * A mouseclick on a certain screen area of the help panel + could lead to a crash. + + New or improved functionality: + * The English documentation was proofread and corrected, + the service was kindly contributed by Richard Harris. + * Text file preview function was added. + * The initial working directory for the secondary file + panel is now set by a bookmark named DIR2. This + replaces the configuration parameter DIR2. + * The initial working directory for the primary file + panel can be now set by a bookmark named DIR1. + * New configuration parameter TIME_DATE controls the + display of date and time. + * Changes to the mouse control were made. + * The recommendation against using alt-R for normal file + renaming was dropped. + + +4.5 released on 24-SEP-2009 + + Problems fixed: + * Name completion could not complete user and group names + containing a dot, comma or a dash character. + + New or improved functionality: + * A mouse is supported on xterm-compatible terminals. + * The location of configuration files has been moved + again in order to comply with the XDG Specification. + The standard place for these files is from now on the + ~/.config/clex directory. Use the 'cfg-clex' utility to + move the files to the new location. + * There is a new option in the completion panel which + allows completion of a name which is a part of a longer + word. The option has a self-explaining description: + 'name to be completed starts at the cursor position'. + * Configuration parameter C_PANEL_SIZE (completion panel + size) cannot be set to AUTO (screen size) because this + size is often uncomfortably small. + * The Unicode soft hyphen character is displayed as a + control character. + * In the history panel a command separator is + automatically inserted into the input line when a + command is appended to the end of another command. + * Configuration parameters CMD_Fn accept a new control + sequence $~ which disables the 'Press enter to + continue' prompt. The control is returned to CLEX + immediately after the command execution terminates + provided that: + + * the command has not been modified; and + * the command terminates successfully (exit code + zero). + + * The $! control sequence can appear anywhere in a + configuration parameter CMD_Fn, not only at the end. + * New function: alt-Z places the current line to the + center of the panel. People using cluster-ssh might + find it useful. + + +4.4 released on 07-APR-2009 + + Problems fixed: + * In the help text there were few Unicode characters which + are now eliminated because they could not be displayed + properly in non-Unicode encodings. + + New or improved functionality: + * New function was added: change into a subdirectory + showing the contents in the other file panel (alt-X). + This function allows a return into the original + directory simply by switching panels (ctrl-X). + + +4.3 released on 29-MAR-2009 + + Problems fixed: + * A newly added bookmark did not appear on the screen + immediately. + * A misleading message 'Ignoring the DIR2 configuration + parameter' was logged when the 'DIR2' was set to + 'HOME'. + + New or improved functionality: + * The bookmark organizer has been merged with the regular + bookmark panel. + * Bookmarks can have descriptive names. + * The current working directory can be bookmarked from + the file panel (ctrl-D). + * The 'B_PANEL_SIZE' config parameter was removed. + + +4.2 released on 15-MAR-2009 + + Problems fixed: + * In some cases the 'cfg-clex' utility was generating an + unusable template for the copy command (F5). + * Under certain circumstances a crash was occurring on + exit when CLEX was used over a ssh connection. + + New or improved functionality: + * All configuration files now reside in the .clex + subdirectory. Use the 'cfg-clex' utility to move the + files to new location. + + +4.1 released on 09-FEB-2009 + + Problems fixed: + * Usage of uninitialized memory during the start-up has + been corrected. It caused a crash on the Apple Mac OS X + platform. Systems where CLEX starts normally are not + affected by this bug. + * A compilation problem on Apple Mac OS X was fixed. + * The xterm title change feature did not work on remote + telnet or ssh connections. + + New or improved functionality: + * If a directory comparison is restricted to regular + files, then only information about this type of file is + displayed in the summary. + * A small program named 'kbd-test' was added. It is a + utility for troubleshooting keyboard related problems. + + +CLEX 4.0 released on 22-DEC-2008 + + This is the initial release of the CLEX 4 branch. Main new + features are: + * Unicode support was implemented. + * Several configuration parameters have been converted to options + which are saved automatically. + * The log panel and optional logging to a file for auditing and + troubleshooting were added. + * There is now 'cfg-clex' utility. + * A built-in function for renaming files with invalid or + unprintable characters was added. + Enhancements (compared to previous releases) include: + * Configuring prompt, time format and date format is more flexible. + * The help is not limited to one link per line. + * The user interface of the directory compare function was redesigned. + * Changes in the pattern matching routine were made + * Panel filtering is now available in two more panels. + * A new tool for inserting control characters into the input line + was added. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..216321a --- /dev/null +++ b/Makefile.am @@ -0,0 +1,6 @@ +SUBDIRS = src +EXTRA_DIST = ChangeLog clex.spec.in clex.spec +DISTCLEANFILES = clex.spec + +README: README.md + cp README.md README diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..b574474 --- /dev/null +++ b/NEWS @@ -0,0 +1 @@ +NEWS: see ChangeLog diff --git a/README.md b/README.md new file mode 100644 index 0000000..99fbac3 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# CLEX + +## Project status + +The CLEX File Manager is an old software. +It exists as an open source project since January 2002. +The last major release is dated 2011. +No new features will be added, we only fix bugs. + +Many users are still using it and we appreciate that. +But if you came here looking for a file manager and do not +need a lightweight terminal based program, choose a modern +file manager instead. + +## Description + +CLEX is a file manager with a full-screen user interface written +in C with the curses library. + +It displays directory contents (including file status details) +and provides features like command history, filename insertion, +or name completion in order to help the user to construct commands +to be executed by the shell (there are no built-in commands). + +CLEX is easily configurable and all its features are explained +in the on-line help. + +## License + +CLEX is free software without warranty of any kind; see the +GNU General Public License as set out in the "COPYING" document +which accompanies the CLEX File Manager package. diff --git a/clex.spec.in b/clex.spec.in new file mode 100644 index 0000000..56fbe99 --- /dev/null +++ b/clex.spec.in @@ -0,0 +1,52 @@ +# +# CLEX File Manager RPM spec file +# +Summary: A free file manager with a full-screen user interface +Name: clex +Version: @VERSION@ +Release: @RPMRELEASE@%{?dist} +License: GPLv2+ +Group: Applications/File +Source: clex-@VERSION@.tar.gz +URL: https://github.com/xitop/clex +Requires: ncurses +BuildRequires: ncurses-devel +BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) +%changelog +# empty + +%description +CLEX (pronounced KLEKS) is a file manager with a full-screen user +interface. It displays directory contents including the file status +details and provides features like command history, filename insertion, +or name completion in order to help users to create commands to be +executed by the shell. + +CLEX is a versatile tool for system administrators and all users that +utilize the enormous power of the command line. Its unique one-panel +user interface enhances productivity and lessens the probability of +mistake. There are no built-in commands, CLEX is an add-on to your +favorite shell. + +%prep +%setup -q + +%build +./configure --bindir=/usr/bin --mandir=/usr/share/man --sysconfdir=/etc +make + +%install +rm -rf %{buildroot} +make install DESTDIR=%{buildroot} + +%files +%defattr(-,root,root,-) +%{_bindir}/clex +%{_bindir}/cfg-clex +%{_bindir}/kbd-test +%{_mandir}/man1/clex.1.gz +%{_mandir}/man1/cfg-clex.1.gz +%{_mandir}/man1/kbd-test.1.gz + +%clean +rm -rf %{buildroot} diff --git a/clex.sublime-project b/clex.sublime-project new file mode 100644 index 0000000..3c3499b --- /dev/null +++ b/clex.sublime-project @@ -0,0 +1,11 @@ +{ + "folders": + [ + { + "path": "src", + "name": "source code", + "file_exclude_patterns": ["*.o"], + "folder_exclude_patterns": [".deps"] + } + ] +} diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..674a2dd --- /dev/null +++ b/configure.ac @@ -0,0 +1,89 @@ +AC_PREREQ(2.61) +dnl numbering example: 4.6.test1 4.6.test2 4.6.3 4.6.patch4 4.6.patch5 +AC_INIT([CLEX File Manager],[4.7],[https://github.com/xitop/clex/issues],[clex]) +AC_SUBST([RPMRELEASE],1) + +AM_INIT_AUTOMAKE +AC_CONFIG_SRCDIR([src/clex.h]) +AC_CONFIG_HEADER([config.h]) + +# Checks for programs. +AC_PROG_CC +if test "$GCC" = "yes" ; then + CFLAGS="$CFLAGS -Wall -pedantic" + if gcc --help=warnings 2>&1 | grep -q 'Wstrict-overflow=' ; then + CFLAGS="$CFLAGS -Wstrict-overflow=0" + fi + if gcc --help=warnings 2>&1 | grep -q 'Wformat-overflow=' ; then + CFLAGS="$CFLAGS -Wformat-overflow=0" + fi +fi + +# Checks for libraries. +AC_SEARCH_LIBS([setupterm],[tinfo]) +AC_SEARCH_LIBS([addwstr],[ncursesw ncurses cursesw curses],[CURSESLIB="yes"],[CURSESLIB="no"]) +if test "$CURSESLIB" = "no" ; then + AC_MSG_ERROR([CLEX requires CURSES library with a wide character support]) +fi + +# Checks for header files. + +dnl glibc 2.25 deprecates 'major' and 'minor' in and requires to +dnl include . However the logic in AC_HEADER_MAJOR has not yet +dnl been updated in Autoconf 2.69 +if test "$GCC" = "yes" ; then + saved_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -Werror" + AC_HEADER_MAJOR + CFLAGS=$saved_CFLAGS +else + AC_HEADER_MAJOR +fi + +AC_CHECK_HEADERS([fcntl.h langinfo.h limits.h locale.h stdlib.h string.h termios.h unistd.h wchar.h wctype.h]) +if echo "$LIBS" | grep -e "-lncurses" > /dev/null ; then + dnl ncurses header file for ncurses library + for dir in /usr/include /opt/include /usr/local/include /opt/local/include ; do + for subdir in ncursesw ncurses ; do + if test -d "$dir/$subdir" ; then + CPPFLAGS="$CPPFLAGS -I$dir/$subdir" + fi + done + done + AC_CHECK_HEADERS([ncursesw.h ncurses.h]) +else + dnl curses header file for curses library + for dir in /usr/include /opt/include /usr/local/include /opt/local/include ; do + for subdir in cursesw curses ; do + if test -d "$dir/$subdir" ; then + CPPFLAGS="$CPPFLAGS -I$dir/$subdir" + fi + done + done + AC_CHECK_HEADERS([cursesw.h curses.h]) +fi +AC_CHECK_HEADERS([term.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_UID_T +AC_TYPE_MODE_T +AC_TYPE_OFF_T +AC_TYPE_PID_T +AC_TYPE_SIZE_T +AC_TYPE_SSIZE_T +AC_TYPE_SIGNAL +AC_CHECK_MEMBERS([struct stat.st_rdev]) +AC_DECL_SYS_SIGLIST +AC_FUNC_FNMATCH +AC_FUNC_FORK +AC_FUNC_STRCOLL + +# Checks for library functions. +AC_DEFINE([_GNU_SOURCE],[1],[required for strsignal]) +AC_CHECK_FUNCS([alarm btowc dup2 endgrent endpwent getcwd iswprint memset nl_langinfo setenv setlocale strchr strerror strsignal uname wcwidth]) + +# Checks for system services. +AC_SYS_LARGEFILE + +AC_CONFIG_FILES([Makefile clex.spec src/Makefile]) +AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..e5b0ad2 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,25 @@ +dist_man_MANS = clex.1 cfg-clex.1 kbd-test.1 +dist_bin_SCRIPTS = cfg-clex + +CLEANFILES = help.inc +EXTRA_DIST = help_en.hlp convert.sed +BUILT_SOURCES = help.inc +bin_PROGRAMS = clex kbd-test +clex_SOURCES = bookmarks.c bookmarks.h cfg.c cfg.h \ + clex.h clexheaders.h cmp.c cmp.h completion.c completion.h \ + control.c control.h directory.c directory.h edit.c edit.h \ + exec.c exec.h filepanel.c filepanel.h filter.c filter.h \ + filerw.c filerw.h help.c help.h history.c history.h inout.c inout.h \ + inschar.c inschar.h lang.c lang.h lex.c lex.h list.c list.h \ + log.c log.h notify.c notify.h opt.c opt.h match.c match.h \ + mbwstring.c mbwstring.h mouse.c mouse.h panel.c panel.h \ + preview.c preview.h rename.c rename.h \ + sdstring.c sdstring.h select.c select.h signals.c signals.h \ + sort.c sort.h start.c tty.c tty.h undo.c undo.h userdata.c userdata.h \ + ustring.c ustring.h ustringutil.c ustringutil.h util.c util.h \ + xterm_title.c xterm_title.h +kbd-test_SOURCES: kbd-test.c + +# convert the on-line help text to a C language array of strings +help.inc: help_en.hlp convert.sed + sed -f convert.sed help_en.hlp > help.inc diff --git a/src/bookmarks.c b/src/bookmarks.c new file mode 100644 index 0000000..690a37a --- /dev/null +++ b/src/bookmarks.c @@ -0,0 +1,540 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* log.h */ +#include /* fprintf() */ +#include /* free() */ +#include /* strerror() */ + +#include "bookmarks.h" + +#include "cfg.h" /* cfg_num() */ +#include "completion.h" /* compl_text() */ +#include "control.h" /* control_loop() */ +#include "edit.h" /* edit_setprompt() */ +#include "filepanel.h" /* changedir() */ +#include "filerw.h" /* fr_open() */ +#include "inout.h" /* win_panel() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr_set() */ +#include "mbwstring.h" /* convert2w() */ +#include "panel.h" /* pan_adjust() */ +#include "util.h" /* emalloc() */ + +#define BM_SIZE 100 /* size of bookmark table */ +#define BM_MAXMEM (200 * BM_SIZE) /* memory limit for the bookmark file */ + +static BOOKMARK *bmlist[BM_SIZE]; /* list of bookmarks */ +static BOOKMARK *filtbm[BM_SIZE]; /* filtered list of bookmarks */ +static int bm_cnt = 0; /* number of bookmarks */ +static time_t bm_file_mod = 0; /* last modification of the file */ +/* there is a small risk of data incoherence if the file is modified by + two running CLEX programs within the same second. Users normally + don't do such things */ +static FLAG changed = 0; /* nonzero if unsaved changes exist */ +static const char *append = 0; /* add this directory when entering the panel */ + +static time_t +mod_time(const char *file) +{ + struct stat stbuf; + + return stat(file,&stbuf) < 0 ? 0 : stbuf.st_mtime; +} + +const BOOKMARK * +get_bookmark(const wchar_t *name) +{ + int i; + + for (i = 0; i < bm_cnt; i++) + if (wcscmp(SDSTR(bmlist[i]->name),name) == 0) { + if (*(USTR(bmlist[i]->dir)) != '/') { + msgout(MSG_NOTICE,"Ignoring the %ls bookmark, because it is not " + "an absolute pathname starting with /", name); + return 0; + } + return bmlist[i]; + } + return 0; +} + +static int +bm_save_main(void) +{ + int i; + wchar_t *name; + FILE *fp; + + if ( (fp = fw_open(user_data.file_bm)) ) { + fprintf(fp,"#\n# CLEX bookmarks file\n#\n"); + for (i = 0; i < bm_cnt; i++) { + name = SDSTR(bmlist[i]->name); + if (*name != L'\0') + fprintf(fp,"*%s\n",convert2mb(name)); + fprintf(fp,"%s\n",USTR(bmlist[i]->dir)); + } + } + if (fw_close(fp)) + return -1; + + changed = 0; + bm_file_mod = mod_time(user_data.file_bm); + return 0; +} + +static void +bm_save(void) +{ + if (!changed) + return; + + if (user_data.nowrite) + msgout(MSG_W,"BOOKMARKS: Saving data to disk is prohibited"); + else if (bm_save_main() < 0) + msgout(MSG_W,"BOOKMARKS: Could not save data, details in log"); + else + msgout(MSG_I,"BOOKMARKS: Data saved"); +} + +void +cx_bm_save(void) +{ + bm_save(); + next_mode = MODE_SPECIAL_RETURN; +} + +static int +is_full(void) +{ + if (bm_cnt >= BM_SIZE) { + msgout(MSG_W,"Bookmark list is full"); + return 1; + } + return 0; +} + +static void +bm_reset(int i) +{ + sdw_reset(&bmlist[i]->name); + us_reset( &bmlist[i]->dir); + usw_reset(&bmlist[i]->dirw); +} + +static void +bm_reset_all(void) +{ + int i; + + bm_cnt = 0; + for (i = 0; i < BM_SIZE; i++) + bm_reset(i); +} + +/* + * return value: + * 0 = bookmarks loaded without problems + * -1 = failure, no or incomplete bookmarks loaded + */ +static int +bm_read_main(void) +{ + int i, tfd, split; + const char *line; + FLAG corrupted, name; + + tfd = fr_open(user_data.file_bm,BM_MAXMEM); + if (tfd == FR_NOFILE) { + bm_reset_all(); + changed = 0; + return 0; /* missing optional file is OK */ + } + if (tfd < 0) + return -1; + msgout(MSG_DEBUG,"BOOKMARKS: Processing bookmark file \"%s\"",user_data.file_bm); + + split = fr_split(tfd,BM_SIZE * 2 /* name + dir = 2 lines */ ); + if (split < 0 && split != FR_LINELIMIT) { + fr_close(tfd); + return -1; + } + + bm_reset_all(); + for (corrupted = name = 0, i = 0; (line = fr_line(tfd,i)); i++) { + if (*line == '/') { + if (is_full()) { + split = FR_LINELIMIT; + break; + } + us_copy(&bmlist[bm_cnt]->dir,line); + usw_convert2w(line,&bmlist[bm_cnt]->dirw); + name = 0; + bm_cnt++; + } + else if (*line == '*') { + if (TSET(name)) + corrupted = 1; + sdw_copy(&bmlist[bm_cnt]->name,convert2w(line + 1)); + } + else + corrupted = 1; + } + fr_close(tfd); + + if (corrupted) + msgout(MSG_NOTICE,"Invalid contents, file is corrupted"); + changed = 0; + return (split < 0 || corrupted) ? -1 : 0; +} + +/* + * bm_read() reads the bookmark file if it was modified or the 'force' flag is set + * return value: 0 file not read, -1 = read error, 1 = read success + */ +static int +bm_read(int force) +{ + time_t mod; + + mod = mod_time(user_data.file_bm); + if (mod == bm_file_mod && !force) + /* file unchanged */ + return 0; + + if (bm_read_main() == 0) { + bm_file_mod = mod; + return 1; + } + + if (!user_data.nowrite) { + /* automatic recovery */ + msgout(MSG_NOTICE,"Attempting to overwrite the invalid bookmark file"); + msgout(MSG_NOTICE,bm_save_main() < 0 ? "Attempt failed" : "Attempt succeeded"); + } + + msgout(MSG_W,"BOOKMARKS: An error occurred while reading data, details in log"); + return -1; +} + +void +bm_panel_data(void) +{ + int i, j; + BOOKMARK *curs; + + curs = VALID_CURSOR(panel_bm.pd) ? panel_bm.bm[panel_bm.pd->curs] : 0; + + if (panel_bm.pd->filtering) { + match_substr_set(panel_bm.pd->filter->line); + for (i = j = 0; i < bm_cnt; i++) { + if (bmlist[i] == curs) + panel_bm.pd->curs = j; + if (!match_substr(USTR(bmlist[i]->dirw)) && !match_substr_ic(SDSTR(bmlist[i]->name))) + continue; + filtbm[j++] = bmlist[i]; + } + panel_bm.bm = filtbm; + panel_bm.pd->cnt = j; + } + else { + if (curs) + for (i = 0; i < bm_cnt; i++) + if (bmlist[i] == curs) { + panel_bm.pd->curs = i; + break; + } + panel_bm.bm = bmlist; + panel_bm.pd->cnt = bm_cnt; + } +} + +void +cx_bm_revert(void) +{ + if (changed && bm_read(1) > 0) + msgout(MSG_i,"original bookmarks restored"); + + next_mode = MODE_SPECIAL_RETURN; +} + +void +bm_initialize(void) +{ + static BOOKMARK storage[BM_SIZE]; + int i; + + for (i = 0; i < BM_SIZE; i++) + bmlist[i] = storage + i; + panel_bm.bm = bmlist; + + bm_read(1); +} + +static void +set_field_width(void) +{ + int i, len, cw; + + cw = 0; + for (i = 0; i < bm_cnt; i++) { + len = wc_cols(SDSTR(bmlist[i]->name),0,-1); + if (len > cw) { + if (len > disp_data.pancols / 3) { + cw = disp_data.pancols / 3; + break; + } + cw = len; + } + } + + panel_bm.cw_name = cw; +} + +int +bm_prepare(void) +{ + int i; + const char *dir; + + if (bm_read(0) > 0) { + msgout(MSG_i,"New version of the bookmarks was loaded"); + panel_bm.pd->curs = panel_bm.pd->min; + } + set_field_width(); + + if (append) { + dir = append; + append = 0; + + for (i = 0; i < bm_cnt; i++) + if (strcmp(USTR(bmlist[i]->dir),dir) == 0) { + msgout(MSG_i,"Already bookmarked"); + return -1; + } + + if (is_full()) + return -1; + + sdw_reset(&bmlist[bm_cnt]->name); + us_copy(&bmlist[bm_cnt]->dir,dir); + usw_convert2w(dir,&bmlist[bm_cnt]->dirw); + panel_bm.pd->curs = bm_cnt++; + changed = 1; + } + + if (panel_bm.pd->curs < 0) + panel_bm.pd->curs = panel_bm.pd->min; + panel_bm.pd->cnt = bm_cnt; + + panel_bm.pd->filtering = 0; + panel_bm.bm = bmlist; + + panel = panel_bm.pd; + textline = 0; + return 0; +} + +void +cx_bm_chdir(void) +{ + bm_save(); + if (changedir(USTR(bmlist[panel_bm.pd->curs]->dir)) == 0) + next_mode = MODE_SPECIAL_RETURN; +} + +static void +bm_rotate(int from, int to) +{ + BOOKMARK *b; + int i; + + if (from < to) { + b = bmlist[from]; + for (i = from; i < to; i++) + bmlist[i] = bmlist[i + 1]; + bmlist[i] = b; + } + else if (from > to) { + b = bmlist[from]; + for (i = from; i > to; i--) + bmlist[i] = bmlist[i - 1]; + bmlist[i] = b; + } + changed = 1; +} + +void +cx_bm_up(void) +{ + int pos; + + if ((pos = panel_bm.pd->curs - 1) < 0) + return; + + bm_rotate(pos + 1, pos); + + panel_bm.pd->curs = pos; /* curs-- */ + LIMIT_MAX(panel_bm.pd->top,pos); + win_panel(); +} + +void +cx_bm_down(void) +{ + int pos; + + if ((pos = panel_bm.pd->curs + 1) == bm_cnt) + return; + + bm_rotate(pos - 1,pos); + + panel_bm.pd->curs = pos; /* curs++ */ + LIMIT_MIN(panel_bm.pd->top,pos - disp_data.panlines + 1); + win_panel(); +} + +void +cx_bm_del(void) +{ + bm_reset(panel_bm.pd->curs); + bm_rotate(panel_bm.pd->curs,bm_cnt--); + + panel_bm.pd->cnt = bm_cnt; + if (panel_bm.pd->curs == bm_cnt) { + panel_bm.pd->curs--; + pan_adjust(panel_bm.pd); + } + set_field_width(); + win_panel(); +} + +/* * * edit modes * * */ + +int +bm_edit0_prepare(void) +{ + panel_bm_edit.pd->top = panel_bm_edit.pd->min; + panel_bm_edit.pd->curs = 0; + panel = panel_bm_edit.pd; + textline = 0; + return 0; +} + +int +bm_edit1_prepare(void) +{ + /* panel = panel_bm_edit.pd; */ + textline = &line_tmp; + edit_setprompt(textline,L"Bookmark name: "); + edit_nu_putstr(SDSTR(panel_bm_edit.bm->name)); + win_panel_opt(); + return 0; +} + +int +bm_edit2_prepare(void) +{ + const wchar_t *dirw; + + /* panel = panel_bm_edit.pd; */ + textline = &line_tmp; + edit_setprompt(textline,L"Bookmark directory: "); + dirw = USTR(panel_bm_edit.bm->dirw); + edit_nu_putstr(dirw ? dirw : L"/"); + win_panel_opt(); + return 0; +} + +void +cx_bm_edit(void) +{ + panel_bm_edit.bm = bmlist[panel_bm.pd->curs]; /* current bookmark */ + control_loop(MODE_BM_EDIT0); + set_field_width(); + win_panel(); +} + +void +cx_bm_new(void) +{ + if (is_full()) + return; + + panel_bm_edit.bm = bmlist[bm_cnt]; + bm_reset(bm_cnt); + control_loop(MODE_BM_EDIT0); + + if (USTR(panel_bm_edit.bm->dir) == 0) + /* dir is still null -> operation was canceled with ctrl-C */ + return; + + LIMIT_MIN(panel_bm.pd->curs,-1); + bm_rotate(bm_cnt,++panel_bm.pd->curs); + panel_bm.pd->cnt = ++bm_cnt; + set_field_width(); + + pan_adjust(panel_bm.pd); + win_panel(); +} + +/* note: called from the file panel or the main menu */ +void +cx_bm_addcwd(void) +{ + append = USTR(ppanel_file->dir); + control_loop(MODE_BM); +} + +void +cx_bm_edit0_enter(void) +{ + control_loop(panel_bm_edit.pd->curs ? MODE_BM_EDIT2 : MODE_BM_EDIT1); +} + +void +cx_bm_edit1_enter(void) +{ + sdw_copy(&panel_bm_edit.bm->name,USTR(textline->line)); + changed = 1; + win_panel_opt(); + next_mode = MODE_SPECIAL_RETURN; +} + +void +cx_bm_edit2_enter(void) +{ + const wchar_t *dirw; + + dirw = USTR(textline->line); + if (*dirw != L'/') { + msgout(MSG_w,"Directory name must start with a slash /"); + return; + } + usw_copy(&panel_bm_edit.bm->dirw,dirw); + us_convert2mb(dirw,&panel_bm_edit.bm->dir); + changed = 1; + win_panel_opt(); + next_mode = MODE_SPECIAL_RETURN; +} + +void +cx_bm_edit2_compl(void) +{ + if (compl_text(COMPL_TYPE_DIRPANEL) < 0) + msgout(MSG_i,"COMPLETION: please type at least the first character"); +} diff --git a/src/bookmarks.h b/src/bookmarks.h new file mode 100644 index 0000000..cb50fc0 --- /dev/null +++ b/src/bookmarks.h @@ -0,0 +1,21 @@ +extern void bm_initialize(void); +extern int bm_prepare(void); +extern int bm_edit0_prepare(void); +extern int bm_edit1_prepare(void); +extern int bm_edit2_prepare(void); +extern void bm_panel_data(void); +extern const BOOKMARK *get_bookmark(const wchar_t *); + +extern void cx_bm_save(void); +extern void cx_bm_revert(void); +extern void cx_bm_chdir(void); +extern void cx_bm_edit(void); +extern void cx_bm_up(void); +extern void cx_bm_down(void); +extern void cx_bm_del(void); +extern void cx_bm_new(void); +extern void cx_bm_addcwd(void); +extern void cx_bm_edit0_enter(void); +extern void cx_bm_edit1_enter(void); +extern void cx_bm_edit2_enter(void); +extern void cx_bm_edit2_compl(void); diff --git a/src/cfg-clex b/src/cfg-clex new file mode 100755 index 0000000..440ea05 --- /dev/null +++ b/src/cfg-clex @@ -0,0 +1,269 @@ +#!/bin/sh + +echo 'CLEX configuration utility' +echo '==========================' +echo + +if [ x"`echo ~`" = x ] ; then + echo 'Error: no home directory' + exit 1 +fi + +if [ x"$XDG_CONFIG_HOME" != x ] ; then + clex_dir="$XDG_CONFIG_HOME/clex" +else + clex_dir=~/.config/clex +fi +clex_opt="$clex_dir/options" +clex_cfg="$clex_dir/config" +clex_bmk="$clex_dir/bookmarks" + +if [ -f "$clex_cfg" ] ; then + echo "CLEX configuration file does exist. Please use the CLEX's" + echo 'configuration panel to make changes.' + echo + echo 'If you want to create a new configuration from scratch, delete' + echo "the file $clex_cfg and then run `basename $0` again." + exit 1 +fi + +if [ ! -d "$clex_dir" ] ; then + if ! mkdir -p "$clex_dir" ; then + echo "Error: could not create the $clex_dir directory" + exit 1 + fi +fi + +restart_msg() { + if [ x"$CLEX" != x ] ; then + echo "Please restart CLEX" + fi +} + +# move files from 4.2, 4.3 or 4.4 ================== +old_dir=~/.clex +old_cfg="$old_dir/config" +old_opt="$old_dir/options" +old_bmk="$old_dir/bookmarks" + +if [ -f "$old_cfg" ] ; then + echo 'moving the configuration file to new location' + mv "$old_cfg" "$clex_cfg" +fi +if [ -f "$old_opt" ] ; then + echo 'moving the options file to new location' + mv "$old_opt" "$clex_opt" +fi +if [ -f "$old_bmk" ] ; then + echo 'moving the bookmarks file to new location' + mv "$old_bmk" "$clex_bmk" +fi +rmdir "$old_dir" 2>/dev/null +if [ -f "$clex_cfg" ] ; then + restart_msg + exit 0 +fi + +# move files from 4.0 or 4.1 ======================= +old_cfg=~/.clexcfg +old_opt=~/.clexopt +old_bmk=~/.clexbm + +if [ -f "$old_cfg" ] ; then + echo 'moving the configuration file to new location' + mv "$old_cfg" "$clex_cfg" +fi +if [ -f "$old_opt" ] ; then + echo 'moving the options file to new location' + mv "$old_opt" "$clex_opt" +fi +if [ -f "$old_bmk" ] ; then + echo 'moving the bookmarks file to new location' + mv "$old_bmk" "$clex_bmk" +fi +if [ -f "$clex_cfg" ] ; then + restart_msg + exit 0 +fi + +# convert files from 3.X if any ================== + +# list of CLEX4 variables to be converted from CLEX3 +VARIABLE_LIST='C_PANEL_SIZE D_PANEL_SIZE H_PANEL_SIZE + CMD_F3 CMD_F4 CMD_F5 CMD_F6 CMD_F7 CMD_F8 CMD_F9 CMD_F10 CMD_F11 CMD_F12 + TIME_FMT DATE_FMT LAYOUT_ACTIVE LAYOUT1 LAYOUT2 LAYOUT3 + CMD_LINES DIR2 FRAME KILOBYTE PROMPT QUOTE XTERM_TITLE' + +confirmation() +{ + echo -n "$1 (y/N) ? " + read answer junk + if [ x"$answer" != x'y' -a x"$answer" != x'Y' ] ; then + echo "Exiting" + exit 1 + fi + echo +} + +reset_config() { + local var + + for var in $VARIABLE_LIST ; do + unset CLEX_$var + done +} + +read_config() { + local line var val + + while read line ; do + if ! echo $line | LC_ALL=C grep -q '^[A-Z][A-Z0-9_]*=.*$' ; then + continue + fi + var=`echo $line | sed -e 's/=.*$//'` + val=`echo $line | LC_ALL=C sed -e 's/^[A-Z0-9_]*=//'` + eval CLEX_$var='"$val"' + done < "$1" +} + +convert_config() { + echo 'Converting data from version 3 to version 4' + if [ x"$CLEX_FMT_DATE" != x ] ; then + CLEX_DATE_FMT=`echo $CLEX_FMT_DATE | sed -e 's/d/%d/;s/D/%e/;s/m/%m/;s/M/%b/;s/y/%y/;s/Y/%Y/'` + fi + if [ x"$CLEX_FMT_TIME" = x'1' ] ; then + CLEX_TIME_FMT='%I:%M%p' + elif [ x"$CLEX_FMT_TIME" = x'2' ] ; then + CLEX_TIME_FMT='%H:%M' + fi + if [ x"$CLEX_ACTIVE_LAYOUT" != x ] ; then + CLEX_LAYOUT_ACTIVE=`expr $CLEX_ACTIVE_LAYOUT + 1` + fi + if [ x"$CLEX_XTERM_TITLE" == x'2' ] ; then + CLEX_XTERM_TITLE='1' + fi + if [ x"$CLEX_LAYOUT1" != x ] ; then + CLEX_LAYOUT1=`echo $CLEX_LAYOUT1 | sed -e 's/| /|/'` + fi + if [ x"$CLEX_LAYOUT2" != x ] ; then + CLEX_LAYOUT2=`echo $CLEX_LAYOUT2 | sed -e 's/| /|/'` + fi + if [ x"$CLEX_LAYOUT3" != x ] ; then + CLEX_LAYOUT3=`echo $CLEX_LAYOUT3 | sed -e 's/| /|/'` + fi +} + +detect_programs() { + local opt var val prog arg + + unset PROG3 PROG4 PROG5 PROG6 PROG7 PROG8 PROG9 + ARG3='$f' + ARG4='$f' + if cp -- "$0" /dev/null >/dev/null 2>&1 ; then + echo "End of options mark '--' is supported" + opt=' --' + PROG3='more' + PROG4='vi' + PROG5='cp -ir' ; ARG5='$f $2' + PROG6='mv -i' ; ARG6='$f $2' + PROG7='mkdir' ; ARG7='' + PROG8='rm' ; ARG8='$f' + PROG9='lpr' ; ARG9='$f' + else + echo "End of options mark '--' is NOT supported" + opt='' + fi + + if [ x"$CLEX_CMD_F3" = x ] ; then + if [ x"$PAGER" != x ] ; then + PROG3="$PAGER" + elif less -V >/dev/null 2>&1; then + PROG3='less' + fi + fi + echo "Pager program: $PROG3" + + if [ x"$CLEX_CMD_F4" = x ] ; then + if [ x"$EDITOR" != x ] ; then + PROG4="$EDITOR" + elif vim --version >/dev/null 2>&1; then + PROG4='vim' + fi + fi + echo "Text editor: $PROG4" + + for cmd in 3 4 5 6 7 8 9 ; do + var=CLEX_CMD_F$cmd + eval val="\$$var" + if [ x"$val" != x ] ; then + continue + fi + eval prog="\$PROG$cmd" + if [ x"$prog" = x ] ; then + continue + fi + eval arg="\$ARG$cmd" + eval $var='"$prog$opt $arg"' + done +} + +print_config() { + local var val + + for var in $VARIABLE_LIST ; do + eval val="\$CLEX_$var" + if [ x"$val" != x ] ; then + echo "$var=$val" + fi + done +} + +reset_config + +cfg3usr=~/.clexrc +cfg3sys=/etc/clexrc +cfg3alt=/usr/local/etc/clexrc +clex3files='' +for file in "$cfg3sys" "$cfg3alt" ; do + if [ -f "$file" ] ; then + echo 'System-wide configuration file from previous version 3 found' + read_config "$file" + clex3files="$clex3files $file" + fi +done +if [ -f "$cfg3usr" ] ; then + echo 'Personal configuration file from previous version 3 found' + read_config "$cfg3usr" + clex3files="$clex3files $cfg3usr" +fi +if [ x"$clex3files" != x ] ; then + convert_config +fi + +# build and save the configuration ============== + +detect_programs +echo +echo '=== BEGIN ==================' +print_config +echo '=== END ====================' +echo + +confirmation 'Save this configuration' +{ + echo '#' + echo '# CLEX configuration file' + echo '#' + print_config +} > "$clex_cfg" +if [ "$?" -ne 0 ] ; then + echo 'Error saving configuration' + exit 1 +fi +echo "Configuration saved" + +if [ x"$clex3files" != x ] ; then + echo "You might now want to delete the old configuration file(s):$clex3files" +fi +restart_msg +exit 0 diff --git a/src/cfg-clex.1 b/src/cfg-clex.1 new file mode 100644 index 0000000..a7e8dbf --- /dev/null +++ b/src/cfg-clex.1 @@ -0,0 +1,29 @@ +.TH CFG-CLEX 1 +.SH "NAME" +cfg-clex \- CLEX configuration utility +.SH "SYNOPSIS" +.B cfg-clex +.SH "DESCRIPTION" +.B cfg-clex +builds the initial configuration file for the CLEX file manager or +migrates existing files from an older version. +.PP +Once the configuration file is created, all further changes should +be made using the CLEX's configuration panel. +.SH "ENVIRONMENT" +.B cfg-clex +makes use of the following environment variables: +.TP +.I PAGER +Preferred pager for the view command. If not set, +.I less +is chosen if available, otherwise +.IR more . +.TP +.I EDITOR +Preferred editor for the edit command. If not set, +.I vim +is chosen if available, otherwise +.IR vi . +.SH "SEE ALSO" +.IR clex (1) diff --git a/src/cfg.c b/src/cfg.c new file mode 100644 index 0000000..2d4168b --- /dev/null +++ b/src/cfg.c @@ -0,0 +1,734 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* mkdir() */ +#include /* va_list */ +#include /* printf() */ +#include /* strchr() */ + +#include "cfg.h" + +#include "completion.h" /* compl_reconfig() */ +#include "control.h" /* control_loop() */ +#include "directory.h" /* dir_reconfig() */ +#include "edit.h" /* edit_setprompt() */ +#include "exec.h" /* set_shellprompt() */ +#include "filerw.h" /* fr_open() */ +#include "history.h" /* hist_reconfig() */ +#include "inout.h" /* win_panel_opt() */ +#include "list.h" /* kb_reconfig() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2w() */ +#include "mouse.h" /* mouse_reconfig() */ +#include "panel.h" /* cx_pan_home() */ +#include "xterm_title.h" /* xterm_title_reconfig() */ + +#define CFG_FILESIZE_LIMIT 2000 /* config file size limit (in bytes) */ +#define CFG_LINES_LIMIT 100 /* config file size limit (in lines) */ +#define CFG_ERRORS_LIMIT 10 /* error limit */ + +static int error_cnt; /* error counter */ + +typedef struct { + enum CFG_TYPE code; /* CFG_XXX */ + const wchar_t *extra_val; /* if defined - name of the special value (represented as 0) */ + int min, initial, max; /* allowed range and the default value */ + const wchar_t *desc[4]; /* if defined - show this text instead + of numbers (enumerated type) */ + int current, new; /* values */ +} CNUM; + +static CNUM table_numeric[] = { + /* enumerated */ + { CFG_CMD_LINES, 0, + 2, 2, MAX_CMDLINES, + { L"2 screen lines", + L"3 screen lines", + L"4 screen lines", + L"5 screen lines" } }, + { CFG_FRAME, 0, + 0, 0, 2, + { L"--------", + L"========", + L"line graphics (not supported on some terminals)" } }, + { CFG_KILOBYTE, 0, + 0, 0, 1, + { L"1 KiB is 1024 bytes (IEC standard)", + L"1 KB is 1000 bytes (SI standard)" } }, + { CFG_LAYOUT, 0, + 1, 1, 3, + { L"Layout #1", + L"Layout #2", + L"Layout #3" } }, + { CFG_MOUSE, 0, + 0, 1, 2, + { L"Disabled", + L"Enabled, right-handed", + L"Enabled, left-handed" } }, + { CFG_TIME_DATE, 0, + 0, 0, 2, + { L"Short format: time or date", + L"Long format: time and date", + L"Long format: date and time" } }, + { CFG_XTERM_TITLE, 0, + 0, 1, 1, + { L"Disabled", + L"Enabled" } }, + /* really numeric */ + { CFG_C_SIZE, 0, 10, 120, 200 }, + { CFG_D_SIZE, L"AUTO", 10, 0, 200 }, + { CFG_H_SIZE, 0, 10, 60, 200 }, + { CFG_MOUSE_SCROLL, 0, 1, 3, 8 }, + { CFG_DOUBLE_CLICK, 0, 200, 400, 800 } +}; + +typedef struct { + enum CFG_TYPE code; /* CFG_XXX */ + const wchar_t *extra_val; /* if defined - name of special value (represented as L"") */ + wchar_t *initial; + wchar_t current[CFGVALUE_LEN + 1], new[CFGVALUE_LEN + 1]; +} CSTR; + +static CSTR table_string[] = { + { CFG_CMD_F3, 0, L"more $f" }, + { CFG_CMD_F4, 0, L"vi $f" }, + { CFG_CMD_F5, 0, L"cp -ir $f $2" }, + { CFG_CMD_F6, 0, L"mv -i $f $2" }, + { CFG_CMD_F7, 0, L"mkdir " }, + { CFG_CMD_F8, 0, L"rm $f" }, + { CFG_CMD_F9, 0, L"lpr $f" }, + { CFG_CMD_F10, 0, L"" }, + { CFG_CMD_F11, 0, L"" }, + { CFG_CMD_F12, 0, L"" }, + { CFG_FMT_TIME, L"AUTO", L"" }, + { CFG_FMT_DATE, L"AUTO", L"" }, + { CFG_LAYOUT1, 0, L"$d $S $>$t $M $*|$p $o $L" }, + { CFG_LAYOUT2, 0, L"$d $R $t $*|$p $o", }, + { CFG_LAYOUT3, 0, L"$p $o $s $d $>$t $*|mode=$m atime=$a ctime=$i links=$l" }, + { CFG_PROMPT, 0, L"$s $p " }, + { CFG_QUOTE, 0, L"" } +}; + +/* everything in one place and alphabetically sorted for easy editing */ +static struct { + CODE code; /* internal code */ + const char *name; /* name in cfg file - may not exceed max length of CFGVAR_LEN */ + const wchar_t *help; /* help text - should fit on minimal width screen */ +} table_desc[CFG_TOTAL_] = { + { CFG_C_SIZE, "C_PANEL_SIZE", + L"Advanced: Completion panel size" }, + { CFG_CMD_F3, "CMD_F3", + L"Command F3 = view file(s)" }, + { CFG_CMD_F4, "CMD_F4", + L"Command F4 = edit file(s)" }, + { CFG_CMD_F5, "CMD_F5", + L"Command F5 = copy file(s)" }, + { CFG_CMD_F6, "CMD_F6", + L"Command F6 = move file(s)" }, + { CFG_CMD_F7, "CMD_F7", + L"Command F7 = make directory" }, + { CFG_CMD_F8, "CMD_F8", + L"Command F8 = remove file(s)" }, + { CFG_CMD_F9, "CMD_F9", + L"Command F9 = print file(s)" }, + { CFG_CMD_F10, "CMD_F10", + L"Command F10 = user defined" }, + { CFG_CMD_F11, "CMD_F11", + L"Command F11 = user defined" }, + { CFG_CMD_F12, "CMD_F12", + L"Command F12 = user defined" }, + { CFG_CMD_LINES, "CMD_LINES", + L"Appearance: How many lines are occupied by the input line" }, + { CFG_D_SIZE, "D_PANEL_SIZE", + L"Advanced: Directory panel size (AUTO = screen size)" }, + { CFG_DOUBLE_CLICK, "DOUBLE_CLICK", + L"Mouse double click interval in milliseconds" }, + { CFG_FRAME, "FRAME", + L"Appearance: Panel frame: ----- or ===== or line graphics" }, + { CFG_FMT_TIME, "TIME_FMT", + L"Appearance: Time format string (e.g. %H:%M) or AUTO" }, + { CFG_FMT_DATE, "DATE_FMT", + L"Appearance: Date format string (e.g. %Y-%m-%d) or AUTO" }, + { CFG_H_SIZE, "H_PANEL_SIZE", + L"Advanced: History panel size" }, + { CFG_KILOBYTE, "KILOBYTE", + L"Appearance: Filesize unit definition" }, + { CFG_LAYOUT, "LAYOUT_ACTIVE", + L"Appearance: Which file panel layout is active" }, + { CFG_LAYOUT1, "LAYOUT1", + L"Appearance: File panel layout #1, see help" }, + { CFG_LAYOUT2, "LAYOUT2", + L"Appearance: File panel layout #2" }, + { CFG_LAYOUT3, "LAYOUT3", + L"Appearance: File panel layout #3" }, + { CFG_MOUSE, "MOUSE", + L"Mouse input (supported terminals only)" }, + { CFG_MOUSE_SCROLL, "MOUSE_SCROLL", + L"Mouse wheel scrolls by this number of lines" }, + { CFG_PROMPT, "PROMPT", + L"Appearance: Command line prompt, see help" }, + { CFG_QUOTE, "QUOTE", + L"Advanced: Additional filename chars to be quoted, see help" }, + { CFG_TIME_DATE, "TIME_DATE", + L"Appearance: Time and date display mode" }, + { CFG_XTERM_TITLE, "XTERM_TITLE", + L"Appearance: Change the X terminal window title" } +}; + +static CFG_ENTRY config[CFG_TOTAL_]; + +/* 'copy' values CP_X2Y understood by set_value() */ +#define CP_SRC_CURRENT 1 +#define CP_SRC_INITIAL 2 +#define CP_SRC_NEW 4 +#define CP_DST_CURRENT 8 +#define CP_DST_NEW 16 +#define CP_N2C (CP_SRC_NEW | CP_DST_CURRENT) +#define CP_C2N (CP_SRC_CURRENT | CP_DST_NEW) +#define CP_I2N (CP_SRC_INITIAL | CP_DST_NEW) +#define CP_I2C (CP_SRC_INITIAL | CP_DST_CURRENT) + +static void +set_value(int code, int cp) +{ + int src_num, *dst_num; + const wchar_t *src_str; + wchar_t *dst_str; + CNUM *pnum; + CSTR *pstr; + + if (config[code].isnum) { + pnum = config[code].table; + if (cp & CP_SRC_CURRENT) + src_num = pnum->current; + else if (cp & CP_SRC_INITIAL) + src_num = pnum->initial; + else + src_num = pnum->new; + if (cp & CP_DST_CURRENT) + dst_num = &pnum->current; + else + dst_num = &pnum->new; + *dst_num = src_num; + } + else { + pstr = config[code].table; + if (cp & CP_SRC_CURRENT) + src_str = pstr->current; + else if (cp & CP_SRC_INITIAL) + src_str = pstr->initial; + else + src_str = pstr->new; + if (cp & CP_DST_CURRENT) + dst_str = pstr->current; + else + dst_str = pstr->new; + wcscpy(dst_str,src_str); + } +} + +static CFG_ENTRY * +get_variable_by_name(const char *var, int len) +{ + int i; + + for (i = 0; i < CFG_TOTAL_; i++) + if (strncmp(table_desc[i].name,var,len) == 0) + return config + table_desc[i].code; + return 0; +} + +static CNUM * +get_numeric(int code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(table_numeric); i++) + if (table_numeric[i].code == code) + return table_numeric + i; + return 0; +} + +static CSTR * +get_string(int code) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(table_string); i++) + if (table_string[i].code == code) + return table_string + i; + return 0; +} + +static int +get_desc(int code) +{ + int i; + + for (i = 0; i < CFG_TOTAL_; i++) + if (table_desc[i].code == code) + return i; + return -1; +} + +static void +parse_error(const char *format, ...) +{ + va_list argptr; + + error_cnt++; + va_start(argptr,format); + vmsgout(MSG_NOTICE,format,argptr); + va_end(argptr); + +} + +static int +cfgfile_read(void) +{ + int i, len, nvalue, tfd, split; + const char *line, *value; + const wchar_t *wvalue; + CFG_ENTRY *pce; + CNUM *pnum; + + tfd = fr_open(user_data.file_cfg,CFG_FILESIZE_LIMIT); + if (tfd == FR_NOFILE) { + if (!user_data.nowrite) { + mkdir(user_data.subdir,0755); /* might exist already */ + msgout(MSG_w,"Configuration file not found.\n" + "It is recommended to run the \"cfg-clex\" utility."); + user_data.noconfig = 1; /* cfg-clex will appear on the command line */ + } + return 0; /* missing optional file is ok */ + } + if (tfd < 0) + return -1; + msgout(MSG_DEBUG,"CONFIG: Processing configuration file \"%s\"",user_data.file_cfg); + + split = fr_split(tfd,CFG_LINES_LIMIT); + if (split < 0 && split != FR_LINELIMIT) { + fr_close(tfd); + return -1; + } + + error_cnt = 0; + for (i = 0; (line = fr_line(tfd,i)); i++) { + /* split VARIABLE and VALUE */ + if ( (value = strchr(line,'=')) == 0) { + parse_error("Syntax error (expected was \"VARIABLE=value\") in \"%s\"",line); + continue; + } + + pce = get_variable_by_name(line,value - line); + if (pce == 0) { + /* --- begin 4.6 transition --- */ + if (strncmp(line,"DIR2=",5) == 0) { + msgout(MSG_w, + "NOTE: DIR2 is no longer a valid configuration parameter.\n" + "If you want to use \"%s\" as the secondary panel's initial directory:\n" + " * please create a bookmark named DIR2 with this value\n" + " * save your configuration to purge DIR2 from the configuration file\n", value + 1); + continue; + } + /* --- end --- */ + parse_error("Unknown variable in \"%s\"",line); + continue; + } + value++; + + if (pce->isnum) { + pnum = pce->table; + if (sscanf(value," %d %n",&nvalue,&len) < 1 || len != strlen(value)) + parse_error("Invalid number in \"%s\"",line); + else if ((nvalue < pnum->min || nvalue > pnum->max) + && (nvalue != 0 || pnum->extra_val == 0)) + parse_error("Numeric value out of range in \"%s\"",line); + else + pnum->current = nvalue; + } else { + wvalue = convert2w(value); + if (wcslen(wvalue) > CFGVALUE_LEN) + parse_error("String value too long in \"%s\"",line); + else + wcscpy(((CSTR *)pce->table)->current,wvalue); + } + + if (error_cnt > CFG_ERRORS_LIMIT) { + parse_error("Too many errors, ignoring the rest of the file"); + break; + } + } + fr_close(tfd); + + return (split < 0 || error_cnt ) ? -1 : 0; +} + +void +cfg_initialize(void) +{ + int i, desc; + CNUM *pnum; + CSTR *pstr; + + /* initialize 'config' & 'pcfg' tables */ + for (i = 0; i < CFG_TOTAL_; i++) { + if ( (pnum = get_numeric(i)) ) { + config[i].isnum = 1; + config[i].table = pnum; + pcfg[i] = &pnum->current; + } + else if ( (pstr = get_string(i)) ) { + config[i].table = pstr; + pcfg[i] = &pstr->current; + } + else + err_exit("BUG: config variable not defined (code %d)",i); + + if ( (desc = get_desc(i)) < 0) + err_exit("BUG: no description for config variable (code %d)",i); + + config[i].var = table_desc[desc].name; + config[i].help = table_desc[desc].help; + if (wcslen(config[i].help) > MIN_COLS - 4) + msgout(MSG_NOTICE,"CONFIG: variable %s: help string \"%s\" is too long", + config[i].var,convert2mb(config[i].help)); + } + + /* initialize and read values */ + for (i = 0; i < CFG_TOTAL_; i++) + set_value(i,CP_I2C); + if (cfgfile_read() < 0) { + if (!user_data.nowrite) + msgout(MSG_NOTICE,"This might help: Main menu -> Configure CLEX -> Apply+Save"); + msgout(MSG_W,"CONFIG: An error occurred while reading data, details in log"); + } + + panel_cfg.config = config; +} + +/* the 'new' value in readable text form */ +static const wchar_t * +print_str_value(int i) +{ + CSTR *pstr; + + pstr = config[i].table; + if (pstr->extra_val != 0 && *pstr->new == L'\0') + return pstr->extra_val; + return pstr->new; +} + +/* the 'new' value in readable text form */ +static const wchar_t * +print_num_value(int i) +{ + static wchar_t buff[16]; + CNUM *pnum; + + pnum = config[i].table; + if (pnum->extra_val != 0 && pnum->new == 0) + return pnum->extra_val; + if (pnum->desc[0]) + /* enumerated */ + return pnum->desc[pnum->new - pnum->min]; + /* really numeric */ + swprintf(buff,ARRAY_SIZE(buff),L"%d",pnum->new); + return buff; +} + +const wchar_t * +cfg_print_value(int i) +{ + return config[i].isnum ? print_num_value(i) : print_str_value(i); +} + +static void +cfgfile_save(void) +{ + int i; + FILE *fp; + + if (user_data.nowrite) { + msgout(MSG_W,"CONFIG: Saving data to disk is prohibited"); + return; + } + + if ( (fp = fw_open(user_data.file_cfg)) ) { + fprintf(fp,"#\n# CLEX configuration file\n#\n"); + for (i = 0; i < CFG_TOTAL_; i++) + if (config[i].saveit) { + fprintf(fp,"%s=",config[i].var); + if (config[i].isnum) + fprintf(fp,"%d\n",((CNUM *)config[i].table)->new); + else + fprintf(fp,"%s\n",convert2mb(((CSTR *)config[i].table)->new)); + } + } + if (fw_close(fp)) { + msgout(MSG_W,"CONFIG: Could not save data, details in log"); + return; + } + + msgout(MSG_I,"CONFIG: Data saved"); +} + +int +cfg_prepare(void) +{ + int i; + + for (i = 0; i < CFG_TOTAL_; i++) + set_value(i,CP_C2N); + panel_cfg.pd->top = panel_cfg.pd->curs = panel_cfg.pd->min; + panel = panel_cfg.pd; + textline = 0; + return 0; +} + +int +cfg_menu_prepare(void) +{ + CNUM *pnum; + + pnum = config[panel_cfg.pd->curs].table; + panel_cfg_menu.pd->top = 0; + panel_cfg_menu.pd->cnt = pnum->max - pnum->min + 1; + panel_cfg_menu.pd->curs = pnum->new - pnum->min; + panel_cfg_menu.desc = pnum->desc; + panel = panel_cfg_menu.pd; + textline = 0; + + return 0; +} + +int +cfg_edit_num_prepare(void) +{ + wchar_t prompt[CFGVAR_LEN + 48]; + CNUM *pnum; + + /* inherited panel = panel_cfg.pd */ + pnum = config[panel_cfg.pd->curs].table; + textline = &line_tmp; + swprintf(prompt,ARRAY_SIZE(prompt),L"%s (range: %d - %d%s%ls): ", + config[panel_cfg.pd->curs].var,pnum->min,pnum->max, + pnum->extra_val != 0 ? " or " : "", + pnum->extra_val != 0 ? pnum->extra_val : L""); + edit_setprompt(textline,prompt); + edit_nu_putstr(print_num_value(panel_cfg.pd->curs)); + return 0; +} + +int +cfg_edit_str_prepare(void) +{ + wchar_t prompt[CFGVAR_LEN + 32]; + CSTR *pstr; + + /* inherited panel = panel_cfg.pd */ + pstr = config[panel_cfg.pd->curs].table; + textline = &line_tmp; + swprintf(prompt,ARRAY_SIZE(prompt),L"%s (" STR(CFGVALUE_LEN) " chars max%s%ls): ", + config[panel_cfg.pd->curs].var, + pstr->extra_val != 0 ? " or " : "", + pstr->extra_val != 0 ? pstr->extra_val : L""); + edit_setprompt(textline,prompt); + edit_nu_putstr(print_str_value(panel_cfg.pd->curs)); + return 0; +} + +void +cx_cfg_menu_enter(void) +{ + CNUM *pnum; + + pnum = config[panel_cfg.pd->curs].table; + pnum->new = pnum->min + panel_cfg_menu.pd->curs; + next_mode = MODE_SPECIAL_RETURN; +} + +void +cx_cfg_num_enter(void) +{ + int nvalue, len; + CNUM *pnum; + + pnum = config[panel_cfg.pd->curs].table; + if (pnum->extra_val != 0 && wcscmp(USTR(textline->line),pnum->extra_val) == 0) { + pnum->new = 0; + next_mode = MODE_SPECIAL_RETURN; + return; + } + + if (swscanf(USTR(textline->line),L" %d %n",&nvalue,&len) < 1 || len != textline->size) + msgout(MSG_i,"numeric value required"); + else if (nvalue < pnum->min || nvalue > pnum->max) + msgout(MSG_i,"value is out of range"); + else { + pnum->new = nvalue; + next_mode = MODE_SPECIAL_RETURN; + } +} + +void +cx_cfg_str_enter(void) +{ + CSTR *pstr; + + pstr = config[panel_cfg.pd->curs].table; + if (pstr->extra_val != 0 && wcscmp(USTR(textline->line),pstr->extra_val) == 0) { + *pstr->new = L'\0'; + next_mode = MODE_SPECIAL_RETURN; + return; + } + + if (textline->size > CFGVALUE_LEN) + msgout(MSG_i,"string is too long"); + else { + wcscpy(pstr->new,USTR(textline->line)); + next_mode = MODE_SPECIAL_RETURN; + } +} + +void +cx_cfg_default(void) +{ + set_value(panel_cfg.pd->curs,CP_I2N); + win_panel_opt(); +} + +void +cx_cfg_original(void) +{ + set_value(panel_cfg.pd->curs,CP_C2N); + win_panel_opt(); +} + +/* detect what has changed */ +static void +detect_changes(void) +{ + int i; + CNUM *pnum; + CSTR *pstr; + + for (i = 0; i < CFG_TOTAL_; i++) + if (config[i].isnum) { + pnum = config[i].table; + config[i].changed = pnum->new != pnum->current; + config[i].saveit = pnum->new != pnum->initial; + } + else { + pstr = config[i].table; + config[i].changed = wcscmp(pstr->new,pstr->current) != 0; + config[i].saveit = wcscmp(pstr->new,pstr->initial) != 0; + } +} + +static void +apply_changes(void) +{ + int i; + FLAG reread = 0; + + for (i = 0; i < CFG_TOTAL_; i++) + if (config[i].changed) + set_value(i,CP_N2C); + + if (config[CFG_FRAME].changed) { + win_frame_reconfig(); + win_frame(); + } + if (config[CFG_CMD_LINES].changed) { + curses_stop(); + msgout(MSG_i,"SCREEN: changing geometry"); + curses_restart(); + } + if (config[CFG_XTERM_TITLE].changed) { + xterm_title_restore(); + xterm_title_reconfig(); + xterm_title_set(0,0,0); + } + if (config[CFG_MOUSE].changed) { + mouse_restore(); + mouse_reconfig(); + mouse_set(); + } + if (config[CFG_PROMPT].changed) + set_shellprompt(); + if (config[CFG_LAYOUT].changed || config[CFG_LAYOUT1].changed + || config[CFG_LAYOUT2].changed || config[CFG_LAYOUT3].changed) { + layout_reconfig(); + reread = 1; + } + if (config[CFG_FMT_TIME].changed || config[CFG_FMT_DATE].changed + || config[CFG_TIME_DATE].changed) { + td_fmt_reconfig(); + reread = 1; + } + if (config[CFG_KILOBYTE].changed) { + kb_reconfig(); + reread = 1; + } + if (config[CFG_C_SIZE].changed) + compl_reconfig(); + if (config[CFG_D_SIZE].changed) + dir_reconfig(); + if (config[CFG_H_SIZE].changed) + hist_reconfig(); + + if (reread) { + list_directory(); + ppanel_file->other->expired = 1; + } +} + +void +cx_cfg_apply(void) +{ + detect_changes(); + apply_changes(); +} + +void +cx_cfg_apply_save(void) +{ + detect_changes(); + apply_changes(); + cfgfile_save(); +} + +void +cx_cfg_enter(void) +{ + CNUM *pnum; + + if (config[panel_cfg.pd->curs].isnum) { + pnum = config[panel_cfg.pd->curs].table; + control_loop(pnum->desc[0] ? MODE_CFG_MENU : MODE_CFG_EDIT_NUM); + } + else + control_loop(MODE_CFG_EDIT_TXT); + win_panel_opt(); +} + +void +cx_cfg_noexit(void) +{ + msgout(MSG_i,"please select Cancel, Apply or Save"); + cx_pan_home(); +} diff --git a/src/cfg.h b/src/cfg.h new file mode 100644 index 0000000..0093746 --- /dev/null +++ b/src/cfg.h @@ -0,0 +1,23 @@ +extern void cfg_initialize(void); +extern int cfg_prepare(void); +extern int cfg_menu_prepare(void); +extern int cfg_edit_num_prepare(void); +extern int cfg_edit_str_prepare(void); +extern const wchar_t *cfg_print_value(int); +extern void cx_cfg_enter(void); +extern void cx_cfg_menu_enter(void); +extern void cx_cfg_num_enter(void); +extern void cx_cfg_str_enter(void); +extern void cx_cfg_default(void); +extern void cx_cfg_original(void); +extern void cx_cfg_apply(void); +extern void cx_cfg_apply_save(void); +extern void cx_cfg_noexit(void); + +#define cfg_num(X) (*(const int *)pcfg[X]) +#define cfg_str(X) ((const wchar_t *)pcfg[X]) +#define cfg_layout (cfg_str(CFG_LAYOUT1 + cfg_num(CFG_LAYOUT) - 1)) + +/* max string lengths */ +#define CFGVAR_LEN 16 /* name */ +#define CFGVALUE_LEN 80 /* string value */ diff --git a/src/clex.1 b/src/clex.1 new file mode 100644 index 0000000..1a826b4 --- /dev/null +++ b/src/clex.1 @@ -0,0 +1,47 @@ +.TH CLEX 1 +.SH "NAME" +clex \- file manager +.SH "SYNOPSIS" +.B clex +.RI [ option ] +.SH "DESCRIPTION" +CLEX is an interactive full-screen file manager. Refer to the +on-line help for more information about usage. +.SH OPTIONS +.TP +.BI \-\-log " logfile" +Append log information to the +.IR logfile . +.TP +.B \-\-version +Display program version and some basic information and exit. +.TP +.B \-\-help +Display help and exit. +.SH "ENVIRONMENT" +CLEX makes use of the following environment variables: +.TP +.I SHELL +Determines the program to execute the commands. If not set, the shell from the +user database is used instead. +.TP +.I HOME +User's home directory. If not set, the directory from the +user database is used instead. +.SH "FILES" +.TP +.I ~/.config/clex/config +configuration file +.TP +.I ~/.config/clex/bookmarks +bookmarks file +.TP +.I ~/.config/clex/options +options file +.SH "SEE ALSO" +.IR cfg-clex (1) +.SH "AUTHOR" +Copyright (C) 2001-2022 Vlado Potisk +.SH "NOTES" +CLEX web page: +.B https://github.com/xitop/clex diff --git a/src/clex.h b/src/clex.h new file mode 100644 index 0000000..ab6c628 --- /dev/null +++ b/src/clex.h @@ -0,0 +1,711 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* + * naming convention for strings and buffers: + * #define XXX_STR is a buffer size, i.e. with the trailing null byte + * #define XXX_LEN is max string length, i.e. without the null byte + */ + +/* useful macros */ +#define ARRAY_SIZE(X) ((int)(sizeof(X) / sizeof(X[0]))) +#define TOGGLE(X) ((X) = !(X)) +#define TSET(X) ((X) ? 1 : ((X) = 1, 0)) /* test & set */ +#define TCLR(X) ((X) ? ((X) = 0, 1) : 0) /* test & clear */ +#define LIMIT_MIN(X,MIN) do if ((X) < (MIN)) (X) = (MIN); while (0) +#define LIMIT_MAX(X,MAX) do if ((X) > (MAX)) (X) = (MAX); while (0) +#define CH_CTRL(X) ((X) & 0x1F) /* byte ASCII ctrl-X */ +#define WCH_CTRL(X) ((wchar_t)CH_CTRL(X)) /* wide char ASCII ctrl-X */ +#define WCH_ESC L'\033' /* wchar ASCII escape */ +/* CMP: no overflow and result is an int even if V1 and V2 are not */ +#define CMP(V1,V2) ((V1) == (V2) ? 0 : (V1) < (V2) ? -1 : 1) +#define STR(X) STRINGIZE(X) +#define STRINGIZE(X) #X + +/* typedefs */ +typedef unsigned short int FLAG; /* true or false */ +typedef short int CODE; /* usually enum or some #define-d value */ + +/* minimal required screen size */ +#define MIN_COLS 64 +#define MIN_LINES 12 + +/* limits for prompts */ +#define MAX_SHORT_CWD_LEN ((MIN_COLS * 2) / 5) /* 40% */ +#define MAX_PROMPT_WIDTH ((MIN_COLS * 4) / 5) /* 80% */ + +/* max textline lines - valid values: 3, 4 and 5 */ +#define MAX_CMDLINES 4 + +/* operation modes */ +enum MODE_TYPE { + /* + * value 0 is reserved, it means mode unchanged in control_loop() + * and also mode not set during startup + */ + MODE_VALUE_RESERVED = 0, + /* regular modes */ + MODE_BM, MODE_BM_EDIT0, MODE_BM_EDIT1, MODE_BM_EDIT2, + MODE_CFG, MODE_CFG_EDIT_NUM, MODE_CFG_EDIT_TXT, MODE_CFG_MENU, + MODE_COMPL, MODE_CMP, MODE_CMP_SUM, MODE_DESELECT, + MODE_DIR, MODE_DIR_SPLIT, MODE_FILE, MODE_FOPT, MODE_GROUP, MODE_HELP, + MODE_HIST, MODE_INSCHAR, MODE_LOG, MODE_MAINMENU, MODE_NOTIF, MODE_PASTE, + MODE_PREVIEW, MODE_RENAME, MODE_SELECT, MODE_SORT, MODE_USER, + /* pseudo-modes */ + MODE_SPECIAL_QUIT, MODE_SPECIAL_RETURN +}; + +/* info about screen display/layout/appearance */ +typedef struct { + FLAG curses; /* curses active */ + FLAG wait; /* a message has been written to the + text-mode screen, wait for a keypress + before starting curses */ + FLAG noenter; /* if set, disable "press enter to continue" after command execution */ + unsigned int noenter_hash; /* a hash value of the command line is saved in order to detect + modifications which cancel the 'noenter' feature */ + FLAG bs177; /* KEY_BACKSPACE sends \177 (see control.c) */ + FLAG xterm; /* TERM is xterm or similar type which can change window title */ + FLAG noxterm; /* TERM is unable to change the window title */ + /* note: if not known, both xterm and noxterm are 0 */ + FLAG xwin; /* running in X Window environment */ + FLAG mouse; /* KEY_MOUSE suitable for xterm mouse tracking */ + FLAG mouse_swap; /* left-handed mouse (swap left and right buttons) */ + int scrcols; /* number of columns */ + int pancols; /* number of columns in the panel area */ + int panrcol; /* pancols adjusted for putwcs_trunc_col() */ + int scrlines; /* number of lines */ + int cmdlines; /* number of lines in the textline area */ + int panlines; /* number of lines in the panel area */ + int date_len; /* length of date/time field */ + int dir1end, dir2start; /* columns of the directory names in the file panel title */ + wchar_t *layout_panel; /* layout: file panel part */ + wchar_t *layout_line; /* layout: info line part */ +} DISP_DATA; + +/* info about language/encoding */ +typedef struct { + FLAG utf8; /* UTF-8 mode */ + wchar_t sep000; /* thousands separator */ + wchar_t repl; /* replacement for unprintable characters */ + const wchar_t *time_fmt, *date_fmt; /* time/date format strings (for strftime) */ +} LANG_DATA; + +/* info about the user */ +#define SHELL_SH 0 /* Bourne shell or similar */ +#define SHELL_CSH 1 /* C-shell or similar */ +#define SHELL_OTHER 2 /* other */ +typedef struct { + const char *login; /* my login name */ + const wchar_t *loginw; /* my login name */ + const char *host; /* my host */ + const wchar_t *hostw; /* my host */ + const char *homedir; /* my home directory */ + const wchar_t *homedirw; /* my home directory */ + const char *shell; /* my login shell - full path */ + const wchar_t *shellw; /* my login shell - basename only */ + const char *subdir; /* configuration directory */ + const char *file_cfg; /* configuration file */ + const char *file_opt; /* options file */ + const char *file_bm; /* bookmarks file */ + CODE shelltype; /* one of SHELL_XXX */ + FLAG isroot; /* effective uid is 0(root) */ + FLAG nowrite; /* do not write config/options/bookmark file */ + FLAG noconfig; /* no config file, cfg-clex recommended */ +} USER_DATA; + +typedef struct { + pid_t pid; /* process ID */ + char pidstr[16]; /* PID as a string */ + mode_t umask; /* umask value */ +} CLEX_DATA; + +/* description of an editing operation */ +enum OP_TYPE { + OP_NONE = 0, /* no change (cursor movement is OK) - value must be zero */ + OP_INS, /* simple insert */ + OP_DEL, /* simple deletion */ + OP_CHANGE /* modification other than OP_INS or OP_DEL */ +}; +typedef struct { + enum OP_TYPE code; +/* 'pos' and 'len' are used with OP_INSERT and OP_DELETE only */ + int pos; /* position within the edited string */ + int len; /* length of the inserted/deleted part */ +} EDIT_OP; + +#define UNDO_LEVELS 10 /* undo steps */ + +/* line of text where the user can enter and edit his/her input */ +typedef struct { + USTRINGW prompt; /* prompt */ + int promptwidth; /* prompt width in display columns */ + USTRINGW line; /* user's input */ + int size; /* number of chars in the line */ + int curs; /* cursor position from 0 to 'size' */ + int offset; /* offset - when the line is too long, + first 'offset' characters are hidden */ + /* values for the UNDO function */ + struct { + USTRINGW save_line; + int save_size; + int save_curs; + int save_offset; + } undo [UNDO_LEVELS]; /* used in a circular manner */ + int undo_base; /* index of the first entry */ + int undo_levels; /* occupied entries for undo */ + int redo_levels; /* freed entries usable for redo */ + EDIT_OP last_op; /* last editing operation */ +} TEXTLINE; + +/* minimalistic version of TEXTLINE used for panel filters */ +#define INPUT_STR 23 +typedef struct { + wchar_t line[INPUT_STR]; /* user's input */ + int size; /* number of chars in the line */ + int curs; /* cursor position from 0 to 'size' */ + FLAG changed; /* 'line' has been modified */ +} INPUTLINE; + +/********************************************************************/ + +/* keyboard and mouse input */ + +typedef struct { + CODE fkey; /* 2 = not a key, but a mouse event, 1 = keypad or 0 = character */ + wint_t key; + FLAG prev_esc; /* previous key was an ESCAPE */ +} KBD_INPUT; + +enum AREA_TYPE { + /* from top to bottom */ + AREA_TITLE = 0, AREA_TOPFRAME, AREA_PANEL, AREA_BOTTOMFRAME, + AREA_INFO, AREA_HELP, AREA_BAR, AREA_PROMPT, AREA_LINE, + AREA_NONE +}; +typedef struct { + /* mouse data */ + int y, x; /* zero-based coordinates: line and column */ + CODE button; /* regular buttons: 1, 2 and 3, wheel: 4 and 5 */ + FLAG doubleclick; /* a doubleclick (NOTE: first click is aslo reported!) */ + FLAG motion; /* the mouse was in motion (drag) */ + + /* computed values */ + int area; /* one of AREA_xxx */ + int ypanel; /* panel screen line (AREA_PANEL only): 0 = top */ + int cursor; /* calculated cursor position, see mouse_data() in inout.c */ +} MOUSE_INPUT; + +/* shortcuts */ +#define MI_B(X) (minp.button == (X)) +#define MI_DC(X) (MI_B(X) && minp.doubleclick) +#define MI_CLICK (minp.button == 1 || minp.button == 3) +#define MI_WHEEL (minp.button == 4 || minp.button == 5) +#define MI_AREA(X) (minp.area == AREA_ ## X) +#define MI_CURSBAR (MI_AREA(PANEL) && VALID_CURSOR(panel) && panel->top + minp.ypanel == panel->curs) +#define MI_DRAG (minp.motion) +#define MI_PASTE (MI_B(3) && !MI_DRAG && MI_CURSBAR) + +/********************************************************************/ + +/* panel types */ +enum PANEL_TYPE { + PANEL_TYPE_BM = 0, PANEL_TYPE_CFG, PANEL_TYPE_CFG_MENU, PANEL_TYPE_CMP, PANEL_TYPE_CMP_SUM, + PANEL_TYPE_COMPL, PANEL_TYPE_DIR, PANEL_TYPE_DIR_SPLIT, PANEL_TYPE_FILE, + PANEL_TYPE_FOPT, PANEL_TYPE_GROUP, PANEL_TYPE_HELP, PANEL_TYPE_HIST, + PANEL_TYPE_LOG, PANEL_TYPE_MAINMENU, PANEL_TYPE_NOTIF, PANEL_TYPE_PASTE, + PANEL_TYPE_PREVIEW, PANEL_TYPE_SORT, PANEL_TYPE_USER +}; + +/* + * extra lines appear in a panel before the first regular line, + * extra lines: -MIN .. -1 + * regular lines: 0 .. MAX + */ +typedef struct { + const wchar_t *text; /* text to be displayed in the panel */ + /* default (if null): "Exit this panel" */ + const wchar_t *info; /* text to be displayed in the info line */ + /* when this extra line is selected: */ + CODE mode_next; /* set next_mode to this mode and then ... */ + void (*fn)(void); /* ... invoke this function */ +} EXTRA_LINE; + +/* description of a panel */ +typedef struct { + int cnt, top; /* panel lines: total count, top of the screen */ + int curs, min; /* panel lines: cursor bar, top of the panel */ + /* + * 'min' is used to insert extra lines before the real first line + * which is always line number 0; to insert N extra lines set + * 'min' to -N; the number of extra lines is not included in 'cnt' + */ + enum PANEL_TYPE type; + FLAG norev; /* do not show the current line in reversed video */ + EXTRA_LINE *extra; /* extra panel lines */ + INPUTLINE *filter; /* filter (if applicable to this panel type) */ + void (*drawfn)(int);/* function drawing one line of this panel */ + CODE filtering; /* filter: 0 = off */ + /* 1 = on - focus on the filter string */ + /* 2 = on - focus on the command line */ + const wchar_t *help; /* helpline override */ +} PANEL_DESC; + +#define VALID_CURSOR(P) ((P)->cnt > 0 && (P)->curs >= 0 && (P)->curs < (P)->cnt) + +/********************************************************************/ + +/* + * file types recognized in the file panel, + * if you change this, you must also update + * the type_symbol[] array in inout.c + */ +#define FT_PLAIN_FILE 0 +#define FT_PLAIN_EXEC 1 +#define FT_PLAIN_SUID 2 +#define FT_PLAIN_SUID_ROOT 3 +#define FT_PLAIN_SGID 4 +#define FT_DIRECTORY 5 +#define FT_DIRECTORY_MNT 6 +#define FT_DEV_BLOCK 7 +#define FT_DEV_CHAR 8 +#define FT_FIFO 9 +#define FT_SOCKET 10 +#define FT_OTHER 11 +#define FT_NA 12 + +/* file type tests */ +#define IS_FT_PLAIN(X) ((X) >= 0 && (X) <= 4) +#define IS_FT_EXEC(X) ((X) >= 1 && (X) <= 4) +#define IS_FT_DIR(X) ((X) >= 5 && (X) <= 6) +#define IS_FT_DEV(X) ((X) >= 7 && (X) <= 8) + +/* + * if you change any of the FE_XXX_STR #defines, you must change + * the corresponding stat2xxx() function(s) in list.c accordingly + */ +/* text buffer sizes */ /* examples: */ +#define FE_LINKS_STR 4 /* 1 999 max */ +#define FE_TIME_STR 23 /* 11:34:30_am_2010/12/01 */ +#define FE_AGE_STR 10 /* -01:02:03 */ +#define FE_SIZE_DEV_STR 12 /* 3.222.891Ki */ +#define FE_MODE_STR 5 /* 0644 */ +#define FE_NAME_STR 17 /* root */ +#define FE_OWNER_STR (2 * FE_NAME_STR) /* root:mail */ + +/* + * file description - exhausting, isn't it ? + * we allocate many of these, bitfields save memory + */ +typedef struct { + SDSTRING file; /* file name - as it is */ + SDSTRINGW filew; /* file name - converted to wchar for the screen output */ + USTRING link; /* where the symbolic link points to */ + USTRINGW linkw; /* ditto */ + const char *extension; /* file name extension (suffix) */ + time_t mtime; /* last file modification */ + off_t size; /* file size */ + dev_t devnum; /* major/minor numbers (devices only) */ + CODE file_type; /* one of FT_XXX */ + uid_t uid, gid; /* owner and group */ + short int mode12; /* file mode - low 12 bits */ + unsigned int select:1; /* flag: this entry is selected */ + unsigned int symlink:1; /* flag: it is a symbolic link */ + unsigned int dotdir:2; /* . (1) or .. (2) directory */ + unsigned int fmatch:1; /* flag: matches the filter */ + /* + * note: the structure members below are used + * only when the file panel layout requires them + */ + unsigned int normal_mode:1; /* file mode same as "normal" file */ + unsigned int links:1; /* has multiple hard links */ + wchar_t atime_str[FE_TIME_STR]; /* access time */ + wchar_t ctime_str[FE_TIME_STR]; /* inode change time */ + wchar_t mtime_str[FE_TIME_STR]; /* file modification time */ + wchar_t owner_str[FE_OWNER_STR];/* owner and group */ + char age_str[FE_AGE_STR]; /* time since the last modification ("file age") */ + char links_str[FE_LINKS_STR]; /* number of links */ + char mode_str[FE_MODE_STR]; /* file mode - octal number */ + char size_str[FE_SIZE_DEV_STR]; /* file size or dev major/minor */ +} FILE_ENTRY; + +/* + * When a filter or the selection panel is activated or when panels are switched, the + * file panel will be refreshed if the contents are older than PANEL_EXPTIME seconds. + * This time is not configurable because it would confuse a typical user. + */ +#define PANEL_EXPTIME 60 + +typedef struct ppanel_file { + PANEL_DESC *pd; + USTRING dir; /* working directory */ + USTRINGW dirw; /* working directory for screen output */ + struct ppanel_file *other; /* primary <--> secondary panel ptr */ + time_t timestamp; /* when was the directory listed */ + FLAG expired; /* expiration: panel needs to be re-read */ + FLAG filtype; /* filter type: 0 = substring, 1 = pattern */ + CODE order; /* sort order: one of SORT_XXX */ + CODE group; /* group by type: one of GROUP_XXX */ + CODE hide; /* ignore hidden .files: one of HIDE_XXX */ + FLAG hidden; /* there exist hidden .files not shown */ + /* unfiltered data - access only in list.c and sort.c */ + int all_cnt; /* number of all files */ + int all_alloc; /* allocated FILE_ENTRies in 'all_files' below */ + FILE_ENTRY **all_files; /* list of files in panel's working directory 'dir' */ + /* filtered data */ + int filt_alloc; + int selected_out; /* number of selected entries filtered out */ + FILE_ENTRY **filt_files;/* list of files selected by the filter */ + /* presented data */ + int selected; + FILE_ENTRY **files; /* 'all_files' or 'filt_files' depending on the filter status */ + /* column width information (undefined for unused fields) */ + /* number of blank leading characters */ + int cw_ln1; /* $l */ + int cw_sz1; /* $r,$s,$R,$S */ + int cw_ow1; /* $o */ + int cw_age; /* $g */ + /* field width */ + int cw_mod; /* $M */ + int cw_lns; /* $> */ + int cw_lnh; /* $L */ + int cw_sz2; /* $r,$s,$R,$S */ + int cw_ow2; /* $o */ +} PANEL_FILE; + +/********************************************************************/ + +typedef struct bookmark { + SDSTRINGW name; + USTRING dir; + USTRINGW dirw; +} BOOKMARK; + +typedef struct { + PANEL_DESC *pd; + BOOKMARK **bm; + int cw_name; /* field width */ +} PANEL_BM; + +typedef struct { + PANEL_DESC *pd; + BOOKMARK *bm; +} PANEL_BM_EDIT; + +/********************************************************************/ + +/* notifications */ +enum NOTIF_TYPE { + NOTIF_RM = 0, NOTIF_LONG, NOTIF_DOTDIR, NOTIF_SELECTED, NOTIF_FUTURE, + NOTIF_TOTAL_ +}; + +typedef struct { + PANEL_DESC *pd; + FLAG option[NOTIF_TOTAL_]; +} PANEL_NOTIF; + +/* IMPORTANT: value 1 = notification disabled, 0 = enabled (default) */ +#define NOPT(X) (panel_notif.option[X]) + +/********************************************************************/ + +/* + * file sort order and grouping- if you change this, you must also update + * panel initization in start.c and descriptions in inout.c + */ +enum SORT_TYPE { + SORT_NAME_NUM = 0, SORT_NAME, SORT_EXT, SORT_SIZE, + SORT_SIZE_REV, SORT_TIME, SORT_TIME_REV, SORT_EMAN, + SORT_TOTAL_ +}; + +enum GROUP_TYPE { + GROUP_NONE = 0, GROUP_DSP, GROUP_DBCOP, + GROUP_TOTAL_ +}; + +enum HIDE_TYPE { + HIDE_NEVER, HIDE_HOME, HIDE_ALWAYS, + HIDE_TOTAL_ +}; + +typedef struct { + PANEL_DESC *pd; + /* current values: */ + CODE group; /* group by type: one of GROUP_XXX */ + CODE order; /* default file sort order: one of SORT_XXX */ + CODE hide; /* do not show hidden .files */ + /* for the user interface menu: */ + CODE newgroup; + CODE neworder; + CODE newhide; +} PANEL_SORT; + +/********************************************************************/ + +typedef struct { + const char *name; /* directory name */ + const wchar_t *namew; /* directory name for display */ + int shlen; /* length of the repeating 'namew' part */ +} DIR_ENTRY; + +typedef struct { + PANEL_DESC *pd; + DIR_ENTRY *dir; /* list of directories to choose from */ +} PANEL_DIR; + +typedef struct { + PANEL_DESC *pd; + const char *name; /* directory name */ +} PANEL_DIR_SPLIT; + +/********************************************************************/ +/* configuration variables in config panel order */ +enum CFG_TYPE { + /* appearance */ + CFG_FRAME, CFG_CMD_LINES, CFG_XTERM_TITLE, CFG_PROMPT, + CFG_LAYOUT1, CFG_LAYOUT2, CFG_LAYOUT3, CFG_LAYOUT, CFG_KILOBYTE, + CFG_FMT_TIME, CFG_FMT_DATE, CFG_TIME_DATE, + /* command execution */ + CFG_CMD_F3, CFG_CMD_F4, CFG_CMD_F5, CFG_CMD_F6, CFG_CMD_F7, + CFG_CMD_F8, CFG_CMD_F9, CFG_CMD_F10, CFG_CMD_F11, CFG_CMD_F12, + /* mouse */ + CFG_MOUSE, CFG_MOUSE_SCROLL, CFG_DOUBLE_CLICK, + /* other */ + CFG_QUOTE, CFG_C_SIZE, CFG_D_SIZE, CFG_H_SIZE, + /* total count*/ + CFG_TOTAL_ +}; + +typedef struct { + const char *var; /* name of the variable */ + const wchar_t *help; /* one line help */ + void *table; /* -> internal table with details */ + unsigned int isnum:1; /* is numeric (not string) */ + unsigned int changed:1; /* value changed */ + unsigned int saveit:1; /* value should be saved to disk */ +} CFG_ENTRY; + +typedef struct { + PANEL_DESC *pd; + CFG_ENTRY *config; /* list of all configuration variables */ +} PANEL_CFG; + +typedef struct { + PANEL_DESC *pd; + const wchar_t **desc; /* list of textual descriptions */ +} PANEL_CFG_MENU; + +/********************************************************************/ + +typedef struct { + USTRINGW cmd; /* command text */ + FLAG failed; /* command failed or not */ +} HIST_ENTRY; + +typedef struct { + PANEL_DESC *pd; + HIST_ENTRY **hist; /* list of previously executed commands */ +} PANEL_HIST; + +/********************************************************************/ + +typedef struct { + CODE type; /* one of HL_XXX defined in help.h */ + const char *data; /* value of HL_LINK, HL_PAGE or HL_VERSION */ + const wchar_t *text; /* text of HL_TEXT... or HL_TITLE */ + int links; /* number of links in the whole line + (valid for leading HL_TEXT only) */ +} HELP_LINE; + +typedef struct { + PANEL_DESC *pd; + CODE pagenum; /* internal number of current page */ + const wchar_t *title; /* title of the current help page */ + int lnk_act, lnk_ln; /* multiple links in a single line: + lnk_act = which link is active + lnk_ln = for which line is lnk_act valid */ + HELP_LINE **line; +} PANEL_HELP; + +/********************************************************************/ + +typedef struct { + SDSTRINGW str; /* name suitable for a completion */ + FLAG is_link; /* filenames only: it is a symbolic link */ + CODE file_type; /* filenames only: one of FT_XXX */ + const wchar_t *aux; /* additional data (info line) */ +} COMPL_ENTRY; + +typedef struct { + PANEL_DESC *pd; + FLAG filenames; /* stored names are names of files */ + const wchar_t *aux; /* type of additional data - as a string */ + const wchar_t *title; /* panel title */ + COMPL_ENTRY **cand; /* list of completion candidates */ +} PANEL_COMPL; + +/********************************************************************/ + +/* - must correspond with descriptions in draw_line_fopt() in inout.c */ +/* - must correspond with panel_fopt initializer in start.c */ +/* - fopt_saveopt(), fopt_restoreopt() must remain backward compatible */ +enum FOPT_TYPE { + FOPT_IC, FOPT_ALL, FOPT_SHOWDIR, + FOPT_TOTAL_ +}; + +typedef struct { + PANEL_DESC *pd; + FLAG option[FOPT_TOTAL_]; +} PANEL_FOPT; +#define FOPT(X) (panel_fopt.option[X]) + +/********************************************************************/ + +/* - must correspond with descriptions in draw_line_cmp() in inout.c */ +/* - must correspond with panel_cmp initializer in start.c */ +/* - cmp_saveopt(), cmp_restoreopt() must remain backward compatible */ +enum CMP_TYPE { + CMP_REGULAR, CMP_SIZE, CMP_MODE, CMP_OWNER, CMP_DATA, + CMP_TOTAL_ +}; + +typedef struct { + PANEL_DESC *pd; + FLAG option[CMP_TOTAL_]; +} PANEL_CMP; +#define COPT(X) (panel_cmp.option[X]) + +/********************************************************************/ + +#define LOG_LINES 50 +#define TIMESTAMP_STR 48 + +typedef struct { + CODE level; /* one of MSG_xxx (defined in log.h) */ + const char *levelstr; /* level as a string */ + char timestamp[TIMESTAMP_STR]; /* time/date as a string */ + USTRINGW msg; /* message */ + int cols; /* message width in screen columns */ +} LOG_ENTRY; + +typedef struct { + PANEL_DESC *pd; /* no additional data */ + int scroll; /* amount of horizontal text scroll, normally 0 */ + int maxcols; /* max message width */ + LOG_ENTRY *line[LOG_LINES]; +} PANEL_LOG; + +/********************************************************************/ + +typedef struct { + PANEL_DESC *pd; /* no additional data */ +} PANEL_MENU; + +/********************************************************************/ + +typedef struct { + PANEL_DESC *pd; + FLAG wordstart; /* complete from: 0 = beginning of the word, 1 = cursor position */ +} PANEL_PASTE; + +/********************************************************************/ + +#define PREVIEW_LINES 400 +#define PREVIEW_BYTES 16383 /* fr_open() adds 1 */ + +typedef struct { + PANEL_DESC *pd; + int realcnt; /* lines with real data, used for --end-- mark */ + wchar_t *title; /* name of the file */ + USTRINGW line[PREVIEW_LINES]; +} PANEL_PREVIEW; + +/********************************************************************/ + +typedef struct { + uid_t uid; + const wchar_t *login; + const wchar_t *gecos; +} USER_ENTRY; + +typedef struct { + PANEL_DESC *pd; + USER_ENTRY *users; + int usr_alloc; /* allocated entries in 'users' */ + size_t maxlen; /* length of the longest name */ +} PANEL_USER; + +typedef struct { + gid_t gid; + const wchar_t *group; +} GROUP_ENTRY; + +typedef struct { + PANEL_DESC *pd; + GROUP_ENTRY *groups; + int grp_alloc; /* allocated entries in 'groups' */ +} PANEL_GROUP; + +/********************************************************************/ + +typedef struct { + PANEL_DESC *pd; + int nonreg1, nonreg2, errors, names, equal; +} PANEL_CMP_SUM; + +/********************************************************************/ + +/* global variables */ + +extern const void *pcfg[CFG_TOTAL_]; + +extern DISP_DATA disp_data; +extern LANG_DATA lang_data; +extern USER_DATA user_data; +extern CLEX_DATA clex_data; + +extern TEXTLINE *textline; /* -> active line */ +extern TEXTLINE line_cmd, line_dir, line_tmp, line_inschar; + +extern MOUSE_INPUT minp; +extern KBD_INPUT kinp; + +extern PANEL_DESC *panel; /* -> description of the active panel */ +extern PANEL_FILE *ppanel_file; +extern PANEL_CFG panel_cfg; +extern PANEL_CFG_MENU panel_cfg_menu; +extern PANEL_BM_EDIT panel_bm_edit; +extern PANEL_BM panel_bm; +extern PANEL_CMP panel_cmp; +extern PANEL_COMPL panel_compl; +extern PANEL_CMP_SUM panel_cmp_sum; +extern PANEL_DIR panel_dir; +extern PANEL_DIR_SPLIT panel_dir_split; +extern PANEL_FOPT panel_fopt; +extern PANEL_GROUP panel_group; +extern PANEL_HELP panel_help; +extern PANEL_HIST panel_hist; +extern PANEL_LOG panel_log; +extern PANEL_MENU panel_mainmenu; +extern PANEL_NOTIF panel_notif; +extern PANEL_PASTE panel_paste; +extern PANEL_PREVIEW panel_preview; +extern PANEL_SORT panel_sort; +extern PANEL_USER panel_user; + +extern CODE next_mode; /* see control.c comments */ +extern volatile FLAG ctrlc_flag; diff --git a/src/clexheaders.h b/src/clexheaders.h new file mode 100644 index 0000000..d226ced --- /dev/null +++ b/src/clexheaders.h @@ -0,0 +1,14 @@ +#if defined(__APPLE__) +# define _XOPEN_SOURCE_EXTENDED +#elif !defined(__FreeBSD__) +# define _XOPEN_SOURCE 600 +#endif + +#include "../config.h" + +#include +#include +#include "sdstring.h" +#include "ustring.h" + +#include "clex.h" diff --git a/src/cmp.c b/src/cmp.c new file mode 100644 index 0000000..5dd32e0 --- /dev/null +++ b/src/cmp.c @@ -0,0 +1,354 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* comparing the contents of two directories (filepanels) */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* errno */ +#include /* open() */ +#include /* log.h */ +#include /* qsort() */ +#include /* strcmp() */ +#include /* close() */ + +#include "select.h" + +#include "inout.h" /* win_waitmsg() */ +#include "list.h" /* list_both_directories() */ +#include "log.h" /* msgout() */ +#include "opt.h" /* opt_changed() */ +#include "panel.h" /* pan_adjust() */ +#include "signals.h" /* signal_ctrlc_on() */ +#include "util.h" /* pathname_join() */ + +int +cmp_prepare(void) +{ + if (strcmp(USTR(ppanel_file->dir),USTR(ppanel_file->other->dir)) == 0) { + msgout(MSG_i,"COMPARE: same directory in both panels"); + return -1; + } + + panel_cmp.pd->top = panel_fopt.pd->min; + panel_cmp.pd->curs = panel_cmp.pd->cnt - 1; /* last line */ + panel = panel_cmp.pd; + pan_adjust(panel); + textline = 0; + + return 0; +} + +int +cmp_summary_prepare(void) +{ + panel_cmp_sum.pd->top = panel_cmp_sum.pd->curs = panel_cmp_sum.pd->min; + panel_cmp_sum.pd->cnt = panel_cmp_sum.errors ? 6 : 5; + panel = panel_cmp_sum.pd; + textline = 0; + + if (panel_cmp_sum.errors) + win_sethelp(HELPMSG_BASE,L"Error messages can be found in the log (alt-L)"); + + return 0; +} + +/* write options to a string */ +const char * +cmp_saveopt(void) +{ + int i, j; + static char buff[CMP_TOTAL_ + 1]; + + for (i = j = 0; i < CMP_TOTAL_; i++) + if (COPT(i)) + buff[j++] = 'A' + i; + buff[j] = '\0'; + + return buff; +} + +/* read options from a string */ +int +cmp_restoreopt(const char *opt) +{ + int i; + unsigned char ch; + + for (i = 0; i < CMP_TOTAL_; i++) + COPT(i) = 0; + + while ( (ch = *opt++) ) { + if (ch < 'A' || ch >= 'A' + CMP_TOTAL_) + return -1; + COPT(ch - 'A') = 1; + } + + return 0; +} + +static int +qcmp(const void *e1, const void *e2) +{ + return strcmp( /* not strcoll() */ + SDSTR((*(FILE_ENTRY **)e1)->file), + SDSTR((*(FILE_ENTRY **)e2)->file)); +} + +#define CMP_BUF_STR 16384 + +/* return value: -1 error, 0 compare ok, +1 compare failed */ +static int +data_cmp(int fd1, const char *file1, int fd2, const char *file2) +{ + struct stat st1, st2; + static char buff1[CMP_BUF_STR], buff2[CMP_BUF_STR]; + off_t filesize; + size_t chunksize; + + if (fstat(fd1,&st1) < 0 || !S_ISREG(st1.st_mode)) { + msgout(MSG_NOTICE,"COMPARE: File \"./%s\" is not a regular file",file1); + return -1; + } + if (fstat(fd2,&st2) < 0 || !S_ISREG(st2.st_mode)) { + msgout(MSG_NOTICE,"COMPARE: File \"%s\" is not a regular file",file2); + return -1; + } + if (st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) + /* same file */ + return 0; + if ((filesize = st1.st_size) != st2.st_size) + return 1; + + while (filesize > 0) { + chunksize = filesize > CMP_BUF_STR ? CMP_BUF_STR : filesize; + if (ctrlc_flag) + return -1; + if (read_fd(fd1,buff1,chunksize) != chunksize) { + msgout(MSG_NOTICE,"COMPARE: Cannot read from \"./%s\" (%s)",file1,strerror(errno)); + return -1; + } + if (read_fd(fd2,buff2,chunksize) != chunksize) { + msgout(MSG_NOTICE,"COMPARE: Cannot read from \"%s\" (%s)",file2,strerror(errno)); + return -1; + } + if (memcmp(buff1,buff2,chunksize) != 0) + return 1; + filesize -= chunksize; + } + + return 0; +} + +/* return value: -1 error, 0 compare ok, +1 compare failed */ +static int +file_cmp(const char *file1, const char *file2) +{ + int cmp, fd1, fd2; + + fd1 = open(file1,O_RDONLY | O_NONBLOCK); + if (fd1 < 0) { + msgout(MSG_NOTICE,"COMPARE: Cannot open \"./%s\" (%s)",file1,strerror(errno)); + return -1; + } + fd2 = open(file2,O_RDONLY | O_NONBLOCK); + if (fd2 < 0) { + msgout(MSG_NOTICE,"COMPARE: Cannot open \"%s\" (%s)",file2,strerror(errno)); + close(fd1); + return -1; + } + cmp = data_cmp(fd1,file1,fd2,file2); + close(fd1); + close(fd2); + return cmp; +} + +static void +cmp_directories(void) +{ + int min, med, max, cmp, i, j, cnt1, selcnt1, selcnt2; + const char *name2; + FILE_ENTRY *pfe1, *pfe2; + static FILE_ENTRY **p1 = 0; /* copy of panel #1 sorted for binary search */ + static int p1_alloc = 0; + + /* + * - select all files and both panels + * - for each file from panel #2: + * - find a matching file from panel #1 + * - if a pair is found, compare them according to the selected options + * - if the compared files are equal, deselect them + */ + + panel_cmp_sum.errors = panel_cmp_sum.names = panel_cmp_sum.equal = 0; + + /* reread panels */ + list_both_directories(); + + ctrlc_flag = 0; + if (COPT(CMP_DATA)) { /* going to compare data */ + signal_ctrlc_on(); + win_waitmsg(); + pathname_set_directory(USTR(ppanel_file->other->dir)); + } + + if (COPT(CMP_REGULAR)) { + for (cnt1 = i = 0; i < ppanel_file->pd->cnt; i++) { + pfe1 = ppanel_file->files[i]; + if ( (pfe1->select = IS_FT_PLAIN(pfe1->file_type)) ) + cnt1++; + } + panel_cmp_sum.nonreg1 = ppanel_file->pd->cnt - cnt1; + } + else { + cnt1 = ppanel_file->pd->cnt; + panel_cmp_sum.nonreg1 = 0; + } + selcnt1 = cnt1; + + if (cnt1) { + if (p1_alloc < cnt1) { + efree(p1); + p1_alloc = cnt1; + p1 = emalloc(p1_alloc * sizeof(FILE_ENTRY *)); + } + + if (COPT(CMP_REGULAR)) + for (i = j = 0; i < ppanel_file->pd->cnt; i++) { + pfe1 = ppanel_file->files[i]; + if (pfe1->select) + p1[j++] = pfe1; + } + else + for (i = 0; i < ppanel_file->pd->cnt; i++) { + pfe1 = p1[i] = ppanel_file->files[i]; + pfe1->select = 1; + } + + qsort(p1,cnt1,sizeof(FILE_ENTRY *),qcmp); + } + + panel_cmp_sum.nonreg2 = 0; + selcnt2 = 0; + for (i = 0; i < ppanel_file->other->pd->cnt; i++) { + pfe2 = ppanel_file->other->files[i]; + if ( !(pfe2->select = !COPT(CMP_REGULAR) || IS_FT_PLAIN(pfe2->file_type)) ) { + panel_cmp_sum.nonreg2++; + continue; + } + selcnt2++; + + if (panel_cmp_sum.names == cnt1) + /* we have seen all files from panel#1 */ + continue; /* not break */ + + name2 = SDSTR(pfe2->file); + for (pfe1 = 0, min = 0, max = cnt1 - 1; min <= max; ) { + med = (min + max) / 2; + cmp = strcmp(name2,SDSTR(p1[med]->file)); + if (cmp == 0) { + pfe1 = p1[med]; + /* entries *pfe1 and *pfe2 have the same name */ + break; + } + if (cmp < 0) + max = med - 1; + else + min = med + 1; + } + if (pfe1 == 0 || !pfe1->select) + continue; + + panel_cmp_sum.names++; + + /* always comparing type */ + if (pfe1->file_type == FT_NA || !( + (IS_FT_PLAIN(pfe1->file_type) && IS_FT_PLAIN(pfe2->file_type)) + || (IS_FT_DIR(pfe1->file_type) && IS_FT_DIR(pfe2->file_type)) + || (pfe1->file_type == pfe2->file_type) ) + ) + continue; + if (pfe1->symlink != pfe2->symlink) + continue; + + /* comparing size (or device numbers) */ + if (COPT(CMP_SIZE) + && ((IS_FT_DEV(pfe1->file_type) && pfe1->devnum != pfe2->devnum) + || (IS_FT_PLAIN(pfe1->file_type) && pfe1->size != pfe2->size))) + continue; + + if (COPT(CMP_OWNER) + && (pfe1->uid != pfe2->uid || pfe1->gid != pfe2->gid)) + continue; + + if (COPT(CMP_MODE) && pfe1->mode12 != pfe2->mode12) + continue; + + if (COPT(CMP_DATA) && IS_FT_PLAIN(pfe1->file_type)) { + if (pfe1->size != pfe2->size) + continue; + if ( (cmp = file_cmp(SDSTR(pfe1->file),pathname_join(name2))) ) { + if (ctrlc_flag) + break; + if (cmp < 0) + panel_cmp_sum.errors++; + continue; + } + } + + /* pair of matching files found */ + pfe1->select = 0; + selcnt1--; + pfe2->select = 0; + selcnt2--; + panel_cmp_sum.equal++; + } + ppanel_file->selected = selcnt1; + ppanel_file->other->selected = selcnt2; + + if (COPT(CMP_DATA)) + signal_ctrlc_off(); + + if (ctrlc_flag) { + msgout(MSG_i,"COMPARE: operation canceled"); + /* clear all marks */ + for (i = 0; i < cnt1; i++) + ppanel_file->files[i]->select = 0; + ppanel_file->selected = 0; + for (i = 0; i < ppanel_file->other->pd->cnt; i++) + ppanel_file->other->files[i]->select = 0; + ppanel_file->other->selected = 0; + + next_mode = MODE_SPECIAL_RETURN; + return; + } + + next_mode = MODE_CMP_SUM; +} + +void +cx_cmp(void) +{ + int sel; + + sel = panel_cmp.pd->curs; + if (sel < CMP_TOTAL_) { + TOGGLE(COPT(sel)); + opt_changed(); + win_panel_opt(); + } + else + cmp_directories(); +} diff --git a/src/cmp.h b/src/cmp.h new file mode 100644 index 0000000..a3d2c38 --- /dev/null +++ b/src/cmp.h @@ -0,0 +1,5 @@ +extern int cmp_prepare(void); +extern int cmp_summary_prepare(void); +extern const char *cmp_saveopt(void); +extern int cmp_restoreopt(const char *); +extern void cx_cmp(void); diff --git a/src/completion.c b/src/completion.c new file mode 100644 index 0000000..fc54e49 --- /dev/null +++ b/src/completion.c @@ -0,0 +1,1010 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* tolower() */ +#include /* readdir() */ +#include /* errno */ +#include /* log.h */ +#include /* qsort() */ +#include /* EOF */ +#include /* strerror() */ +#include /* time() */ +#include /* stat() */ +#include /* iswalnum() */ + +#include "completion.h" + +#include "cfg.h" /* cfg_num() */ +#include "control.h" /* control_loop() */ +#include "edit.h" /* edit_update() */ +#include "history.h" /* get_history_entry() */ +#include "inout.h" /* win_waitmsg() */ +#include "lex.h" /* usw_dequote() */ +#include "list.h" /* stat2type() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr() */ +#include "mbwstring.h" /* convert2w() */ +#include "sort.h" /* num_wcscoll() */ +#include "userdata.h" /* username_find() */ +#include "util.h" /* emalloc() */ + +/* internal use only */ +#define COMPL_TYPE_PATHCMD 100 /* search $PATH */ +#define COMPL_TYPE_USERDIR 101 /* with trailing slash (~name/) */ +#define COMPL_TYPE_ENV2 102 /* with trailing curly brace ${env} */ + +/* commands are stored in several linked lists depending on the first character */ +#define LIST_NR(ch) ((ch) >= L'a' && (ch) <= L'z' ? (ch) - L'a' : 26) +#define LISTS 27 +typedef struct cmd { + SDSTRING cmd; /* command name */ + SDSTRINGW cmdw; /* command name */ + struct cmd *next; /* linked list ptr */ +} CMD; +/* PATHDIR holds info about commands in a PATH member directory */ +typedef struct { + const char *dir; /* PATH directory name */ + const wchar_t *dirw; /* PATH directory name */ + time_t timestamp; /* time of last successfull directory scan, or 0 */ + dev_t device; /* device/inode from stat() */ + ino_t inode; + CMD *commands[LISTS]; /* lists of commands in this directory */ +} PATHDIR; + +static PATHDIR *pd_list; /* PATHDIRs for all members of the PATH search list */ +static int pd_cnt = 0; /* number od PATHDIRs in pd_list */ + +#define QFL_NONE 0 +#define QFL_INQ 1 /* inside quotes '...' or "..." */ +#define QFL_MDQ 2 /* missing closing double quote */ +#define QFL_MSQ 3 /* missing closing single quote */ +/* input: completion request data */ +static struct { + CODE type; /* type of completion - one of COMPL_TYPE_XXX */ + const wchar_t *str; /* string to be completed */ + int strlen; /* length of 'str' */ + const wchar_t *dirw;/* directory to attempt the completion in */ + const char *dir; /* multibyte version of 'dirw' if available or 0 */ + int qlevel; /* quote level for names when inserting (one of QUOT_XXX in edit.h) */ + CODE qflags; /* other quoting issues - one of QFL_XXX */ +} rq; + +/* output: completion results */ +static struct { + FLAG filenames; /* names are filenames */ + int cnt; /* number of completion candidates */ + int err; /* errno value (if cnt is zero) */ + size_t clen; /* how many characters following 'str' + are the same for all candidates */ +} compl; +/* output: candidates */ + +static COMPL_ENTRY *cc_list = 0; /* list of all candidates */ +static int cc_alloc = 0; /* max number of candidates in CC_LIST */ + +static FLAG unfinished; /* completion not finished (partial success) */ +extern char **environ; + +/* environment in wchar */ +typedef struct { + const wchar_t *var; + const wchar_t *val; +} ENW; +static ENW *enw; + +static void +path_init(void) +{ + char *path, *p; + int i, list; + + if ( (path = getenv("PATH")) == 0) { + msgout(MSG_NOTICE,"There is no PATH environment variable"); + return; + } + + /* split PATH to components */ + path = estrdup(path); + for (pd_cnt = 1, p = path; *p; p++) + if (*p == ':') { + *p = '\0'; + pd_cnt++; + } + pd_list = emalloc(pd_cnt * sizeof(PATHDIR)); + + for (i = 0, p = path; i < pd_cnt; i++) { + pd_list[i].dir = *p ? p : "."; + pd_list[i].dirw = ewcsdup(convert2w(pd_list[i].dir)); + pd_list[i].timestamp = 0; + for (list = 0; list < LISTS; list++) + pd_list[i].commands[list] = 0; + while (*p++) + ; + } +} + +/* transform the environment into wide char strings */ +void +environ_init(void) +{ + int i; + char *var, *val; + static USTRING buff = UNULL; + + for (i = 0; environ[i] != 0; i++) + ; + enw = emalloc(sizeof(ENW) * (i + 1)); + for (i = 0; environ[i] != 0; i++) { + for (var = val = us_copy(&buff,environ[i]); *val != '\0'; val++) + if (*val == '=') { + *val++ = '\0'; + break; + } + enw[i].var = ewcsdup(convert2w(var)); + enw[i].val = ewcsdup(convert2w(val)); + } + enw[i].var = enw[i].val = 0; +} + +void +compl_initialize(void) +{ + path_init(); + environ_init(); + compl_reconfig(); +} + +void +compl_reconfig(void) +{ + int i; + + if (cc_alloc > 0) { + for (i = 0; i < cc_alloc; i++) + sdw_reset(&cc_list[i].str); + free(cc_list); + free(panel_compl.cand); + } + + cc_alloc = cfg_num(CFG_C_SIZE); + cc_list = emalloc(cc_alloc * sizeof(COMPL_ENTRY)); + panel_compl.cand = emalloc(cc_alloc * sizeof(COMPL_ENTRY *)); + for (i = 0; i < cc_alloc; i++) + SD_INIT(cc_list[i].str); +} + +static int +qcmp(const void *e1, const void *e2) +{ + return (panel_sort.order == SORT_NAME_NUM ? num_wcscoll : wcscoll)( + SDSTR(((COMPL_ENTRY *)e1)->str), + SDSTR(((COMPL_ENTRY *)e2)->str)); +} + +/* simplified version of sort_group() found in sort.c */ +enum FILETYPE_TYPE { + FILETYPE_DIR, FILETYPE_BDEV, FILETYPE_CDEV, FILETYPE_OTHER, FILETYPE_PLAIN +}; +static int +sort_group(int type) +{ + if (IS_FT_PLAIN(type)) + return FILETYPE_PLAIN; + if (IS_FT_DIR(type)) + return FILETYPE_DIR; + if (panel_sort.group == GROUP_DBCOP) { + if (type == FT_DEV_CHAR) + return FILETYPE_CDEV; + if (type == FT_DEV_BLOCK) + return FILETYPE_BDEV; + } + return FILETYPE_OTHER; +} + +static int +qcmp_group(const void *e1, const void *e2) +{ + int cmp, group1, group2; + + group1 = sort_group(((COMPL_ENTRY *)e1)->file_type); + group2 = sort_group(((COMPL_ENTRY *)e2)->file_type); + cmp = group1 - group2; + if (cmp) + return cmp; + return qcmp(e1,e2); +} + +void +compl_panel_data(void) +{ + int i, j; + COMPL_ENTRY *pcc, *curs; + + curs = VALID_CURSOR(panel_compl.pd) ? panel_compl.cand[panel_compl.pd->curs] : 0; + if (panel_compl.pd->filtering) + match_substr_set(panel_compl.pd->filter->line); + + for (i = j = 0; i < compl.cnt; i++) { + pcc = &cc_list[i]; + if (pcc == curs) + panel_compl.pd->curs = j; + if (panel_compl.pd->filtering && !match_substr(SDSTR(pcc->str))) + continue; + panel_compl.cand[j++] = pcc; + } + panel_compl.pd->cnt = j; +} + +int +compl_prepare(void) +{ + const wchar_t *title, *aux; + static wchar_t msg[80]; /* win_sethelp() stores this ptr */ + + if (compl.cnt > cc_alloc) { + swprintf(msg,ARRAY_SIZE(msg),L"%d additional entries not shown (table full)",compl.cnt - cc_alloc); + win_sethelp(HELPMSG_TMP,msg); + compl.cnt = cc_alloc; + } + + if (rq.type != COMPL_TYPE_HIST) + qsort(cc_list,compl.cnt,sizeof(COMPL_ENTRY), + (compl.filenames && panel_sort.group) ? qcmp_group : qcmp); + + aux = 0; + switch (rq.type) { + case COMPL_TYPE_FILE: + title = L"FILENAME COMPLETION"; + break; + case COMPL_TYPE_DIR: + title = L"DIRECTORY NAME COMPLETION"; + break; + case COMPL_TYPE_PATHCMD: + aux = L"found in: "; + /* no break */ + case COMPL_TYPE_CMD: + title = L"COMMAND NAME COMPLETION"; + break; + case COMPL_TYPE_HIST: + title = L"COMMAND COMPLETION FROM HISTORY"; + win_sethelp(HELPMSG_BASE,L"commands are listed in order of their execution"); + break; + case COMPL_TYPE_GROUP: + title = L"GROUP NAME COMPLETION"; + break; + case COMPL_TYPE_USER: + case COMPL_TYPE_USERDIR: + aux = L"name/comment: "; + title = L"USER NAME COMPLETION"; + break; + case COMPL_TYPE_ENV: + case COMPL_TYPE_ENV2: + title = L"ENVIRONMENT VARIABLE COMPLETION"; + aux = L"value: "; + break; + default: + title = L"NAME COMPLETION"; + } + panel_compl.title = title; + panel_compl.aux = aux; + panel_compl.filenames = compl.filenames; + + panel_compl.pd->filtering = 0; + panel_compl.pd->curs = -1; + compl_panel_data(); + panel_compl.pd->top = panel_compl.pd->min; + panel_compl.pd->curs = rq.type == COMPL_TYPE_HIST ? 0 : panel_compl.pd->min; + + panel = panel_compl.pd; + /* textline inherited from previous mode */ + return 0; +} + +static void +register_candidate(const wchar_t *cand, int is_link, int file_type, const wchar_t *aux) +{ + int i; + static const wchar_t *cand0; + + if (rq.type == COMPL_TYPE_PATHCMD) + /* check for duplicates like awk in both /bin and /usr/bin */ + for (i = 0; i < compl.cnt && i < cc_alloc; i++) + if (wcscmp(SDSTR(cc_list[i].str),cand) == 0) + return; + + if (compl.cnt < cc_alloc) { + sdw_copy(&cc_list[compl.cnt].str,cand); + cc_list[compl.cnt].is_link = is_link; + cc_list[compl.cnt].file_type = file_type; + cc_list[compl.cnt].aux = aux; + } + + if (compl.cnt == 0) { + cand0 = SDSTR(cc_list[0].str); /* cand0 = cand; would be an error */ + compl.clen = wcslen(cand0) - rq.strlen; + } + else + for (i = 0; i < compl.clen ; i++) + if (cand[rq.strlen + i] != cand0[rq.strlen + i]) { + compl.clen = i; + break; + } + compl.cnt++; +} + +static void +complete_environ(void) +{ + int i; + + for (i = 0; enw[i].var != 0 ; i++) + if (rq.strlen == 0 || wcsncmp(enw[i].var,rq.str,rq.strlen) == 0) + register_candidate(enw[i].var,0,0,enw[i].val); +} + +static void +complete_history() +{ + int i; + const HIST_ENTRY *ph; + + for (i = 0; (ph = get_history_entry(i)); i++) + if (wcsncmp(USTR(ph->cmd),rq.str,rq.strlen) == 0) + register_candidate(USTR(ph->cmd),0,0, + ph->failed ? L"this command failed last time" : 0); +} + +static void +complete_username(void) +{ + const wchar_t *login, *fullname; + + username_find_init(rq.str,rq.strlen); + while ( (login = username_find(&fullname)) ) + register_candidate(login,0,0,fullname); +} + +static void +complete_groupname(void) +{ + const wchar_t *group; + + groupname_find_init(rq.str,rq.strlen); + while ( (group = groupname_find()) ) + register_candidate(group,0,0,0); +} + +static void +complete_file(void) +{ + FLAG is_link; + CODE type; + const char *path, *dir, *file; + const wchar_t *filew; + struct stat st; + struct dirent *direntry; + DIR *dd; + static USTRING mbdir = UNULL; + + if (rq.dirw == 0) { + /* special case: bare tilde */ + if (wcscmp(rq.str,L"~") == 0) { + register_candidate(L"~",0,FT_DIRECTORY,0); + return; + } + rq.dir = "."; + rq.dirw = L"."; + } + dir = rq.dir ? rq.dir : us_convert2mb(rq.dirw,&mbdir); + if ( (dd = opendir(dir)) == 0) { + compl.err = errno; + return; + } + + win_waitmsg(); + pathname_set_directory(dir); + while ( ( direntry = readdir(dd)) ) { + filew = convert2w(file = direntry->d_name); + if (rq.strlen == 0) { + if (file[0] == '.' && (file[1] == '\0' || (file[1] == '.' && file[2] == '\0'))) + continue; + } + else if (wcsncmp(filew,rq.str,rq.strlen)) + continue; + + if (lstat(path = pathname_join(file),&st) < 0) + continue; /* file just deleted ? */ + if ( (is_link = S_ISLNK(st.st_mode)) && stat(path,&st) < 0) + type = FT_NA; + else + type = stat2type(st.st_mode,st.st_uid); + + if (rq.type == COMPL_TYPE_DIR && !IS_FT_DIR(type)) + continue; /* must be a directory */ + if (rq.type == COMPL_TYPE_CMD && !IS_FT_DIR(type) && !IS_FT_EXEC(type)) + continue; /* must be a directory or executable */ + if (rq.type == COMPL_TYPE_PATHCMD && !IS_FT_EXEC(type)) + continue; /* must be an executable */ + + register_candidate(filew,is_link,type,rq.type == COMPL_TYPE_PATHCMD ? rq.dirw : 0); + } + closedir(dd); +} + +static void +pathcmd_refresh(PATHDIR *ppd) +{ + FLAG stat_ok; + int list; + const wchar_t *filew; + struct dirent *direntry; + struct stat st; + DIR *dd; + CMD *pc; + + /* + * fstat(dirfd()) instead of stat() followed by opendir() would be + * better, but dirfd() is not available on some systems + */ + + stat_ok = stat(ppd->dir,&st) == 0; + if (stat_ok && st.st_mtime < ppd->timestamp + && st.st_dev == ppd->device && st.st_ino == ppd->inode) + return; + + /* clear all command lists */ + for (list = 0; list < LISTS; list++) { + while ( (pc = ppd->commands[list]) ) { + ppd->commands[list] = pc->next; + sd_reset(&pc->cmd); + sdw_reset(&pc->cmdw); + free(pc); + } + } + + ppd->timestamp = time(0); + if (!stat_ok || (dd = opendir(ppd->dir)) == 0) { + ppd->timestamp = 0; + msgout(MSG_NOTICE,"Command name completion routine cannot list " + "directory \"%s\" (member of $PATH): %s",ppd->dir,strerror(errno)); + return; + } + ppd->device = st.st_dev; + ppd->inode = st.st_ino; + + win_waitmsg(); + while ( (direntry = readdir(dd)) ) { + filew = convert2w(direntry->d_name); + list = LIST_NR(*filew); + pc = emalloc(sizeof(CMD)); + SD_INIT(pc->cmd); + SD_INIT(pc->cmdw); + sd_copy(&pc->cmd,direntry->d_name); + sdw_copy(&pc->cmdw,filew); + pc->next = ppd->commands[list]; + ppd->commands[list] = pc; + } + closedir(dd); +} + +static void +complete_pathcmd(void) +{ + FLAG is_link; + CODE file_type; + int i, list; + const char *path; + const wchar_t *filew; + CMD *pc; + PATHDIR *ppd; + struct stat st; + + /* include subdirectories of the current directory */ + rq.type = COMPL_TYPE_DIR; + complete_file(); + rq.type = COMPL_TYPE_PATHCMD; + + list = LIST_NR(*rq.str); + for (i = 0; i < pd_cnt; i++) { + ppd = &pd_list[i]; + if (*ppd->dir == '/') { + /* absolute PATH directories are cached */ + pathcmd_refresh(ppd); + pathname_set_directory(ppd->dir); + for (pc = ppd->commands[list]; pc; pc = pc->next) { + filew = SDSTR(pc->cmdw); + if (wcsncmp(filew,rq.str,rq.strlen) != 0) + continue; + if (lstat(path = pathname_join(SDSTR(pc->cmd)),&st) < 0) + continue; + if ( (is_link = S_ISLNK(st.st_mode)) + && stat(path,&st) < 0) + continue; + file_type = stat2type(st.st_mode,st.st_uid); + if (!IS_FT_EXEC(file_type)) + continue; + register_candidate(filew,is_link,file_type,ppd->dirw); + } + } + else { + /* relative PATH directories are impossible to cache */ + rq.dir = ppd->dir; + rq.dirw = ppd->dirw; + complete_file(); + } + } +} + +static void +reset_results(void) +{ + compl.cnt = 0; + compl.err = 0; + compl.filenames = 0; +} + +static void +complete_it(void) +{ + if (rq.type == COMPL_TYPE_ENV || rq.type == COMPL_TYPE_ENV2) + complete_environ(); + else if (rq.type == COMPL_TYPE_USER || rq.type == COMPL_TYPE_USERDIR) + complete_username(); + else if (rq.type == COMPL_TYPE_GROUP) + complete_groupname(); + else if (rq.type == COMPL_TYPE_HIST) + complete_history(); + else { + compl.filenames = 1; + if (rq.type == COMPL_TYPE_PATHCMD) + complete_pathcmd(); + else + /* FILE, DIR, CMD completion */ + complete_file(); + } +} + +/* insert char 'ch' if it is not already there */ +static void +condinsert(wchar_t ch) +{ + if (USTR(textline->line)[textline->curs] == ch) + textline->curs++; + else + edit_nu_insertchar(ch); +} + + +static void +insert_candidate(COMPL_ENTRY *pcc) +{ + edit_nu_insertstr(SDSTR(pcc->str) + rq.strlen,rq.qlevel); + + if ((compl.filenames && IS_FT_DIR(pcc->file_type)) + || rq.type == COMPL_TYPE_USERDIR /* ~user is a directory */ ) { + unfinished = 1; /* a directory may have subdirectories */ + condinsert(L'/'); + } + else { + if (rq.type == COMPL_TYPE_ENV2) + condinsert(L'}'); + + if (rq.qflags == QFL_INQ) + /* move over the closing quote */ + textline->curs++; + else if (rq.qflags == QFL_MSQ) + edit_nu_insertchar(L'\''); + else if (rq.qflags == QFL_MDQ) + edit_nu_insertchar(L'\"'); + else if (compl.filenames) + condinsert(L' '); + } + + edit_update(); +} + +static const char * +code2string(int type) +{ + switch (type) { + case COMPL_TYPE_FILE: + return "filename"; + case COMPL_TYPE_DIR: + return "directory name"; + case COMPL_TYPE_PATHCMD: + case COMPL_TYPE_CMD: + return "command name"; + case COMPL_TYPE_HIST: + return "command"; + case COMPL_TYPE_GROUP: + return "group name"; + case COMPL_TYPE_USER: + case COMPL_TYPE_USERDIR: + return "user name"; + case COMPL_TYPE_ENV: + case COMPL_TYPE_ENV2: + return "environment variable"; + default: + return "string"; + } +} + +static void +show_results(void) +{ + static SDSTRINGW common = SDNULL(L""); + + if (compl.cnt == 0) { + msgout(MSG_i,"cannot complete this %s (%s)",code2string(rq.type), + compl.err == 0 ? "no match" : strerror(compl.err)); + return; + } + + if (compl.cnt == 1) { + insert_candidate(&cc_list[0]); + return; + } + + if (compl.clen) { + /* insert the common part of all candidates */ + sdw_copyn(&common,SDSTR(cc_list[0].str) + rq.strlen,compl.clen); + edit_insertstr(SDSTR(common),rq.qlevel); + /* + * pretend that the string to be completed already contains + * the chars just inserted + */ + rq.strlen += compl.clen; + } + + control_loop(MODE_COMPL); +} + +#define ISAZ09(CH) ((CH) == L'_' || iswalnum(CH)) + +/* + * compl_name(type) is a completion routine for alphanumerical strings + * type is one of COMPL_TYPE_AUTO, ENV, GROUP, USER + * + * return value: + * 0 = completion process completed (successfully or not) + * -1 = nothing to complete + */ +#define ISUGCHAR(CH) ((CH) == L'.' || (CH) == L',' || (CH) == L'-') +#define ISUGTYPE(T) ((T) == COMPL_TYPE_USER || (T) == COMPL_TYPE_GROUP) +#define TESTAZ09(POS) (lex[POS] == LEX_PLAINTEXT \ + && (ISAZ09(pline[POS]) || (ISUGTYPE(type) && ISUGCHAR(pline[POS])) ) ) +static int +compl_name(int type) +{ + static USTRINGW str_buff = UNULL; + const char *lex; + const wchar_t *pline; + int start, end; + + /* find start and end */ + pline = USTR(textline->line); + lex = cmd2lex(pline); + start = end = textline->curs; + + if (TESTAZ09(start)) + /* complete the name at the cursor */ + while (end++, TESTAZ09(end)) + ; + else if (panel_paste.wordstart || !TESTAZ09(start - 1)) + return -1; /* nothing to complete */ + /* else complete the name immediately before the cursor */ + + if (!panel_paste.wordstart) + while (TESTAZ09(start - 1)) + start--; + + /* set the proper COMPL_TYPE */ + if (type == COMPL_TYPE_AUTO) { + if (lex[start - 1] == LEX_PLAINTEXT && pline[start - 1] == L'~' && !IS_LEX_WORD(lex[start - 2])) + type = COMPL_TYPE_USERDIR; + else if (lex[start - 1] == LEX_VAR) + type = pline[start - 1] == L'{' ? COMPL_TYPE_ENV2 : COMPL_TYPE_ENV; + else + return -1; /* try compl_file() */ + } + else if (type == COMPL_TYPE_ENV && lex[start - 1] == LEX_VAR && pline[start - 1] == L'{') + type = COMPL_TYPE_ENV2; + + /* fill in the 'rq' struct */ + rq.qlevel = QUOT_NONE; + rq.qflags = QFL_NONE; + rq.type = type; + rq.dir = 0; + rq.dirw = 0; + rq.strlen = end - start; + rq.str = usw_copyn(&str_buff,pline + start,rq.strlen); + + /* move cursor to the end of the current word */ + textline->curs = end; + edit_update_cursor(); + + reset_results(); + complete_it(); + show_results(); + return 0; +} + +/* + * compl_file() attempts to complete the partial text in the command line + * type is on of the COMPL_TYPE_AUTO, CMD, DIR, DIRPANEL, HISTORY, FILE, or DRYRUN + * + * return value: + * 0 = completion process completed (successfully or not) + * -1, -2 = nothing to complete (current word is an empty string): + * -1 = first word (usually the command) + * -2 = not the first word (usually one of the arguments) + * -3 = could complete, but not allowed to (COMPL_TYPE_DRYRUN) + */ +static int +compl_file(int type) +{ + static USTRINGW dequote_str = UNULL, dequote_dir = UNULL; + const char *lex; + const wchar_t *p, *pslash, *pstart, *pend, *pline; + wchar_t ch; + int i, start, end, dirlen; + FLAG tilde, wholeline, userdir; + + /* + * pline -> the input line + * pstart -> start of the string to be completed + * pend -> position immediately after the last character of that string + * pslash -> last slash '/' in the string (if any) + */ + pline = USTR(textline->line); + + wholeline = type == COMPL_TYPE_DIRPANEL || type == COMPL_TYPE_HIST; + if (wholeline) { + /* complete the whole line */ + rq.qlevel = QUOT_NONE; + rq.qflags = QFL_NONE; + start = 0; + end = textline->size; + } + else { + /* find the start and end */ + rq.qlevel = QUOT_NORMAL; + rq.qflags = QFL_NONE; + lex = cmd2lex(pline); + start = end = textline->curs; + if (IS_LEX_WORD(lex[start])) { + /* complete the name at the cursor */ + while (end++, IS_LEX_WORD(lex[end])) + ; + } + else if (IS_LEX_WORD(lex[start - 1]) && !panel_paste.wordstart) + ; /* complete the name immediately before the cursor */ + else if ((IS_LEX_CMDSEP(lex[start - 1]) || IS_LEX_SPACE(lex[start - 1]) + || panel_paste.wordstart) && IS_LEX_EMPTY(lex[start])) { + /* there is no text to complete */ + for (i = start - 1; IS_LEX_SPACE(lex[i]); i--) + ; + return IS_LEX_CMDSEP(lex[i]) ? -1 : -2; + } else { + /* the text at the cursor is not a name */ + msgout(MSG_i,"cannot complete a special symbol"); + return 0; + } + + if (type == COMPL_TYPE_DRYRUN) + return -3; + + if (!panel_paste.wordstart) + while (IS_LEX_WORD(lex[start - 1])) + start--; + + for (i = start; i < end; i++) + if (lex[i] == LEX_VAR) { + msgout(MSG_i,"cannot complete a name containing a $variable"); + return 0; + } + + if (type == COMPL_TYPE_AUTO) { + if (lex[start - 1] == LEX_OTHER) { + msgout(MSG_i,"cannot complete after a special symbol"); + return 0; + } + + /* find out what precedes the name to be completed */ + for (i = start - 1; IS_LEX_SPACE(lex[i]); i--) + ; + type = IS_LEX_CMDSEP(lex[i]) ? COMPL_TYPE_CMD : COMPL_TYPE_FILE; + + /* special case - complete file in expressions like + name:file, --opt=file or user@some.host:file */ + if (!panel_paste.wordstart && type == COMPL_TYPE_FILE && lex[i] != LEX_IO) { + for (i = start; i < end; i++) { + if (lex[i] != LEX_PLAINTEXT) + break; + ch = pline[i]; + if (i > start && i < textline->curs && (ch == L':' || ch == L'=')) { + start = i + 1; + if (start == end) + return -2; + break; + } + if (ch != L'.' && ch != L'-' && ch != L'@' && !ISAZ09(ch)) + break; + } + } + } + + if (lex[end] == LEX_END_ERR_SQ) { + rq.qlevel = QUOT_NONE; + rq.qflags = QFL_MSQ; + } + else if (lex[end] == LEX_END_ERR_DQ) { + rq.qlevel = QUOT_IN_QUOTES; + rq.qflags = QFL_MDQ; + } + else if (lex[end - 1] == LEX_QMARK) { + if ((ch = pline[end - 1]) == L'\'') + rq.qlevel = QUOT_NONE; + else if (ch == L'\"') + rq.qlevel = QUOT_IN_QUOTES; + rq.qflags = QFL_INQ; + } + } + pstart = pline + start; + pend = pline + end; + + pslash = 0; + if (type != COMPL_TYPE_HIST) + /* separate the name into the directory part and the file part */ + for (p = pend; p > pstart;) + if (*--p == L'/') { + pslash = p; + break; + } + /* set rq.dirw */ + if (pslash == 0) { + rq.str = pstart; + rq.dirw = 0; + } + else { + rq.str = pslash + 1; + rq.dirw = pstart; + + dirlen = pslash - pstart; + /* dequote 'dir' (if appropriate) + add terminating null byte */ + if (type != COMPL_TYPE_DIRPANEL && isquoted(rq.dirw)) { + tilde = is_dir_tilde(rq.dirw); + dirlen = usw_dequote(&dequote_dir,rq.dirw,dirlen); + } + else { + tilde = *rq.dirw == L'~'; + usw_copyn(&dequote_dir,rq.dirw,dirlen); + } + rq.dirw = USTR(dequote_dir); + if (dirlen == 0) + /* first slash == last slash */ + rq.dirw = L"/"; + else if (tilde) + rq.dirw = dir_tilde(rq.dirw); + } + rq.dir = 0; /* will be converted from rq.dirw on demand */ + + /* set the proper completion type */ + if (type == COMPL_TYPE_DIRPANEL) { + if ( (userdir = *pstart == L'~') ) { + for (i = 1; (ch = pstart[i]) != L'\0' && ch != L'/'; i++) + if (!ISAZ09(ch)) { + userdir = 0; + break; + } + if (textline->curs > i) + userdir = 0; + } + if (userdir) { + rq.str = pstart + 1; + pend = pstart + i; + type = COMPL_TYPE_USERDIR; + } + else + type = COMPL_TYPE_DIR; + } + else if (type == COMPL_TYPE_CMD && pslash == 0) + type = COMPL_TYPE_PATHCMD; + + rq.strlen = pend - rq.str; + /* dequote 'str' (if appropriate) + add terminating null byte */ + if (!wholeline && isquoted(rq.str)) { + rq.strlen = usw_dequote(&dequote_str,rq.str,rq.strlen); + rq.str = USTR(dequote_str); + } + else if (*pend != L'\0') + rq.str = usw_copyn(&dequote_str,rq.str,rq.strlen); + + /* move cursor to the end of the current word */ + textline->curs = (pend - pline) - (rq.qflags == QFL_INQ /* 1 or 0 */); + edit_update_cursor(); + + rq.type = type; + reset_results(); + complete_it(); + show_results(); + return 0; +} + +int +compl_text(int type) +{ + if (textline->size == 0) + return -1; + + if (get_current_mode() != MODE_PASTE) + panel_paste.wordstart = 0; /* valid in the completion/insertion panel only */ + + if (type == COMPL_TYPE_AUTO) + return compl_name(COMPL_TYPE_AUTO) == 0 ? 0 : compl_file(COMPL_TYPE_AUTO); + + if (type == COMPL_TYPE_ENV || type == COMPL_TYPE_GROUP || type == COMPL_TYPE_USER) + return compl_name(type); + + return compl_file(type); +} + +static void +complete_type(int type) +{ + int mode, curs, offset; + + curs = textline->curs; + offset = textline->offset; + mode = get_current_mode(); + + unfinished = 0; + if (compl_text(type) != 0) + msgout(MSG_i,"there is nothing to complete"); + if (unfinished) { + if (mode == MODE_PASTE && panel_paste.wordstart) { + textline->curs = curs; + if (textline->offset != offset) + edit_update_cursor(); + } + } + else if (mode != MODE_FILE) + next_mode = MODE_SPECIAL_RETURN; +} + +void cx_complete_auto(void) { complete_type(COMPL_TYPE_AUTO); } +void cx_complete_file(void) { complete_type(COMPL_TYPE_FILE); } +void cx_complete_dir(void) { complete_type(COMPL_TYPE_DIR); } +void cx_complete_cmd(void) { complete_type(COMPL_TYPE_CMD); } +void cx_complete_user(void) { complete_type(COMPL_TYPE_USER); } +void cx_complete_group(void){ complete_type(COMPL_TYPE_GROUP); } +void cx_complete_env(void) { complete_type(COMPL_TYPE_ENV); } +void cx_complete_hist(void) { complete_type(COMPL_TYPE_HIST); } + +void +cx_compl_wordstart(void) +{ + TOGGLE(panel_paste.wordstart); + win_panel_opt(); +} + +void +cx_compl_enter(void) +{ + insert_candidate(panel_compl.cand[panel_compl.pd->curs]); + next_mode = MODE_SPECIAL_RETURN; +} diff --git a/src/completion.h b/src/completion.h new file mode 100644 index 0000000..b04925a --- /dev/null +++ b/src/completion.h @@ -0,0 +1,26 @@ +#define COMPL_TYPE_AUTO 0 /* autodetect */ +#define COMPL_TYPE_DIRPANEL 1 /* whole textline is one directory name */ +#define COMPL_TYPE_FILE 2 /* any file */ +#define COMPL_TYPE_DIR 3 /* directory */ +#define COMPL_TYPE_CMD 4 /* executable */ +#define COMPL_TYPE_USER 5 /* user name */ +#define COMPL_TYPE_GROUP 6 /* group name */ +#define COMPL_TYPE_ENV 7 /* environment variable */ +#define COMPL_TYPE_HIST 8 /* command history */ +#define COMPL_TYPE_DRYRUN 9 /* NO COMPLETION, just parse the line */ + +extern void compl_initialize(void); +extern void compl_reconfig(void); +extern int compl_prepare(void); +extern void compl_panel_data(void); +extern int compl_text(int); +extern void cx_compl_enter(void); +extern void cx_compl_wordstart(void); +extern void cx_complete_auto(void); +extern void cx_complete_file(void); +extern void cx_complete_dir(void); +extern void cx_complete_cmd(void); +extern void cx_complete_user(void); +extern void cx_complete_group(void); +extern void cx_complete_env(void); +extern void cx_complete_hist(void); diff --git a/src/control.c b/src/control.c new file mode 100644 index 0000000..a9276cb --- /dev/null +++ b/src/control.c @@ -0,0 +1,1138 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include "curses.h" +#include /* log.h */ +#include /* fputs() */ +#include /* exit() */ +#include /* iswcntrl() */ + +#include "control.h" + +#include "bookmarks.h" /* bm_prepare() */ +#include "cmp.h" /* cmp_prepare() */ +#include "cfg.h" /* cfg_prepare() */ +#include "completion.h" /* compl_prepare() */ +#include "directory.h" /* dir_main_prepare() */ +#include "edit.h" /* cx_edit_xxx() */ +#include "filepanel.h" /* cx_files_xxx() */ +#include "filerw.h" /* fw_cleanup() */ +#include "filter.h" /* fopt_prepare() */ +#include "help.h" /* help_prepare() */ +#include "history.h" /* hist_prepare() */ +#include "inout.h" /* win_panel() */ +#include "inschar.h" /* inschar_prepare() */ +#include "log.h" /* vmsgout() */ +#include "mouse.h" /* cx_common_mouse() */ +#include "notify.h" /* notif_prepare() */ +#include "opt.h" /* opt_save() */ +#include "panel.h" /* cx_pan_xxx() */ +#include "preview.h" /* preview_prepare() */ +#include "rename.h" /* rename_prepare() */ +#include "select.h" /* select_prepare() */ +#include "sort.h" /* sort_prepare() */ +#include "string.h" /* strncmp() */ +#include "tty.h" /* tty_reset() */ +#include "undo.h" /* undo_reset() */ +#include "userdata.h" /* user_prepare() */ +#include "xterm_title.h" /* xterm_title_restore() */ + +/* + * PANEL is the main part of the screen, it shows data + * depending on the panel type; the user can scroll through it. + * + * TEXTLINE is a line of text where the user can enter and edit + * his/her input. + * + * KEY_BINDING contains a keystroke and a corresponding function + * to be called every time that key is pressed. All such handler + * function names begin with the cx_ prefix. + * + * CLEX operation mode is defined by a PANEL, TEXTLINE, and a set of + * KEY_BINDING tables. The PANEL and TEXTLINE are initialized by + * a so-called preparation function after each mode change. + * + * Operation mode can be changed in one of these two ways: + * - straightforward transition from mode A to mode B; this + * is achieved by setting the 'next_mode' global variable + * - nesting of modes; this is achieved by calling another + * instance of 'control_loop()'. To go back the variable + * 'next_mode' must be set to MODE_SPECIAL_RETURN. + */ + +typedef struct { + CODE fkey; /* type of the key */ + FLAG escp; /* press escape key first */ + wint_t key; /* if this key was pressed ... */ + void (*fn)(void); /* ... then this function is to be invoked */ + int options; /* option bits - see OPT_XXX below */ +} KEY_BINDING; + +#define OPT_CURS 1 +#define OPT_NOFILT 2 +#define OPT_ALL 4 +/* + * OPT_CURS: call the handler function only if the cursor is on a valid regular line, + * i.e. not on an extra line and not in an empty panel + * extra panel lines: curs < 0 + * regular panel lines: curs >= 0 + * note1: in a panel with extra lines is OPT_CURS not necessary for the key + * (i.e. ctrl-M), because when the cursor is not on a regular panel line, the + * is handled by an EXTRA_LINE function + * note2: see the warning at tab_mainmenu[] ! + * + * OPT_NOFILT: ignore the binding when filtering is active + * + * OPT_ALL: call all handlers for the event, not only the first one + * note: do_action() return value is of limited or no use with OPT_ALL + */ + +#define CXM(X,M) void cx_mode_ ## X (void) { control_loop(MODE_ ## M); } +static CXM(bm,BM) +static CXM(cfg,CFG) +static CXM(cmp,CMP) +static CXM(deselect,DESELECT) +static CXM(dir,DIR) +static CXM(fopt,FOPT) +static CXM(group,GROUP) +static CXM(help,HELP) +static CXM(history,HIST) +static CXM(inschar,INSCHAR) +static CXM(log,LOG) +static CXM(mainmenu,MAINMENU) +static CXM(notif,NOTIF) +static CXM(paste,PASTE) +static CXM(preview,PREVIEW) +static CXM(rename,RENAME) +static CXM(select,SELECT) +static CXM(sort,SORT) +static CXM(user,USER) + +#define CXT(X,M) void cx_trans_ ## X (void) { next_mode = MODE_ ## M; } +static CXT(bm,BM) +static CXT(group,GROUP) +static CXT(user,USER) +static CXT(quit,SPECIAL_QUIT) +static CXT(return,SPECIAL_RETURN) + +static void +cx_trans_discard(void) { + msgout(MSG_i,"Changes discarded"); + next_mode = MODE_SPECIAL_RETURN; +} + +static void noop(void) { ; } + +/* defined below */ +static int menu_prepare(void); +static void cx_menu_pick(void); +static int paste_prepare(void); +static void cx_paste_pick(void); + +#define END_TABLE { 0,0,0,0,0 } + +static KEY_BINDING tab_bm[] = { + { 0, 0, WCH_CTRL('M'), cx_bm_chdir, OPT_NOFILT }, + { 0, 0, WCH_CTRL('C'), cx_bm_revert, OPT_NOFILT }, + { 0, 0, L'd', cx_bm_down, OPT_NOFILT | OPT_CURS }, + { 0, 0, L'n', cx_bm_new, OPT_NOFILT }, + { 0, 0, L'p', cx_bm_edit, OPT_NOFILT | OPT_CURS }, + { 0, 0, L'u', cx_bm_up, OPT_NOFILT | OPT_CURS }, + { 0, 1, L'k', cx_bm_save, 0 }, + { 1, 0, KEY_DC, cx_bm_del, OPT_NOFILT | OPT_CURS }, + END_TABLE +}; + +static KEY_BINDING tab_bm_edit0[] = { + { 0, 0, WCH_CTRL('M'), cx_bm_edit0_enter, OPT_CURS }, + END_TABLE +}; + +static KEY_BINDING tab_bm_edit1[] = { + { 0, 0, WCH_CTRL('M'), cx_bm_edit1_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_bm_edit2[] = { + { 0, 0, WCH_CTRL('I'), cx_bm_edit2_compl, 0 }, + { 0, 0, WCH_CTRL('M'), cx_bm_edit2_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_cfg[] = { + { 0, 0, WCH_CTRL('M'), cx_cfg_enter, 0 }, + { 0, 0, L's', cx_cfg_default, OPT_CURS }, + { 0, 0, L'o', cx_cfg_original, OPT_CURS }, + { 0, 1, L'c', cx_cfg_noexit, 0 }, + { 0, 0, WCH_CTRL('C'), cx_trans_discard, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_cfg_edit_num[] = { + { 0, 0, WCH_CTRL('M'), cx_cfg_num_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_cfg_edit_str[] = { + { 0, 0, WCH_CTRL('M'), cx_cfg_str_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_cfg_menu[] = { + { 0, 0, WCH_CTRL('M'), cx_cfg_menu_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_common[] = { + { 0, 0, WCH_CTRL('C'), cx_trans_return, 0 }, + { 0, 0, WCH_CTRL('F'), cx_filter, 0 }, + { 0, 1, L'c', cx_mode_cfg, 0 }, + { 0, 1, L'l', cx_mode_log, 0 }, + { 0, 1, L'n', cx_mode_notif, 0 }, + { 0, 1, L'o', cx_mode_fopt, 0 }, + { 0, 1, L'q', cx_trans_quit, 0 }, + { 0, 1, L'v', cx_version, 0 }, + { 1, 0, KEY_F(1), cx_mode_help, 0 }, +#ifdef KEY_HELP + { 1, 0, KEY_HELP, cx_mode_help, 0 }, +#endif + END_TABLE +}; + +static KEY_BINDING tab_cmp[] = { + { 0, 0, L' ', cx_cmp, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_cmp, OPT_CURS }, + { 0, 1, L'=', cx_trans_return, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_compl[] = { + { 0, 0, WCH_CTRL('I'), cx_compl_enter, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_compl_enter, OPT_CURS }, + END_TABLE +}; + +static KEY_BINDING tab_compl_sum[] = { + { 0, 0, WCH_CTRL('M'), cx_pan_home, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_dir[] = { + { 0, 1, L'k', cx_trans_bm, 0 }, + { 0, 0, WCH_CTRL('I'), cx_dir_tab, 0 }, + { 0, 0, WCH_CTRL('M'), cx_dir_enter, 0 }, + { 0, 1, L'w', cx_trans_return, 0 }, + { 2, 0, 0, cx_dir_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_edit[] = { + { 0, 1, L'b', cx_edit_w_left, 0 }, +#ifdef KEY_SLEFT + { 1, 0, KEY_SLEFT, cx_edit_w_left, 0 }, +#endif + { 1, 1, KEY_LEFT, cx_edit_w_left, 0 }, + { 0, 1, L'd', cx_edit_w_del, 0 }, + { 0, 1, L'i', cx_mode_inschar, 0 }, + { 0, 1, L'f', cx_edit_w_right, 0 }, +#ifdef KEY_SRIGHT + { 1, 0, KEY_SRIGHT, cx_edit_w_right, 0 }, +#endif + { 1, 1, KEY_RIGHT, cx_edit_w_right, 0 }, + { 0, 1, L't', cx_edit_flipcase, 0 }, + { 1, 0, KEY_BACKSPACE, cx_edit_backsp, 0 }, + { 0, 0, WCH_CTRL('K'), cx_edit_delend, 0 }, + { 0, 0, WCH_CTRL('U'), cx_edit_kill, 0 }, + { 0, 0, WCH_CTRL('V'), cx_edit_inschar, 0 }, + { 0, 0, WCH_CTRL('Z'), cx_undo, 0 }, + { 0, 1, WCH_CTRL('Y'), cx_undo, 0 }, + { 0, 0, WCH_CTRL('Y'), cx_redo, 0 }, + { 0, 1, WCH_CTRL('Z'), cx_redo, 0 }, + { 1, 0, KEY_DC, cx_edit_delchar, 0 }, + { 1, 0, KEY_LEFT, cx_edit_left, 0 }, + { 1, 0, KEY_RIGHT, cx_edit_right, 0 }, + { 1, 0, KEY_HOME, cx_edit_begin, 0 }, + { 1, 0, KEY_END, cx_edit_end, 0 }, + { 1, 1, KEY_UP, cx_edit_up, 0 }, + { 1, 1, KEY_DOWN, cx_edit_down, 0 }, + { 2, 0, 0, cx_edit_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_editcmd[] = { + { 0, 0, WCH_CTRL('A'), cx_edit_paste_path, 0 }, + { 0, 0, WCH_CTRL('E'), cx_edit_paste_dir2, 0 }, + { 0, 1, WCH_CTRL('E'), cx_edit_paste_dir1, 0 }, + { 0, 0, WCH_CTRL('I'), cx_files_tab, 0 }, + { 0, 1, WCH_CTRL('I'), cx_mode_paste, 0 }, + { 0, 0, WCH_CTRL('M'), cx_files_enter, 0 }, + { 0, 1, WCH_CTRL('M'), cx_files_cd, OPT_CURS }, + { 0, 0, WCH_CTRL('N'), cx_hist_next, 0 }, + { 0, 0, WCH_CTRL('O'), cx_edit_paste_link, OPT_CURS }, + { 0, 0, WCH_CTRL('P'), cx_hist_prev, 0 }, + { 0, 1, WCH_CTRL('R'), cx_files_reread_ug, 0 }, + { 0, 0, WCH_CTRL('T'), cx_select_toggle, OPT_CURS }, + { 0, 1, WCH_CTRL('T'), cx_select_range, OPT_CURS }, + { 0, 0, WCH_CTRL('X'), cx_files_xchg, 0 }, + { 0, 1, L'e', cx_mode_preview, OPT_CURS }, + { 0, 1, L'g', cx_mode_group, 0 }, + { 0, 1, L'm', cx_mode_mainmenu, 0 }, + { 0, 1, L'p', cx_complete_hist, 0 }, + { 0, 1, L'r', cx_mode_rename, OPT_CURS }, + { 0, 1, L'x', cx_files_cd_xchg, OPT_CURS }, + { 1, 0, KEY_F(16), cx_mode_mainmenu, 0 }, + { 1, 0, KEY_IC, cx_select_toggle, OPT_CURS }, + { 1, 0, KEY_IL, cx_select_toggle, OPT_CURS }, + { 1, 1, KEY_IC, cx_select_range, OPT_CURS }, + { 1, 1, KEY_IL, cx_select_range, OPT_CURS }, +#ifdef KEY_SIC + { 1, 0, KEY_SIC, cx_select_range, OPT_CURS }, +#endif + { 1, 0, KEY_F(2), cx_edit_cmd_f2, 0 }, + { 1, 0, KEY_F(3), cx_edit_cmd_f3, 0 }, + { 1, 0, KEY_F(4), cx_edit_cmd_f4, 0 }, + { 1, 0, KEY_F(5), cx_edit_cmd_f5, 0 }, + { 1, 0, KEY_F(6), cx_edit_cmd_f6, 0 }, + { 1, 0, KEY_F(7), cx_edit_cmd_f7, 0 }, + { 1, 0, KEY_F(8), cx_edit_cmd_f8, 0 }, + { 1, 0, KEY_F(9), cx_edit_cmd_f9, 0 }, + { 1, 0, KEY_F(10), cx_edit_cmd_f10, 0 }, + { 1, 0, KEY_F(11), cx_edit_cmd_f11, 0 }, + { 1, 0, KEY_F(12), cx_edit_cmd_f12, 0 }, + { 2, 0, 0, cx_files_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_filteredit[] = { + { 0, 0, WCH_CTRL('K'), cx_filteredit_delend, 0 }, + { 0, 0, WCH_CTRL('U'), cx_filteredit_kill, 0 }, + { 0, 0, WCH_CTRL('V'), cx_edit_inschar, 0 }, + { 0, 1, L'i', cx_mode_inschar, 0 }, + { 1, 0, KEY_BACKSPACE, cx_filteredit_backsp, 0 }, + { 1, 0, KEY_DC, cx_filteredit_delchar, 0 }, + { 1, 0, KEY_LEFT, cx_filteredit_left, 0 }, + { 1, 0, KEY_RIGHT, cx_filteredit_right, 0 }, + { 1, 0, KEY_HOME, cx_filteredit_begin, 0 }, + { 1, 0, KEY_END, cx_filteredit_end, 0 }, + { 2, 0, 0, cx_edit_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_fopt[] = { + { 0, 0, L' ', cx_fopt_enter, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_fopt_enter, OPT_CURS }, + { 0, 1, L'o', cx_trans_return, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_group[] = { + { 0, 0, WCH_CTRL('I'), cx_group_paste, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_pan_home, 0 }, + { 0, 1, L'g', cx_trans_return, 0 }, + { 0, 1, L'u', cx_trans_user, 0 }, + { 2, 0, 0, cx_group_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_help[] = { + { 0, 0, WCH_CTRL('M'), cx_help_link, OPT_NOFILT }, + { 0, 1, WCH_CTRL('M'), cx_help_link, 0 }, + { 1, 0, KEY_LEFT, cx_help_back, OPT_NOFILT }, + { 1, 1, KEY_LEFT, cx_help_back, 0 }, + { 1, 0, KEY_RIGHT, cx_help_link, OPT_NOFILT }, + { 1, 1, KEY_RIGHT, cx_help_link, 0 }, + { 1, 0, KEY_BACKSPACE, cx_help_back, OPT_NOFILT }, + { 1, 1, KEY_BACKSPACE, cx_help_back, 0 }, + { 1, 0, KEY_F(1), cx_help_main, 0 }, +#ifdef KEY_HELP + { 1, 0, KEY_HELP, cx_help_main, 0 }, +#endif + { 2, 0, 0, cx_help_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_help_panel[] = { + { 1, 0, KEY_UP, cx_help_up, 0 }, + { 1, 0, KEY_DOWN, cx_help_down, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_hist[] = { + { 1, 1, KEY_DC, cx_hist_del, OPT_CURS }, + { 1, 1, KEY_BACKSPACE, cx_hist_del, OPT_CURS }, + { 0, 0, WCH_CTRL('I'), cx_hist_paste, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_hist_enter, OPT_CURS }, + { 0, 0, WCH_CTRL('N'), cx_pan_up, 0 }, /* redefine history next */ + { 0, 0, WCH_CTRL('P'), cx_pan_down, 0 }, /* redefine history prev */ + { 0, 1, L'h', cx_trans_return, 0 }, + { 2, 0, 0, cx_hist_mouse, OPT_ALL }, + END_TABLE +}; + +/* pseudo-table returned by do_action() */ +static KEY_BINDING tab_insertchar[] = { + END_TABLE +}; + +static KEY_BINDING tab_inschar[] = { + { 0, 0, WCH_CTRL('M'), cx_ins_enter, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_log[] = { + { 0, 0, WCH_CTRL('M'), cx_pan_home, 0 }, + { 1, 0, KEY_LEFT, cx_log_left, OPT_NOFILT }, + { 1, 0, KEY_RIGHT, cx_log_right, OPT_NOFILT }, + { 1, 0, KEY_HOME, cx_log_home, OPT_NOFILT }, + { 0, 0, L'm', cx_log_mark, OPT_NOFILT }, + { 0, 1, L'l', cx_trans_return, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_mainmenu[] = { +/* + * these lines correspond with the main menu panel, + * if you change the menu, you must update also the + * initialization in start.c and descriptions in inout.c + * + * Warning: the OPT_CURS option would check the panel_mainmenu, this is probably not + * what you want ! It can be corrected if need be, hint: VALID_CURSOR(filepanel) + * rather than VALID_CURSOR(menupanel) in callfn()) + */ + /* + * no key for cx_mode_help, note the difference: + * in tab_common: main menu -> help -> main menu + * help in tab_mainmenu: main menu -> help -> file panel + */ + { 0, 0, 0, cx_mode_help, 0 }, + { 0, 1, L'w', cx_mode_dir, 0 }, + { 0, 1, L'/', cx_files_cd_root, 0 }, + { 0, 1, L'.', cx_files_cd_parent, 0 }, + { 0, 1, L'~', cx_files_cd_home, 0 }, + { 0, 1, L'k', cx_mode_bm, 0 }, + { 0, 0, WCH_CTRL('D'), cx_bm_addcwd, 0 }, + { 0, 1, L'h', cx_mode_history, 0 }, + { 0, 1, L's', cx_mode_sort, 0 }, + { 0, 0, WCH_CTRL('R'), cx_files_reread, 0 }, + { 0, 1, L'=', cx_mode_cmp, 0 }, + { 0, 0, 0, cx_filter2, 0 }, /* key in tab_mainmenu2 */ + { 0, 1, L'+', cx_mode_select, 0 }, + { 0, 1, L'-', cx_mode_deselect, 0 }, + { 0, 1, L'*', cx_select_invert, 0 }, + { 0, 0, 0, cx_mode_fopt, 0 }, /* key in tab_common */ + { 0, 1, L'u', cx_mode_user, 0 }, + { 0, 1, L'l', cx_mode_log, 0 }, + { 0, 0, 0, cx_mode_notif, 0 }, /* key in tab_common */ + { 0, 0, 0, cx_mode_cfg, 0 }, /* key in tab_common */ + { 0, 1, L'v', cx_version, 0 }, + { 0, 0, 0, cx_trans_quit, 0 }, /* key in tab_common */ +/* the main menu ends here, the following entries are hidden */ + { 0, 1, L'`', cx_files_cd_home, 0 }, /* like alt-~ but easier to type */ + END_TABLE +}; + +/* main menu keys that are not to be used in the file panel */ +/* this table must correspond with the panel_mainmenu as well ! */ +static KEY_BINDING tab_mainmenu2[] = { + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, WCH_CTRL('F'), cx_filter2, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 1, L'g', cx_mode_group, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, + { 0, 0, 0, noop, 0 }, +/* the main menu ends here, the following entries are hidden */ + { 0, 0, WCH_CTRL('M'), cx_menu_pick, 0 }, + { 0, 1, L'm', cx_trans_return,0 }, + END_TABLE +}; + +/* pseudo-table returned for mouse operations with OPT_ALL */ +static KEY_BINDING tab_mouse[] = { + { 2, 0, 0, cx_common_mouse, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_panel[] = { + { 1, 0, KEY_UP, cx_pan_up, 0 }, +#ifdef KEY_SR + { 1, 0, KEY_SR, cx_pan_up, 0 }, +#endif + { 1, 0, KEY_DOWN, cx_pan_down, 0 }, +#ifdef KEY_SF + { 1, 0, KEY_SF, cx_pan_down, 0 }, +#endif + { 1, 0, KEY_PPAGE, cx_pan_pgup, 0 }, + { 1, 0, KEY_NPAGE, cx_pan_pgdown, 0 }, + { 1, 1, KEY_HOME, cx_pan_home, 0 }, +#ifdef KEY_SHOME + { 1, 0, KEY_SHOME, cx_pan_home, 0 }, +#endif + { 1, 1, KEY_END, cx_pan_end, 0 }, +#ifdef KEY_SEND + { 1, 0, KEY_SEND, cx_pan_end, 0 }, +#endif + { 0, 1, L'z', cx_pan_middle, 0 }, + { 2, 0, 0, cx_pan_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_notif[] = { + { 0, 0, L' ', cx_notif, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_notif, OPT_CURS }, + { 0, 1, L'n', cx_trans_return, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_pastemenu[] = { +/* + * these lines correspond with the paste menu panel, + * if you change this, you must update initialization + * in start.c and descriptions in inout.c + */ + { 0, 0, 0, cx_compl_wordstart, 0 }, + { 0, 0, 0, cx_complete_auto, 0 }, + { 0, 0, 0, cx_complete_file, 0 }, /* no key */ + { 0, 0, 0, cx_complete_dir, 0 }, /* no key */ + { 0, 0, 0, cx_complete_cmd, 0 }, /* no key */ + { 0, 0, 0, cx_complete_user, 0 }, /* no key */ + { 0, 0, 0, cx_complete_group, 0 }, /* no key */ + { 0, 0, 0, cx_complete_env, 0 }, /* no key */ + { 0, 1, 'p', cx_complete_hist, 0 }, + { 1, 0, KEY_F(2), cx_edit_paste_currentfile, 0 }, + { 1, 1, KEY_F(2), cx_edit_paste_filenames, 0 }, + { 0, 0, WCH_CTRL('A'), cx_edit_paste_path, 0 }, + { 0, 0, WCH_CTRL('E'), cx_edit_paste_dir2, 0 }, + { 0, 1, WCH_CTRL('E'), cx_edit_paste_dir1, 0 }, + { 0, 0, WCH_CTRL('O'), cx_edit_paste_link, 0 }, +/* the menu ends here, the following entries are hidden */ + { 0, 0, WCH_CTRL('I'), cx_paste_pick, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_paste_pick, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_preview[] = { + { 0, 0, WCH_CTRL('M'), cx_trans_return, 0 }, + { 2, 0, 0, cx_preview_mouse, OPT_ALL }, + END_TABLE +}; + +static KEY_BINDING tab_rename[] = { + { 0, 0, WCH_CTRL('M'), cx_rename, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_select[] = { + { 0, 0, WCH_CTRL('M'), cx_select_files, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_sort[] = { + { 0, 0, L' ', cx_sort_set, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_sort_set, OPT_CURS }, + { 0, 1, L's', cx_trans_return, 0 }, + { 0, 0, WCH_CTRL('C'), cx_trans_discard, 0 }, + END_TABLE +}; + +static KEY_BINDING tab_user[] = { + { 0, 0, WCH_CTRL('I'), cx_user_paste, OPT_CURS }, + { 0, 0, WCH_CTRL('M'), cx_pan_home, 0 }, + { 0, 1, L'u', cx_trans_return, 0 }, + { 0, 1, L'g', cx_trans_group, 0 }, + { 2, 0, 0, cx_user_mouse, OPT_ALL }, + END_TABLE +}; + +typedef struct { + enum MODE_TYPE mode; + FLAG saveopt; /* save options when returning from this mode */ + const char *helppages[MAIN_LINKS - 1]; /* corresponding help page(s) */ + const wchar_t *title; /* panel title, if not variable */ + const wchar_t *help; /* brief help, 0 if none */ + int (*prepare_fn)(void); + KEY_BINDING *table[4]; /* up to 3 tables terminated with NULL; + order is sometimes important, only + the first KEY_BINDING for a given key + is followed */ +} MODE_DEFINITION; + +/* tab_edit and tab_common are appended automatically */ +static MODE_DEFINITION mode_definition[] = { + { MODE_BM_EDIT0, 0, + { "bookmarks_edit" }, + L"DIRECTORY BOOKMARKS > PROPERTIES", + L" = edit", + bm_edit0_prepare, { tab_panel,tab_bm_edit0,0 } }, + { MODE_BM_EDIT1, 0, + { "bookmarks_edit" }, + L"DIRECTORY BOOKMARKS > PROPERTIES > NAME",0, + bm_edit1_prepare, { tab_bm_edit1,0 } }, + { MODE_BM_EDIT2, 0, + { "bookmarks_edit" }, + L"DIRECTORY BOOKMARKS > PROPERTIES > DIRECTORY", 0, + bm_edit2_prepare, { tab_bm_edit2,0 } }, + { MODE_BM, 0, + { "bookmarks" }, + L"DIRECTORY BOOKMARKS", + L"U/D = up/down, N = new, P = properties, = remove", + bm_prepare, { tab_panel,tab_bm,0 } }, + { MODE_CFG, 0, + { "cfg", "cfg_parameters" }, + L"CONFIGURATION", L" = change, O = original, S = standard", + cfg_prepare, { tab_panel,tab_cfg,0 } }, + { MODE_CFG_EDIT_NUM, 0, + { "cfg" }, + L"CONFIGURATION > EDIT", 0, + cfg_edit_num_prepare, { tab_cfg_edit_num,0 } }, + { MODE_CFG_EDIT_TXT, 0, + { "cfg" }, + L"CONFIGURATION > EDIT", 0, + cfg_edit_str_prepare, { tab_cfg_edit_str,0 } }, + { MODE_CFG_MENU, 0, + { "cfg" }, + L"CONFIGURATION > SELECT", 0, + cfg_menu_prepare, { tab_panel, tab_cfg_menu,0 } }, + { MODE_CMP, 1, + { "compare" }, + L"DIRECTORY COMPARE", 0, + cmp_prepare, { tab_panel,tab_cmp,0 } }, + { MODE_CMP_SUM, 1, + { "summary" }, + L"COMPARISON SUMMARY", 0, + cmp_summary_prepare, { tab_panel,tab_compl_sum,0 } }, + { MODE_COMPL, 0, + { "completion" }, + 0, 0, + compl_prepare, { tab_panel,tab_compl,0 } }, + { MODE_DESELECT, 0, + { "select" }, + L"DESELECT FILES", L"wildcards: ? * and [..], see help", + select_prepare, { tab_panel,tab_select,0 } }, + { MODE_DIR, 0, + { "dir" }, + L"CHANGE WORKING DIRECTORY", L" = insert/complete the directory name", + dir_main_prepare, { tab_panel,tab_dir,0 } }, + { MODE_DIR_SPLIT, 0, + { "dir" }, + L"CHANGE WORKING DIRECTORY", 0, + dir_split_prepare, { tab_panel,tab_dir,0 } }, + { MODE_FILE, 0, + { "file1", "file2", "file3" }, + 0, 0, + files_main_prepare, { tab_panel,tab_editcmd,tab_mainmenu,0 } }, + { MODE_FOPT, 1, + { "filter_opt" }, + L"FILTERING AND PATTERN MATCHING OPTIONS", 0, + fopt_prepare, { tab_panel,tab_fopt,0 } }, + { MODE_GROUP, 0, + { "user" }, + L"GROUP INFORMATION", L" = insert the group name", + group_prepare, { tab_panel,tab_group,0 } }, + { MODE_HELP, 0, + { "help" }, + 0, L"Please report any errors at https://github.com/xitop/clex/issues", + help_prepare, { tab_help_panel,tab_panel,tab_help,0 } }, + { MODE_HIST, 0, + { "history" }, + L"COMMAND HISTORY", L" = insert, = delete", + hist_prepare, { tab_panel,tab_hist,0 } }, + { MODE_INSCHAR, 0, + { "insert" }, + L"EDIT > INSERT SPECIAL CHARACTERS", + L"^X (^ and X) = ctrl-X, DDD = decimal code, \\xHHH or 0xHHH or U+HHH = hex code", + inschar_prepare, { tab_panel,tab_inschar,0 } }, + { MODE_LOG, 0, + { "log" }, + L"PROGRAM LOG", L"<-- and --> = scroll, M = add mark", + log_prepare, { tab_panel,tab_log,0 } }, + { MODE_MAINMENU, 0, + { "menu" }, + L"MAIN MENU", 0, + menu_prepare, { tab_panel,tab_mainmenu,tab_mainmenu2,0 } }, + { MODE_NOTIF, 1, + { "notify" }, + L"NOTIFICATIONS", 0, + notif_prepare, { tab_panel,tab_notif,0 } }, + { MODE_PASTE, 0, + { "paste" }, + L"COMPLETE/INSERT NAME", 0, + paste_prepare, { tab_panel,tab_pastemenu,0 } }, + { MODE_PREVIEW, 0, + { "preview" }, + 0,L" = close preview", + preview_prepare, { tab_panel,tab_preview,0 } }, + { MODE_RENAME, 0, + { "rename" }, + L"RENAME FILE", 0, + rename_prepare, { tab_rename,0 } }, + { MODE_SELECT, 0, + { "select" }, + L"SELECT FILES", L"wildcards: ? * and [..], see help", + select_prepare, { tab_panel,tab_select,0 } }, + { MODE_SORT, 1, + { "sort" }, + L"SORT ORDER", 0, + sort_prepare, { tab_panel,tab_sort,0 } }, + { MODE_USER, 0, + { "user" }, + L"USER INFORMATION", L" = insert the user name", + user_prepare, { tab_panel,tab_user,0 } }, + { 0, 0, {0} , 0, 0, 0, { 0 } } +}; + +/* linked list of all control loop instances */ +struct operation_mode { + MODE_DEFINITION *modedef; + PANEL_DESC *panel; + TEXTLINE *textline; + struct operation_mode *previous; +}; + +static struct operation_mode mode_init = { &mode_definition[ARRAY_SIZE(mode_definition) - 1],0,0,0 }; +static struct operation_mode *clex_mode = &mode_init; + +int +get_current_mode(void) +{ + return clex_mode->modedef->mode; +} + +int +get_previous_mode(void) +{ + return clex_mode->previous->modedef->mode; +} + +void +fopt_change(void) +{ + struct operation_mode *pmode; + + for (pmode = clex_mode; pmode->modedef->mode; pmode = pmode->previous) + if (pmode->panel->filter && pmode->panel->filtering) + pmode->panel->filter->changed = 1; +} + +static MODE_DEFINITION * +get_modedef(int mode) +{ + MODE_DEFINITION *p; + + for (p = mode_definition; p->mode; p++) + if (p->mode == mode) + return p; + + err_exit("BUG: operation mode %d is invalid",mode); + + /* NOTREACHED */ + return 0; +} + +const char ** +mode2help(int mode) +{ + return get_modedef(mode)->helppages; +} + +static KEY_BINDING * +callfn(KEY_BINDING *tab, int idx) +{ + PANEL_DESC *pd; + + if ((tab[idx].options & OPT_CURS) && !VALID_CURSOR(panel)) + return 0; + + /* set cursor for tables that correspond with their respective panels (menu) */ + if ((tab == tab_mainmenu || tab == tab_mainmenu2) && get_current_mode() == MODE_MAINMENU) + pd = panel_mainmenu.pd; + else if (tab == tab_pastemenu) + pd = panel_paste.pd; + else + pd = 0; + if (pd && idx != pd->curs && idx < pd->cnt) { + pd->curs = idx; + pan_adjust(pd); + win_panel_opt(); + } + + (*tab[idx].fn)(); + return tab; +} + +static KEY_BINDING * +do_action(wint_t key, KEY_BINDING **tables) +{ + int i, t1, t2, noesc_idx; + wint_t key_lc; + FLAG fkey, filt; + EXTRA_LINE *extra; + KEY_BINDING *tab, *noesc_tab; + static KEY_BINDING *append[4] = { tab_edit, tab_common, tab_mouse, 0 }; + + fkey = kinp.fkey; + filt = panel->filtering == 1; + + /* key substitutions to simplify the program */ + if (fkey == 1) { + if (key == KEY_ENTER) { + fkey = 0; + key = WCH_CTRL('M'); + } +#ifdef KEY_SUSPEND + /* Ctrl-Z (undo) is sometimes mapped to KEY_SUSPEND, that is undesired */ + else if (key == KEY_SUSPEND) { + fkey = 0; + key = WCH_CTRL('Z'); + } +#endif +#ifdef KEY_UNDO + else if (key == KEY_UNDO) { + fkey = 0; + key = WCH_CTRL('Z'); + } +#endif +#ifdef KEY_REDO + else if (key == KEY_REDO) { + fkey = 0; + key = WCH_CTRL('Y'); + } +#endif + } + else if (fkey == 0) { + if (key == WCH_CTRL('G')) + key = WCH_CTRL('C'); + else if (key == WCH_CTRL('H')) { + key = KEY_BACKSPACE; + fkey = 1; + } + else if (key == '\177') { + /* CURSES should have translated the \177 code. + We are doing it only because losing the DEL key is quite annoying */ + key = disp_data.bs177 ? KEY_BACKSPACE : KEY_DC; + fkey = 1; + } + /* end of key substitutions */ + + /* cancel filter ? */ + if (filt && ((key == WCH_CTRL('M') && !kinp.prev_esc) || key == WCH_CTRL('C'))) { + if (panel->type == PANEL_TYPE_DIR && panel->filter->size > 0) + panel->filtering = 2; + /* not turning it off because of some annoying side effects in the dir panel */ + else { + filter_off(); + filter_help(); + } + return 0; + } + if (panel->filtering == 2 && key == WCH_CTRL('C') && panel->type == PANEL_TYPE_FILE) { + filter_off(); + filter_help(); + return 0; + } + } + + /* extra panel lines */ + if (panel->min < 0 && panel->curs < 0 && ((fkey == 0 && key == WCH_CTRL('M')) || + (fkey == 2 && MI_DC(1) && MI_AREA(PANEL) + && panel->top + minp.ypanel < 0 && panel->top + minp.ypanel == panel->curs))) { + extra = panel->extra + (panel->curs - panel->min); + next_mode = extra->mode_next; + if (extra->fn) + (*extra->fn)(); + return 0; + } + + /* mouse event substitutions */ + if (fkey == 2 && MI_DC(1)) { + /* double click in a panel or on the input line -> enter */ + if (MI_CURSBAR || (MI_AREA(LINE) && textline)) { + key = WCH_CTRL('M'); + fkey = 0; /* kinp.fkey is still 2 */ + } + } + + key_lc = fkey != 0 ? key : towlower(key); + + noesc_tab = 0; + noesc_idx = 0; /* prevents compiler warning */ + for (t1 = t2 = 0; (tab = tables[t1]) || (tab = append[t2]); tables[t1] ? t1++ : t2++) { + if (tab == tab_edit) { + if (filt) + tab = tab_filteredit; + else if (textline == 0) + continue; + } + + for (i = 0; tab[i].fn; i++) + if (fkey == tab[i].fkey && key_lc == tab[i].key + && (!filt || (tab[i].options & OPT_NOFILT) == 0)) { + if (tab[i].options & OPT_ALL) + callfn(tab,i); + else if (kinp.prev_esc && !tab[i].escp) { + /* + * an entry with 'escp' flag has higher priority, + * we must continue to search the tables to see + * if such entry for the given key exists + */ + if (noesc_tab == 0) { + /* accept only the first definition */ + noesc_tab = tab; + noesc_idx = i; + } + } + else if (kinp.prev_esc || !tab[i].escp) + return callfn(tab,i); + } + } + if (noesc_tab) + return callfn(noesc_tab,noesc_idx); + + /* key not found in the tables */ + if (fkey == 0 && !kinp.prev_esc && !iswcntrl(key)) { + if (filt) { + filteredit_insertchar(key); + return tab_filteredit; + } + if (textline) { + edit_insertchar(key); + return tab_insertchar; + } + } + + if (fkey != 2) + msgout(MSG_i,"pressed key has no function "); + return 0; +} + +/* + * main control loop for a selected mode 'mode' + * control loops for different modes are nested whenever necessary + */ +void +control_loop(int mode) +{ + KEY_BINDING *kb_tab; + struct operation_mode current_mode, *pmode; + FLAG filter, nr; + + for (pmode = clex_mode; pmode->modedef->mode; pmode = pmode->previous) + if (pmode->modedef->mode == mode) { + msgout(MSG_i,"The requested panel is already in use"); + return; + } + + current_mode.previous = clex_mode; + /* do not call any function that might call get_current_mode() + while current_mode is in inconsistent state (modedef undefined) */ + clex_mode = ¤t_mode; + /* panel and textline inherited the from previous mode */ + clex_mode->panel = clex_mode->previous->panel; + clex_mode->textline = clex_mode->previous->textline; + + for (next_mode = mode; /* until break */; ) { + clex_mode->modedef = get_modedef(next_mode); + next_mode = 0; + win_sethelp(HELPMSG_BASE,0); + win_sethelp(HELPMSG_TMP,0); + if ((*clex_mode->modedef->prepare_fn)() < 0) + break; + win_sethelp(HELPMSG_BASE,clex_mode->modedef->help); + win_settitle(clex_mode->modedef->title); + win_bar(); + if (panel != clex_mode->panel) { + if (panel->filtering || (clex_mode->panel && clex_mode->panel->filtering)) + win_filter(); + pan_adjust(panel); + win_panel(); + + clex_mode->panel = panel; + } + + if (textline != clex_mode->textline) { + undo_reset(); + edit_adjust(); + win_edit(); + clex_mode->textline = textline; + } + + for (; /* until break */;) { + undo_before(); + kb_tab = do_action(kbd_input(),clex_mode->modedef->table); + undo_after(); + if (next_mode) { + if (next_mode == MODE_SPECIAL_RETURN + && clex_mode->previous->modedef->mode == 0) { + msgout(MSG_i,"to quit CLEX press Q"); + next_mode = 0; + } + else + break; + } + + /* some special handling not implemented with tables */ + switch (clex_mode->modedef->mode) { + case MODE_COMPL: + if (kb_tab == tab_edit || kb_tab == tab_insertchar) + next_mode = MODE_SPECIAL_RETURN; + break; + case MODE_DIR: + case MODE_DIR_SPLIT: + if (textline->size == 0 || kb_tab == tab_panel) + nr = 0; + else if (kb_tab == tab_mouse) + nr = minp.area > AREA_BAR; + else + nr = 1; + if (panel->norev != nr) { + panel->norev = nr; + win_edit(); + win_panel_opt(); + } + break; + case MODE_MAINMENU: + if (kb_tab == tab_mainmenu || kb_tab == tab_mainmenu2) + next_mode = MODE_SPECIAL_RETURN; + break; + default: + ; /* shut-up the compiler */ + } + if (panel->filtering && panel->filter->changed) + filter_update(); + + if (next_mode) + break; + } + + if (next_mode == MODE_SPECIAL_QUIT) + err_exit("Normal exit"); + if (next_mode == MODE_SPECIAL_RETURN) { + if (clex_mode->modedef->saveopt) + opt_save(); + next_mode = 0; + break; + } + } + + clex_mode = clex_mode->previous; + win_bar(); + if (panel != clex_mode->panel) { + filter = panel->filtering || clex_mode->panel->filtering; + panel = clex_mode->panel; + if (filter) + win_filter(); + pan_adjust(panel); /* screen size might have changed */ + win_panel(); + } + if (textline != clex_mode->textline) { + textline = clex_mode->textline; + edit_adjust(); + win_edit(); + } + win_sethelp(HELPMSG_BASE,0); + win_sethelp(HELPMSG_BASE,clex_mode->modedef->help); + win_settitle(clex_mode->modedef->title); +} + +static int +menu_prepare(void) +{ + /* leave cursor position unchanged */ + panel = panel_mainmenu.pd; + textline = 0; + return 0; +} + +static void +cx_menu_pick(void) +{ + (*tab_mainmenu[panel_mainmenu.pd->curs].fn)(); + if (next_mode == 0) + next_mode = MODE_SPECIAL_RETURN; +} + +static int +paste_prepare(void) +{ + /* leave cursor position unchanged */ + panel_paste.wordstart = 0; + panel = panel_paste.pd; + /* textline unchanged */ + return 0; +} + +static void +cx_paste_pick(void) +{ + (*tab_pastemenu[panel_paste.pd->curs].fn)(); +} + +void +cx_version(void) +{ + msgout(MSG_i,"Welcome to CLEX " VERSION " !"); +} + +/* + * err_exit() is the only exit function that terminates CLEX main + * process. It is used for normal (no error) termination as well. + */ +void +err_exit(const char *format, ...) +{ + va_list argptr; + + /* + * all cleanup functions used here: + * - must not call err_exit() + * - must not require initialization + */ + fw_cleanup(); + opt_save(); + xterm_title_restore(); + mouse_restore(); + if (disp_data.curses) + curses_stop(); + tty_reset(); + + fputs("\nTerminating CLEX: ",stdout); + msgout(MSG_AUDIT,"Terminating CLEX, reason is given below"); + msgout(MSG_HEADING,0); + va_start(argptr,format); + vmsgout(MSG_I,format,argptr); + va_end(argptr); + putchar('\n'); + logfile_close(); + + jc_reset(); /* this puts CLEX into background, no terminal I/O possible any more */ + exit(EXIT_SUCCESS); + /* NOTREACHED */ +} diff --git a/src/control.h b/src/control.h new file mode 100644 index 0000000..bbcdf67 --- /dev/null +++ b/src/control.h @@ -0,0 +1,7 @@ +extern void control_loop(int); +extern const char **mode2help(int); +extern int get_current_mode(void); +extern int get_previous_mode(void); +extern void fopt_change(void); +extern void cx_version(void); +extern void err_exit(const char *, ...); diff --git a/src/convert.sed b/src/convert.sed new file mode 100644 index 0000000..5106c31 --- /dev/null +++ b/src/convert.sed @@ -0,0 +1,5 @@ +/^#/d +/^$V=/d +s/\([\\'"]\)/\\\1/g +s/^/"/ +s/$/",/ diff --git a/src/curses.h b/src/curses.h new file mode 100644 index 0000000..bf0ae6a --- /dev/null +++ b/src/curses.h @@ -0,0 +1,13 @@ +#ifdef HAVE_NCURSESW_H +# include +#elif defined HAVE_NCURSES_H +# include +#elif defined HAVE_CURSESW_H +# include +#elif defined HAVE_CURSES_H +# include +#endif + +#ifndef NCURSES_MOUSE_VERSION +# define NCURSES_MOUSE_VERSION 0 +#endif diff --git a/src/directory.c b/src/directory.c new file mode 100644 index 0000000..126f34e --- /dev/null +++ b/src/directory.c @@ -0,0 +1,471 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ +#include /* qsort() */ +#include /* strcmp() */ + +#include "directory.h" + +#include "cfg.h" /* cfg_num() */ +#include "completion.h" /* compl_text() */ +#include "control.h" /* get_current_mode() */ +#include "edit.h" /* edit_setprompt() */ +#include "filepanel.h" /* changedir() */ +#include "filter.h" /* cx_filter() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr() */ +#include "mbwstring.h" /* convert2w() */ +#include "panel.h" /* pan_adjust() */ +#include "userdata.h" /* dir_tilde() */ +#include "util.h" /* emalloc() */ + +/* + * The directory list 'dirlist' maintained here contains: + * a) the names of recently visited directories, it is + * a source for the directory panel + * b) the last cursor position in all those directories, allowing + * to restore the cursor position when the user returns to + * a directory previously visited + */ + +/* additional entries to be allocated when the 'dirlist' table is full */ +#define SAVEDIR_ALLOC_UNIT 32 +#define SAVEDIR_ALLOC_MAX 384 /* 'dirlist' size limit */ + +typedef struct { + USTRING dirname; /* directory name */ + USTRINGW dirnamew; /* wide char version (converted on demand) */ + SDSTRING savefile; /* file panel's current file */ + int savetop, savecurs; /* top line, cursor line */ +} SAVEDIR; + +static SAVEDIR **dirlist; /* list of visited directories */ +static int dir_alloc = 0; /* number of existing entries in 'dirlist' */ +static int dir_cnt = 0; /* number of used entries in 'dirlist' */ + +/* directory panel's data is built from 'dirlist' */ +#define DP_LIST (panel_dir.dir) +static int dp_alloc = 0; /* number of existing entries in the dir panel */ +static int dp_max; /* max number of entries to be used */ + +void +dir_initialize(void) +{ + edit_setprompt(&line_dir,L"Change directory: "); + dir_reconfig(); +} + +void +dir_reconfig(void) +{ + dp_max = cfg_num(CFG_D_SIZE); + if (dp_max == 0) /* D_PANEL_SIZE = AUTO */ + dp_max = 100; /* should be enough for every screen */ + + if (dp_max > dp_alloc) { + efree(DP_LIST); + dp_alloc = dp_max; + DP_LIST = emalloc(dp_alloc * sizeof(DIR_ENTRY)); + } +} + +/* + * length of the common part of two directory names (FQDNs) + * (this is not the same as common part of two strings) + */ +static size_t +common_part(const char *dir1, const char *dir2) +{ + char ch1, ch2; + size_t i, slash; + + for (i = slash = 0; /* until return */; i++) { + ch1 = dir1[i]; + ch2 = dir2[i]; + if (ch1 == '\0') + return ch2 == '/' || ch2 == '\0' ? i : slash; + if (ch2 == '\0') + return ch1 == '/' ? i : slash; + if (ch1 != ch2) + return slash; + if (ch1 == '/') + slash = i; + } +} + +static size_t +common_part_w(const wchar_t *dir1, const wchar_t *dir2) +{ + wchar_t ch1, ch2; + size_t i, slash; + + for (i = slash = 0; /* until return */; i++) { + ch1 = dir1[i]; + ch2 = dir2[i]; + if (ch1 == L'\0') + return ch2 == L'/' || ch2 == L'\0' ? i : slash; + if (ch2 == L'\0') + return ch1 == L'/' ? i : slash; + if (ch1 != ch2) + return slash; + if (ch1 == L'/') + slash = i; + } +} + +/* + * check the relationship of two directories (FQDNs) + * return -1: dir2 is subdir of dir1 (/dir2 = /dir1 + /sub) + * return +1: dir1 is subdir of dir2 (/dir1 = /dir2 + /sub) + * return 0: otherwise + */ +static int +check_subdir(const char *dir1, const char *dir2) +{ + size_t slash; + + slash = common_part(dir1,dir2); + return (dir2[slash] == '\0') - (dir1[slash] == '\0'); +} + +int +dir_cmp(const char *dir1, const char *dir2) +{ + static USTRING cut1 = UNULL, cut2 = UNULL; + size_t slash, len1, len2; + + /* skip the common part */ + slash = common_part(dir1,dir2); + if (dir1[slash] == '\0' && dir2[slash] == '\0') + return 0; + if (dir1[slash] == '\0') + return -1; + if (dir2[slash] == '\0') + return 1; + + /* compare one directory component */ + for (dir1 += slash + 1, len1 = 0; dir1[len1] != '/' && dir1[len1] != '\0'; len1++) + ; + for (dir2 += slash + 1, len2 = 0; dir2[len2] != '/' && dir2[len2] != '\0'; len2++) + ; + return strcoll(us_copyn(&cut1,dir1,len1),us_copyn(&cut2,dir2,len2)); +} + +/* + * comparison function for qsort() - comparing directories + * (this is not the same as comparing strings) + */ +static int +qcmp(const void *e1, const void *e2) +{ + return dir_cmp(((DIR_ENTRY *)e1)->name,((DIR_ENTRY *)e2)->name); +} + +/* build the directory panel from the 'dirlist' */ +#define NO_COMPACT 5 /* preserve top 5 directory names */ +void +dir_panel_data(void) +{ + int i, j, cnt, sub; + FLAG store; + const char *dirname; + const wchar_t *dirnamew; + + /* D_PANEL_SIZE = AUTO */ + if (cfg_num(CFG_D_SIZE) == 0) { + /* + * substract extra lines and leave the bottom + * line empty to show there is no need to scroll + */ + dp_max = disp_data.panlines + panel_dir.pd->min - 1; + LIMIT_MAX(dp_max,dp_alloc); + } + + if (panel_dir.pd->filtering) + match_substr_set(panel_dir.pd->filter->line); + + for (i = cnt = 0; i < dir_cnt; i++) { + if (cnt == dp_max) + break; + dirname = USTR(dirlist[i]->dirname); + dirnamew = USTR(dirlist[i]->dirnamew); + if (dirnamew == 0) + dirnamew = usw_convert2w(dirname,&dirlist[i]->dirnamew); + if (panel_dir.pd->filtering && !match_substr(dirnamew)) + continue; + /* compacting */ + store = 1; + if (i >= NO_COMPACT) + for (j = 0; j < cnt; j++) { + sub = check_subdir(dirname,DP_LIST[j].name); + if (sub == -1) { + store = 0; + break; + } + if (sub == 1 && j >= NO_COMPACT) { + DP_LIST[j].name = dirname; + DP_LIST[j].namew = dirnamew; + store = 0; + break; + } + } + if (store) { + DP_LIST[cnt].name = dirname; + DP_LIST[cnt].namew = dirnamew; + cnt++; + } + } + + qsort(DP_LIST,cnt,sizeof(DIR_ENTRY),qcmp); + + /* + * Two lines like these: + * /aaa/bbb/111 + * /aaa/bbb/2222 + * are displayed as: + * /aaa/bbb/111 + * __/2222 + * and for that purpose length 'shlen' is computed + * |<---->| + */ + DP_LIST[0].shlen = 0; + for (i = 1; i < cnt; i++) + DP_LIST[i].shlen = common_part_w(DP_LIST[i].namew,DP_LIST[i - 1].namew); + + panel_dir.pd->cnt = cnt; +} + +int +dir_main_prepare(void) +{ + int i; + const char *prevdir; + + panel_dir.pd->filtering = 0; + dir_panel_data(); + panel_dir.pd->norev = 0; + panel_dir.pd->top = panel_dir.pd->min; + + /* set cursor to previously used directory */ + panel_dir.pd->curs = 0; + /* note: dirlist is never empty, + the current dir is always on the top (dirlist[0]); + prevdir (if exists) is next (dirlist[1]) */ + prevdir = USTR(dirlist[(dir_cnt > 1) /* [1] or [0] */ ]->dirname); + for (i = 0; i < panel_dir.pd->cnt; i++) + if (DP_LIST[i].name == prevdir) { + panel_dir.pd->curs = i; + break; + } + panel = panel_dir.pd; + textline = &line_dir; + edit_nu_kill(); + return 0; +} + +const char * +dir_split_dir(int pos) +{ + int level; + char *p; + static USTRING copy = UNULL; + + if (pos <= 0) + return panel_dir_split.name; + level = panel_dir_split.pd->cnt - pos - 1; + if (level <= 0) + return "/"; + for (p = us_copy(©,panel_dir_split.name); /* until return */ ;) + if (*++p == '/' && --level == 0) { + *p = '\0'; + return USTR(copy); + } +} + +int +dir_split_prepare(void) +{ + int i, cnt; + const char *dirname; + + dirname = panel_dir_split.name = DP_LIST[panel_dir.pd->curs].name; + for (cnt = i = 0; dirname[i]; i++) + if (dirname[i] == '/') + cnt++; + if (i > 1) + cnt++; /* difference between "/" and "/dir" */ + panel_dir_split.pd->cnt = cnt; + panel_dir_split.pd->top = panel_dir_split.pd->min; + panel_dir_split.pd->curs = 0; + panel_dir_split.pd->norev = 0; + + panel = panel_dir_split.pd; + /* textline inherited */ + return 0; +} + +/* + * save the current directory name and the current cursor position + * in the file panel to 'dirlist' + */ +void +filepos_save(void) +{ + int i; + FLAG new = 1; + SAVEDIR *storage, *x, *top; + const char *dir; + + if (dir_cnt == dir_alloc && dir_alloc < SAVEDIR_ALLOC_MAX) { + dir_alloc += SAVEDIR_ALLOC_UNIT; + dirlist = erealloc(dirlist,dir_alloc * sizeof(SAVEDIR *)); + storage = emalloc(SAVEDIR_ALLOC_UNIT * sizeof(SAVEDIR)); + for (i = 0; i < SAVEDIR_ALLOC_UNIT; i++) { + US_INIT(storage[i].dirname); + US_INIT(storage[i].dirnamew); + SD_INIT(storage[i].savefile); + dirlist[dir_cnt + i] = storage + i; + } + } + + dir = USTR(ppanel_file->dir); + for (top = dirlist[0], i = 0; i < dir_alloc; i++) { + x = dirlist[i]; + dirlist[i] = top; + top = x; + if (i == dir_cnt) { + dir_cnt++; + break; + } + if (strcmp(USTR(top->dirname),dir) == 0) { + /* avoid duplicates */ + new = 0; + break; + } + } + if (new) { + us_copy(&top->dirname,dir); + usw_reset(&top->dirnamew); + } + + if (ppanel_file->pd->cnt) { + sd_copy(&top->savefile,SDSTR(ppanel_file->files[ppanel_file->pd->curs]->file)); + top->savecurs = ppanel_file->pd->curs; + top->savetop = ppanel_file->pd->top; + } + else if (new) { + sd_copy(&top->savefile,".."); + top->savecurs = 0; + top->savetop = 0; + } + dirlist[0] = top; +} + +/* set the file panel cursor according to data stored in 'dirlist' */ +void +filepos_set(void) +{ + char *dir; + int i, line; + SAVEDIR *pe; + + if (ppanel_file->pd->cnt == 0) + return; + + dir = USTR(ppanel_file->dir); + for (i = 0; i < dir_cnt; i++) { + pe = dirlist[i]; + if (strcmp(dir,USTR(pe->dirname)) == 0) { + /* found */ + line = file_find(SDSTR(pe->savefile)); + ppanel_file->pd->curs = line >= 0 ? line : pe->savecurs; + ppanel_file->pd->top = pe->savetop; + pan_adjust(ppanel_file->pd); + return; + } + } + + /* not found */ + line = file_find(".."); + ppanel_file->pd->curs = line >= 0 ? line : 0; + ppanel_file->pd->top = ppanel_file->pd->min; + pan_adjust(ppanel_file->pd); +} + +/* following cx_dir_xxx functions are used in both MODE_DIR_XXX modes */ + +static void +dir_paste(void) +{ + const wchar_t *dir; + + dir = + get_current_mode() == MODE_DIR_SPLIT + ? convert2w(dir_split_dir(panel_dir_split.pd->curs)) + : DP_LIST[panel_dir.pd->curs].namew; + edit_nu_insertstr(dir,QUOT_NONE); + if (dir[1]) + edit_nu_insertchar(L'/'); + edit_update(); + if (panel->filtering == 1) + cx_filter(); +} + +void +cx_dir_tab(void) +{ + if (textline->size) + compl_text(COMPL_TYPE_DIRPANEL); + else if (panel->curs >= 0) + dir_paste(); +} + +void +cx_dir_mouse(void) +{ + if (textline->size == 0 && MI_PASTE) + dir_paste(); +} + +void +cx_dir_enter(void) +{ + const char *dir; + + if (panel->norev) { + /* focus on the input line */ + dir = convert2mb(dir_tilde(USTR(textline->line))); + if (changedir(dir) == 0) + next_mode = MODE_SPECIAL_RETURN; + else if (dir[0] == ' ' || dir[strlen(dir) - 1] == ' ') + msgout(MSG_i,"check the spaces before/after the directory name"); + return; + } + + /* focus on the panel (i.e. ignore the input line) */ + if (kinp.fkey == 2 && !MI_AREA(PANEL)) + return; + if (panel->curs < 0) + return; /* next_mode is set by the EXTRA_LINE table */ + + if (textline->size) + cx_edit_kill(); + if (get_current_mode() == MODE_DIR) + next_mode = MODE_DIR_SPLIT; + else if (changedir(dir_split_dir(panel_dir_split.pd->curs)) == 0) + next_mode = MODE_SPECIAL_RETURN; +} diff --git a/src/directory.h b/src/directory.h new file mode 100644 index 0000000..4729e84 --- /dev/null +++ b/src/directory.h @@ -0,0 +1,12 @@ +extern void dir_initialize(void); +extern void dir_reconfig(void); +extern int dir_main_prepare(void); +extern void dir_panel_data(void); +extern int dir_split_prepare(void); +extern const char *dir_split_dir(int); +extern int dir_cmp(const char *, const char *); +extern void filepos_save(void); +extern void filepos_set(void); +extern void cx_dir_tab(void); +extern void cx_dir_enter(void); +extern void cx_dir_mouse(void); diff --git a/src/edit.c b/src/edit.c new file mode 100644 index 0000000..990f674 --- /dev/null +++ b/src/edit.c @@ -0,0 +1,781 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ +#include /* strcmp() */ +#include /* iswlower() */ + +#include "edit.h" + +#include "cfg.h" /* cfg_str() */ +#include "control.h" /* get_current_mode() */ +#include "filter.h" /* cx_filter() */ +#include "filepanel.h" /* cx_files_enter() */ +#include "inout.h" /* win_edit() */ +#include "history.h" /* hist_reset_index() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* wc_cols() */ +#include "util.h" /* jshash() */ + +static FLAG cmd_auto = 0; /* $! control sequence */ + +/* adjust 'offset' so the cursor is visible */ +/* returns 1 if offset has changed, otherwise 0 */ +#define OFFSET_STEP 16 +int +edit_adjust(void) +{ + int old_offset, offset, screen, curs, pwidth, cols, o2c; + wchar_t *str; + + if (textline == 0) + return 0; + + offset = textline->offset; + curs = textline->curs; + pwidth = textline->promptwidth; + str = USTR(textline->line); + + if (curs > 0 && curs < textline->size) { + /* composed UTF-8 characters: place the cursor on the base character */ + while (curs > 0 && utf_iscomposing(str[curs])) + curs--; + textline->curs = curs; + } + + /* + * screen = number of display columns available, + * substract columns possibly left unused because of double-width chars + * not fitting at the right border, reserved position for the '>' mark, + * and the bottom right corner position that could cause a scroll + */ + screen = (disp_data.scrcols - 1) * disp_data.cmdlines - 2; + + if (offset <= curs && wc_cols(str,offset,curs) < screen - (offset ? 1 : pwidth)) + /* no need for a change */ + return 0; + + old_offset = offset; + if (offset && wc_cols(str,0,curs) < screen - pwidth) { + /* 1. zero offset is preferred whenever possible */ + textline->offset = 0; + return 1; + } + /* o2c = desired width in columns from the offset to the cursor */ + if (wc_cols(str,curs,-1) < screen - 1) + /* 2. if EOL is visible, minimize the unused space after the EOL */ + o2c = screen - 1; + else if (curs < offset) + /* 3. if the cursor appears to move left, put it on the first line */ + o2c = disp_data.scrcols - 1; + else + /* 4. if the cursor appears to move right, put it on the last line */ + o2c = (disp_data.cmdlines - 1) * disp_data.scrcols + OFFSET_STEP; + + /* estimate the offset, then fine adjust it in few iteration rounds */ + offset = curs <= o2c ? 0 : (curs - o2c) / OFFSET_STEP * OFFSET_STEP; + cols = wc_cols(str,offset,curs); + while (offset > OFFSET_STEP && cols < o2c) { + cols += wc_cols(str,offset - OFFSET_STEP,offset); + offset -= OFFSET_STEP; + } + while (offset == 0 || cols >= o2c) { + cols -= wc_cols(str,offset,offset + OFFSET_STEP); + offset += OFFSET_STEP; + } + return old_offset != (textline->offset = offset); +} + +/* make changes to 'textline' visible on the screen */ +void +edit_update(void) +{ + edit_adjust(); + win_edit(); +} + +/* + * if you have only moved the cursor, use this optimized + * version of edit_update() instead + */ +void +edit_update_cursor(void) +{ + if (edit_adjust()) + win_edit(); +} + +/* + * edit_islong() can be called after win_edit() and it returns 0 + * if the entire 'textline' is displayed from the beginning to the end, + * or 1 if it does not fit due to excessive length + */ +int +edit_islong(void) +{ + return textline->offset || sum_linechars() < textline->size; +} + +int +edit_isauto(void) +{ + return cmd_auto; +} + +void +cx_edit_begin(void) +{ + textline->curs = 0; + edit_update_cursor(); +} + +void +cx_edit_end(void) +{ + textline->curs = textline->size; + edit_update_cursor(); +} + +/* + * "_nu_" means "no update" version, the caller is responsible + * for calling the update function edit_update(). + * + * The point is to invoke several _nu_ functions and then make the 'update' just once. + * + * Note: The edit_update() consists of edit_adjust() followed by * win_edit(). + * If the 'offset' is fine (e.g. after edit_nu_kill), the edit_adjust() may be skipped. + */ +static void +edit_nu_left(void) +{ + FLAG move = 1; + + /* composed UTF-8 characters: move the cursor also over combining chars */ + while (move && textline->curs > 0) + move = utf_iscomposing(USTR(textline->line)[--textline->curs]); +} + +void +cx_edit_left(void) +{ + edit_nu_left(); + edit_update_cursor(); +} + +static void +edit_nu_right(void) +{ + FLAG move = 1; + + /* composed UTF-8 characters: move the cursor also over combining chars */ + while (move && textline->curs < textline->size) + move = utf_iscomposing(USTR(textline->line)[++textline->curs]); +} + +void +cx_edit_right(void) +{ + edit_nu_right(); + edit_update_cursor(); +} + +void +cx_edit_up(void) +{ + int width, curs; + wchar_t *line; + + line = USTR(textline->line); + width = disp_data.scrcols; + curs = textline->curs; + while (curs > 0 && width > 0) { + curs--; + width -= WCW(line[curs]); + } + textline->curs = curs; + edit_update_cursor(); +} + +void +cx_edit_down(void) +{ + int width, curs; + wchar_t *line; + + line = USTR(textline->line); + width = disp_data.scrcols; + curs = textline->curs; + while (curs < textline->size && width > 0) { + curs++; + width -= WCW(line[curs]); + } + textline->curs = curs; + edit_update_cursor(); +} + +static wchar_t +wordsep(void) +{ + if (textline == &line_cmd) + return L' '; + if (textline == &line_dir) + return L'/'; + return get_current_mode() == MODE_BM_EDIT2 ? L'/' : L' '; +} + +/* move one word left */ +void +cx_edit_w_left(void) +{ + const wchar_t *line; + wchar_t wsep; + int curs; + + wsep = wordsep(); + if ((curs = textline->curs) > 0) { + line = USTR(textline->line); + while (curs > 0 && line[curs - 1] == wsep) + curs--; + while (curs > 0 && line[curs - 1] != wsep) + curs--; + textline->curs = curs; + edit_update_cursor(); + } +} + +/* move one word right */ +void +cx_edit_w_right(void) +{ + const wchar_t *line; + wchar_t wsep; + int curs; + + wsep = wordsep(); + if ((curs = textline->curs) < textline->size) { + line = USTR(textline->line); + while (curs < textline->size && line[curs] != wsep) + curs++; + while (curs < textline->size && line[curs] == wsep) + curs++; + textline->curs = curs; + edit_update_cursor(); + } +} + +void +cx_edit_mouse(void) +{ + int mode; + + if (!MI_AREA(LINE)) + return; + if (!MI_CLICK && !MI_WHEEL) + return; + + if (textline->size) { + if (MI_CLICK) { + if (minp.cursor < 0) + return; + textline->curs = minp.cursor; + } + else { + mode = get_current_mode(); + if (mode == MODE_SELECT || mode == MODE_DESELECT || mode == MODE_CFG_EDIT_NUM) + MI_B(4) ? cx_edit_left() : cx_edit_right(); + else + MI_B(4) ? cx_edit_w_left() : cx_edit_w_right(); + } + } + if (panel->filtering == 1) + cx_filter(); +} + +void +edit_nu_kill(void) +{ + if (textline == &line_cmd) + hist_reset_index(); + + textline->curs = textline->size = textline->offset = 0; + /* + * usw_copy() is called to possibly shrink the allocated memory block, + * other delete functions don't do that + */ + usw_copy(&textline->line,L""); + disp_data.noenter = 0; +} + +void +cx_edit_kill(void) +{ + edit_nu_kill(); + win_edit(); +} + +/* delete 'cnt' chars at cursor position */ +static void +delete_chars(int cnt) +{ + int i; + wchar_t *line; + + line = USTR(textline->line); + textline->size -= cnt; + for (i = textline->curs; i <= textline->size; i++) + line[i] = line[i + cnt]; +} + +void +cx_edit_backsp(void) +{ + int pos; + FLAG del = 1; + + if (textline->curs == 0) + return; + + pos = textline->curs; + /* composed UTF-8 characters: delete also the combining chars */ + while (del && textline->curs > 0) + del = utf_iscomposing(USTR(textline->line)[--textline->curs]); + delete_chars(pos - textline->curs); + edit_update(); +} + +void +cx_edit_delchar(void) +{ + int pos; + FLAG del = 1; + + if (textline->curs == textline->size) + return; + + pos = textline->curs; + /* composed UTF-8 characters: delete also the combining chars */ + while (del && textline->curs < textline->size) + del = utf_iscomposing(USTR(textline->line)[++pos]); + delete_chars(pos - textline->curs); + edit_update(); +} + +/* delete until the end of line */ +void +cx_edit_delend(void) +{ + USTR(textline->line)[textline->size = textline->curs] = L'\0'; + edit_update(); +} + +/* delete word */ +void +cx_edit_w_del(void) +{ + int eow; + wchar_t *line; + + eow = textline->curs; + line = USTR(textline->line); + if (line[eow] == L' ' || line[eow] == L'\0') + return; + + while (textline->curs > 0 && line[textline->curs - 1] != L' ') + textline->curs--; + while (eow < textline->size && line[eow] != L' ') + eow++; + while (line[eow] == L' ') + eow++; + delete_chars(eow - textline->curs); + edit_update(); +} + +/* make room for 'cnt' chars at cursor position */ +static wchar_t * +insert_space(int cnt) +{ + int i; + wchar_t *line, *ins; + + usw_resize(&textline->line,textline->size + cnt + 1); + line = USTR(textline->line); + ins = line + textline->curs; /* insert new character(s) here */ + textline->size += cnt; + textline->curs += cnt; + for (i = textline->size; i >= textline->curs; i--) + line[i] = line[i - cnt]; + + return ins; +} + +void +edit_nu_insertchar(wchar_t ch) +{ + *insert_space(1) = ch; +} + +void +edit_insertchar(wchar_t ch) +{ + edit_nu_insertchar(ch); + edit_update(); +} + +void +edit_nu_putstr(const wchar_t *str) +{ + usw_copy(&textline->line,str); + textline->curs = textline->size = wcslen(str); +} + +void +edit_putstr(const wchar_t *str) +{ + edit_nu_putstr(str); + edit_update(); +} + +/* + * returns number of quoting characters required: + * 2 = single quotes 'X' (newline) + * 1 = backslash quoting \X (shell metacharacters) + * 0 = no quoting (regular characters) + * in other words: it returns non-zero if 'ch' is a special character + */ +int +edit_isspecial(wchar_t ch) +{ + if (ch == WCH_CTRL('J') || ch == WCH_CTRL('M')) + return 2; /* 2 extra characters: X -> 'X' */ + /* built-in metacharacters (let's play it safe) */ + if (wcschr(L"\t ()<>[]{}#$&\\|?*;\'\"`~",ch)) + return 1; /* 1 extra character: X -> \X */ + /* C-shell */ + if (user_data.shelltype == SHELL_CSH && (ch == L'!' || ch == L':')) + return 1; + /* additional special characters to be quoted */ + if (*cfg_str(CFG_QUOTE) && wcschr(cfg_str(CFG_QUOTE),ch)) + return 1; + + return 0; +} + +/* return value: like edit_isspecial() */ +static int +how_to_quote(wchar_t ch, int qlevel) +{ + if (qlevel == QUOT_NORMAL) { + /* full quoting */ + if (ch == L'=' || ch == L':') + /* not special, but quoted to help the name completion */ + return 1; + return edit_isspecial(ch); + } + if (qlevel == QUOT_IN_QUOTES) { + /* inside double quotes */ + if (ch == L'\"' || ch == L'\\' || ch == L'$' || ch == L'`') + return 1; + } + return 0; +} + +void +edit_nu_insertstr(const wchar_t *str, int qlevel) +{ + int len, mch; + wchar_t ch, *ins; + + for (len = mch = 0; (ch = str[len]) != L'\0'; len++) + if (qlevel != QUOT_NONE) + mch += how_to_quote(ch,qlevel); + if (len == 0) + return; + + ins = insert_space(len + mch); + while ((ch = *str++) != L'\0') { + if (qlevel != QUOT_NONE) { + mch = how_to_quote(ch,qlevel); + if (mch == 2) { + *ins++ = L'\''; + *ins++ = ch; + ch = L'\''; + } + else if (mch == 1) + *ins++ = L'\\'; + } + *ins++ = ch; + } + + /* + * if there is no quoting this is equivalent of: + * len = wcslen(str); wcsncpy(insert_space(len),src,len); + */ +} + +void +edit_insertstr(const wchar_t *str, int qlevel) +{ + edit_nu_insertstr(str,qlevel); + edit_update(); +} + +/* + * insert string, expand $x variables: + * $$ -> literal $ + * $1 -> current directory name (primary panel's directory) + * $2 -> secondary directory name (secondary panel's directory) + * $F -> current file name + * $S -> names of all selected file(s) + * $f -> $S - if the key was pressed and + * at least one file has been selected + * $F - otherwise + * everything else is copied literally + */ +void +edit_macro(const wchar_t *macro) +{ + wchar_t ch, *ins; + const wchar_t *src; + int i, cnt, curs, sel; + FLAG prefix, noenter = 0, automatic = 0, warn_dotdir = 0; + FILE_ENTRY *pfe; + + /* + * implementation note: avoid char by char inserts whenever + * possible because of the overhead + */ + + if (textline->curs == 0 || wcschr(L" :=",USTR(textline->line)[textline->curs - 1])) + while (*macro == L' ') + macro++; + + curs = -1; + for (src = macro, prefix = 0; (ch = *macro++) != L'\0'; ) { + if (TCLR(prefix)) { + /* insert everything seen before the '$' prefix */ + cnt = macro - src - 2 /* two chars in "$x" */; + if (cnt > 0) + wcsncpy(insert_space(cnt),src,cnt); + src = macro; + + /* now handle $x */ + if (ch == L'f' && panel->cnt > 0) { + ch = ppanel_file->selected && kinp.prev_esc ? L'S' : L'F'; + if (ch == L'F' && ppanel_file->selected + && !NOPT(NOTIF_SELECTED)) + msgout(MSG_i | MSG_NOTIFY,"press before if you " + "want to work with selected files"); + } + switch (ch) { + case L'$': + edit_nu_insertchar(L'$'); + break; + case L'1': + edit_nu_insertstr(USTR(ppanel_file->dirw),QUOT_NORMAL); + break; + case L'2': + edit_nu_insertstr(USTR(ppanel_file->other->dirw),QUOT_NORMAL); + break; + case L'c': + curs = textline->curs; + break; + case L'S': + if (panel->cnt > 0) { + for (i = cnt = 0, sel = ppanel_file->selected; cnt < sel; i++) { + pfe = ppanel_file->files[i]; + if (pfe->select) { + if (pfe->dotdir) { + sel--; + warn_dotdir = 1; + continue; + } + if (cnt++) + edit_nu_insertchar(L' '); + edit_nu_insertstr(SDSTR(pfe->filew),QUOT_NORMAL); + } + } + } + break; + case L'f': + break; + case L'F': + if (panel->cnt > 0) { + pfe = ppanel_file->files[ppanel_file->pd->curs]; + edit_nu_insertstr(SDSTR(pfe->filew),QUOT_NORMAL); + } + break; + case L':': + curs = -1; + edit_nu_kill(); + break; + case L'!': + automatic = 1; + break; + case L'~': + noenter = 1; + break; + default: + ins = insert_space(2); + ins[0] = L'$'; + ins[1] = ch; + } + } + else if (ch == L'$') + prefix = 1; + } + + /* insert the rest */ + edit_insertstr(src,QUOT_NONE); + + if (curs >= 0) + textline->curs = curs; + + if (noenter) { + disp_data.noenter = 1; + disp_data.noenter_hash = jshash(USTR(textline->line)); + } + + if (warn_dotdir && !NOPT(NOTIF_DOTDIR)) + msgout(MSG_i | MSG_NOTIFY,"directory names . and .. not inserted"); + + if (panel->filtering == 1) + cx_filter(); + + if (automatic && textline->size) { + cmd_auto = 1; + cx_files_enter(); + cmd_auto = 0; + } +} + +void cx_edit_cmd_f2(void) { edit_macro(L"$f "); } +void cx_edit_cmd_f3(void) { edit_macro(cfg_str(CFG_CMD_F3)); } +void cx_edit_cmd_f4(void) { edit_macro(cfg_str(CFG_CMD_F4)); } +void cx_edit_cmd_f5(void) { edit_macro(cfg_str(CFG_CMD_F5)); } +void cx_edit_cmd_f6(void) { edit_macro(cfg_str(CFG_CMD_F6)); } +void cx_edit_cmd_f7(void) { edit_macro(cfg_str(CFG_CMD_F7)); } +void cx_edit_cmd_f8(void) { edit_macro(cfg_str(CFG_CMD_F8)); } +void cx_edit_cmd_f9(void) { edit_macro(cfg_str(CFG_CMD_F9)); } +void cx_edit_cmd_f10(void) { edit_macro(cfg_str(CFG_CMD_F10)); } +void cx_edit_cmd_f11(void) { edit_macro(cfg_str(CFG_CMD_F11)); } +void cx_edit_cmd_f12(void) { edit_macro(cfg_str(CFG_CMD_F12)); } + +static void +paste_exit() +{ + /* when called from the complete/insert panel: exit the panel */ + if (get_current_mode() == MODE_PASTE) + next_mode = MODE_SPECIAL_RETURN; +} + +void +cx_edit_paste_path(void) +{ + if (ppanel_file->pd->cnt) + edit_macro(strcmp(USTR(ppanel_file->dir),"/") ? L" $1/$F " : L" /$F "); + paste_exit(); +} + +void +cx_edit_paste_link(void) +{ + FILE_ENTRY *pfe; + + if (ppanel_file->pd->cnt) { + pfe = ppanel_file->files[ppanel_file->pd->curs]; + if (!pfe->symlink) + msgout(MSG_i,"not a symbolic link"); + else { + edit_nu_insertstr(USTR(pfe->linkw),QUOT_NORMAL); + edit_insertchar(' '); + } + } + paste_exit(); +} + +void +cx_edit_paste_currentfile(void) +{ + if (ppanel_file->pd->cnt) + edit_macro(L"$F "); + paste_exit(); +} + +void +cx_edit_paste_filenames(void) +{ + if (ppanel_file->pd->cnt) { + if (ppanel_file->selected) + edit_macro(L" $S "); + else + msgout(MSG_i,"no selected files"); + } + paste_exit(); +} + +void +cx_edit_paste_dir1(void) +{ + edit_macro(L" $1"); + paste_exit(); +} + +void +cx_edit_paste_dir2(void) +{ + edit_macro(L" $2"); + paste_exit(); +} + +void +cx_edit_flipcase(void) +{ + wchar_t ch; + + ch = USTR(textline->line)[textline->curs]; + if (ch == L'\0') + return; + if (iswlower(ch)) + ch = towupper(ch); + else if (iswupper(ch)) + ch = towlower(ch); + else { + cx_edit_right(); + return; + } + USTR(textline->line)[textline->curs] = ch; + edit_nu_right(); + edit_update(); +} + +void +edit_setprompt(TEXTLINE *pline, const wchar_t *prompt) +{ + wchar_t *p; + int width; + + for (p = usw_copy(&pline->prompt,prompt), width = 0; *p != L'\0'; p++) { + width += WCW(*p); + if (width > MAX_PROMPT_WIDTH) { + p -= 2; + width = width - wc_cols(p,0,3) + 2; + wcscpy(p,L"> "); + msgout(MSG_NOTICE,"Long prompt string truncated: \"%ls\"",USTR(pline->prompt)); + break; + } + } + pline->promptwidth = width; +} diff --git a/src/edit.h b/src/edit.h new file mode 100644 index 0000000..a1f0005 --- /dev/null +++ b/src/edit.h @@ -0,0 +1,62 @@ +extern void edit_update(void); +extern void edit_update_cursor(void); +extern int edit_adjust(void); + +extern int edit_islong(void); +extern int edit_isauto(void); +extern int edit_isspecial(wchar_t); + +extern void edit_nu_insertchar(wchar_t); +extern void edit_insertchar(wchar_t); +extern void edit_nu_insertstr(const wchar_t *, int); +extern void edit_insertstr(const wchar_t *, int); +extern void edit_nu_putstr(const wchar_t *); +extern void edit_putstr(const wchar_t *); +extern void edit_nu_kill(void); +extern void edit_macro(const wchar_t *); +extern void edit_setprompt(TEXTLINE *, const wchar_t *); + +/* the second argument of edit_insertstr() */ +#define QUOT_NONE 0 /* no quoting */ +#define QUOT_NORMAL 1 /* regular quoting */ +#define QUOT_IN_QUOTES 2 /* inside "double quotes" */ + +/* move */ +extern void cx_edit_begin(void); +extern void cx_edit_end(void); +extern void cx_edit_left(void); +extern void cx_edit_right(void); +extern void cx_edit_up(void); +extern void cx_edit_down(void); +extern void cx_edit_w_left(void); +extern void cx_edit_w_right(void); +extern void cx_edit_mouse(void); + +/* delete */ +extern void cx_edit_backsp(void); +extern void cx_edit_delchar(void); +extern void cx_edit_delend(void); +extern void cx_edit_w_del(void); +extern void cx_edit_kill(void); + +/* insert */ +extern void cx_edit_cmd_f2(void); +extern void cx_edit_cmd_f3(void); +extern void cx_edit_cmd_f4(void); +extern void cx_edit_cmd_f5(void); +extern void cx_edit_cmd_f6(void); +extern void cx_edit_cmd_f7(void); +extern void cx_edit_cmd_f8(void); +extern void cx_edit_cmd_f9(void); +extern void cx_edit_cmd_f10(void); +extern void cx_edit_cmd_f11(void); +extern void cx_edit_cmd_f12(void); +extern void cx_edit_paste_link(void); +extern void cx_edit_paste_path(void); +extern void cx_edit_paste_currentfile(void); +extern void cx_edit_paste_filenames(void); +extern void cx_edit_paste_dir1(void); +extern void cx_edit_paste_dir2(void); + +/* transform */ +extern void cx_edit_flipcase(void); diff --git a/src/exec.c b/src/exec.c new file mode 100644 index 0000000..0ff0ffc --- /dev/null +++ b/src/exec.c @@ -0,0 +1,505 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* waitpid() */ +#include /* errno */ +#include /* sigaction() */ +#include /* log.h */ +#include /* puts() */ +#include /* exit() */ +#include /* strcmp() */ +#include /* fork() */ + +#include "exec.h" + +#include "cfg.h" /* cfg_str() */ +#include "control.h" /* get_current_mode() */ +#include "edit.h" /* edit_islong() */ +#include "inout.h" /* win_edit() */ +#include "filepanel.h" /* changedir() */ +#include "history.h" /* hist_save() */ +#include "lex.h" /* cmd2lex() */ +#include "list.h" /* list_directory() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2mb() */ +#include "mouse.h" /* mouse_set() */ +#include "tty.h" /* tty_setraw() */ +#include "userdata.h" /* dir_tilde() */ +#include "ustringutil.h" /* us_getcwd() */ +#include "util.h" /* jshash() */ +#include "xterm_title.h" /* xterm_title_set() */ + +static FLAG promptdir = 0; /* %p in prompt ? */ + +void +exec_initialize(void) +{ + set_shellprompt(); +} + +/* short form of a directory name */ +static const wchar_t * +short_dir(const wchar_t *fulldir) +{ + int len; + wchar_t *pdir, *pshort; + static USTRINGW dir = UNULL; + + pdir = usw_copy(&dir,fulldir); + + /* homedir -> ~ */ + len = wcslen(user_data.homedirw); + if (wcsncmp(pdir,user_data.homedirw,len) == 0 + && (pdir[len] == L'\0' || pdir[len] == L'/')) { + pdir += len - 1; + *pdir = L'~'; + } + + /* short enough ? */ + len = wcslen(pdir); + if (len <= MAX_SHORT_CWD_LEN) + return pdir; + + pshort = pdir += len - MAX_SHORT_CWD_LEN; + /* truncate at / (directory boundary) */ + while (*pdir != L'\0') + if (*pdir++ == L'/') + return pdir; + + wcsncpy(pshort,L"...",3); + return pshort; +} + +void +set_shellprompt(void) +{ + static USTRINGW prompt = UNULL; + static const wchar_t *promptchar[3] + = { L"$#", L"%#", L">>" }; /* for each CLEX_SHELLTYPE */ + wchar_t ch, appendch, *dst; + const wchar_t *src, *append; + int len1, len2, alloc, size; + FLAG var; + + alloc = usw_setsize(&prompt,ALLOC_UNIT); + dst = USTR(prompt); + size = 0; + + if (user_data.isroot) { + wcscpy(dst,L"ROOT "); + dst += 5; + size += 5; + } + + for (var = promptdir = 0, src = cfg_str(CFG_PROMPT); (ch = *src++) != L'\0';) { + append = 0; + appendch = L'\0'; + if (TCLR(var)) { + switch (ch) { + case L'h': + append = user_data.hostw; + break; + case L'p': + appendch = promptchar[user_data.shelltype][user_data.isroot]; + break; + case L's': + append = user_data.shellw; + break; + case L'u': + append = user_data.loginw; + break; + case L'w': + promptdir = 1; + append = short_dir(USTR(ppanel_file->dirw)); + break; + case L'$': + append = L"$"; + break; + default: + append = L"$"; + appendch = ch; + } + len1 = append ? wcslen(append) : 0; + len2 = appendch != L'\0'; + } + else if (ch == L'$') { + var = 1; + continue; + } + else { + appendch = ch; + len1 = 0; + len2 = 1; + } + + if (size + len1 + len2 + 1 > alloc) { + alloc = usw_resize(&prompt,size + len1 + len2 + 1); + dst = USTR(prompt) + size; + } + if (append) { + wcscpy(dst,append); + dst += len1; + } + if (appendch) + *dst++ = appendch; + size += len1 + len2; + } + *dst = L'\0'; + edit_setprompt(&line_cmd,USTR(prompt)); +} + +void +update_shellprompt(void) +{ + if (promptdir) { + set_shellprompt(); + win_edit(); + } +} + +/* return value: 0 = command executed with exit code 0, otherwise -1 */ +static int +execute(const char *command, const wchar_t *commandw) +{ + pid_t childpid; + int status, code, retval = -1 /* OK = 0 */; + struct sigaction act; + const char *signame; + + xterm_title_set(1,command,commandw); + childpid = fork(); + if (childpid == -1) + msgout(MSG_W,"EXEC: Cannot create new process (%s)",strerror(errno)); + else if (childpid == 0) { + /* child process = command */ +#ifdef _POSIX_JOB_CONTROL + /* move this process to a new foreground process group */ + childpid = getpid(); + setpgid(childpid,childpid); + tcsetpgrp(STDIN_FILENO,childpid); +#endif + + /* reset signal dispositions */ + act.sa_handler = SIG_DFL; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGINT,&act,0); + sigaction(SIGQUIT,&act,0); +#ifdef _POSIX_JOB_CONTROL + sigaction(SIGTSTP,&act,0); + sigaction(SIGTTIN,&act,0); + sigaction(SIGTTOU,&act,0); +#endif + + /* execute the command */ + logfile_close(); + execl(user_data.shell,user_data.shell,"-c",command,(char *)0); + printf("EXEC: Cannot execute shell %s (%s)\n",user_data.shell,strerror(errno)); + exit(99); + /* NOTREACHED */ + } + else { + /* parent process = CLEX */ +#ifdef _POSIX_JOB_CONTROL + /* move child process to a new foreground process group */ + setpgid(childpid,childpid); + tcsetpgrp(STDIN_FILENO,childpid); +#endif + msgout(MSG_AUDIT,"Command: \"%s\", working directory: \"%s\"",command,USTR(ppanel_file->dir)); + + for (; /* until break */;) { + /* wait for child process exit or stop */ + while (waitpid(childpid,&status,WUNTRACED) < 0) + /* ignore EINTR */; +#ifdef _POSIX_JOB_CONTROL + /* put CLEX into the foreground */ + tcsetpgrp(STDIN_FILENO,clex_data.pid); +#endif + if (!WIFSTOPPED(status)) + break; + tty_save(); + tty_reset(); + if (tty_dialog('\0', + "CLEX: The command being executed has been suspended.\n" + " Press S to start a shell session or any\n" + " other key to resume the command: ") == 's') { + printf("Suspended process PID = %d\n" + "Type 'exit' to end the shell session\n" + ,childpid); + fflush(stdout); + msgout(MSG_AUDIT,"The command has been stopped." + " Starting an interactive shell session"); + system(user_data.shell); + msgout(MSG_AUDIT,"The interactive shell session has terminated." + " Restarting the stopped command"); + tty_press_enter(); + } + tty_restore(); + +#ifdef _POSIX_JOB_CONTROL + /* now place the command into the foreground */ + tcsetpgrp(STDIN_FILENO,childpid); +#endif + kill(-childpid,SIGCONT); + } + + tty_reset(); + putchar('\n'); + if (WIFEXITED(status)) { + code = WEXITSTATUS(status); + msgout(MSG_AUDIT," Exit code: %d%s",code, + code == 99 ? " (might be a shell execution failure)" : ""); + if (code == 0) { + retval = 0; + fputs("Command successful. ",stdout); + } + else + printf("Exit code = %d. ",code); + } + else { + code = WTERMSIG(status); +#ifdef HAVE_STRSIGNAL + signame = strsignal(code); +#elif defined HAVE_DECL_SYS_SIGLIST + signame = sys_siglist[code]; +#else + signame = "signal name unknown"; +#endif + printf("Abnormal termination, signal %d (%s)",code,signame); + msgout(MSG_AUDIT," Abnormal termination, signal %d (%s)",code,signame); +#ifdef WCOREDUMP + if (WCOREDUMP(status)) + fputs(", core image dumped",stdout); +#endif + putchar('\n'); + } + } + + /* + * List the directory while the user stares at the screen reading + * the command output. This way CLEX appears to restart faster. + */ + xterm_title_set(0,command,commandw); + if (ppanel_file->pd->filtering && ppanel_file->filtype == 0) + ppanel_file->pd->filtering = 0; + list_directory(); + ppanel_file->other->expired = 1; + + if (retval < 0) + disp_data.noenter = 0; + tty_press_enter(); + xterm_title_set(0,0,0); + + return retval; +} + +/* check for a "rm" command (1 = found, 0 = not found) */ +static int +check_rm(const wchar_t *str, const char *lex) +{ + char lx; + int i; + CODE state; /* 0 = no match, 1 = command name follows */ + /* 2 = "r???" found, 3 = "rm???" found */ + + for (state = 1, i = 0; /* until return */; i++) { + lx = lex[i]; + if (lx == LEX_QMARK) + continue; + if (lx == LEX_CMDSEP) { + state = 1; + continue; + } + if (state == 3) { + if (lx != LEX_PLAINTEXT) + return 1; + state = 0; + continue; + } + if (IS_LEX_END(lx)) + return 0; + if (state == 1) { + if (IS_LEX_SPACE(lx)) + continue; + state = (lx == LEX_PLAINTEXT && str[i] == L'r') ? 2 : 0; + } + else if (state == 2) + state = (lx == LEX_PLAINTEXT && str[i] == L'm') ? 3 : 0; + } +} + +/* return value: 0 = no warning issued, otherwise 1 */ +static int +print_warnings(const wchar_t *cmd, const char *lex) +{ + static USTRING cwd = UNULL; + int warn; + const char *dir; + + warn = 0; + + if (us_getcwd(&cwd) < 0) { + msgout(MSG_W,"WARNING: current working directory is not accessible"); + us_copy(&cwd,"???"); + warn = 1; + } + else if (strcmp(USTR(ppanel_file->dir),dir = USTR(cwd))) { + msgout(MSG_w,"WARNING: current working directory has been renamed:\n" + " old name: %s\n" + " new name: %s",USTR(ppanel_file->dir),dir); + us_copy(&ppanel_file->dir,dir); + convert_dir(); + warn = 1; + } + + if ((!NOPT(NOTIF_RM) || edit_isauto()) && check_rm(cmd,lex)) { + msgout(MSG_w | MSG_NOTIFY,"working directory: %s\n" + "WARNING: rm command deletes files, please confirm",USTR(cwd)); + warn = 1; + } + + /* note1: following warning is appropriate in the file mode only */ + /* note2: edit_islong() works with 'textline' instead of 'cmd', that's ok, see note1 */ + if (!NOPT(NOTIF_LONG) && get_current_mode() == MODE_FILE + && edit_islong() && !edit_isauto()) { + msgout(MSG_w | MSG_NOTIFY,"WARNING: This long command did not fit on the command line"); + warn = 1; + } + + return warn; +} + +/* check for a simple "cd [dir]" command */ +static const char * +check_cd(const wchar_t *str, const char *lex) +{ + static USTRINGW cut = UNULL; + static USTRINGW dq = UNULL; + const wchar_t *dirstr; + char lx; + int i, start = 0, len = 0; /* prevent compiler warning */ + FLAG chop, tilde, dequote; + CODE state; + + for (dequote = chop = 0, state = 0, i = 0; /* until return or break */; i++) { + lx = lex[i]; + if (lx == LEX_QMARK) { + if (state >= 3) + dequote = 1; + continue; + } + switch (state) { + case 0: /* init */ + if (IS_LEX_SPACE(lx)) + continue; + if (lx != LEX_PLAINTEXT || str[i] != L'c') + return 0; + state = 1; + break; + case 1: /* "c???" */ + if (lx != LEX_PLAINTEXT || str[i] != L'd') + return 0; + state = 2; + break; + case 2: /* "cd???" */ + if (!IS_LEX_EMPTY(lx)) + return 0; + state = 3; + /* no break */ + case 3: /* "cd" */ + if (IS_LEX_SPACE(lx)) + continue; + if (IS_LEX_END(lx)) + return lx == LEX_END_OK ? user_data.homedir : 0; /* cd without args */ + start = i; + state = 4; + break; + case 4: /* "cd dir???" */ + if (lx == LEX_PLAINTEXT) + continue; + if (!IS_LEX_EMPTY(lx)) + return 0; + len = i - start; + state = 5; + /* no break */ + case 5: /* "cd dir ???" */ + if (IS_LEX_SPACE(lx)) { + chop = 1; + continue; + } + if (lx != LEX_END_OK) + return 0; + + /* check successfull */ + + /* cut out the directory part and dequote it */ + dirstr = str + start; + + if (chop) + dirstr = usw_copyn(&cut,dirstr,len); + if (dequote) { + tilde = is_dir_tilde(dirstr); + usw_dequote(&dq,dirstr,len); + dirstr = USTR(dq); + } + else + tilde = *dirstr == L'~'; + return convert2mb(tilde ? dir_tilde(dirstr) : dirstr); + } + } +} + +/* + * function returns 1 if the command has been executed + * (successfully or not), otherwise 0 + */ +int +execute_cmd(const wchar_t *cmdw) +{ + static FLAG fail, do_it; + const char *cmd, *lex, *dir; + + /* intercept single 'cd' command */ + lex = cmd2lex(cmdw); + if ( (dir = check_cd(cmdw,lex)) ) { + if (changedir(dir) != 0) + return 0; + hist_save(cmdw,0); + win_title(); + win_panel(); + msgout(MSG_i,"directory changed"); + return 1; + } + + if (disp_data.noenter && disp_data.noenter_hash != jshash(cmdw)) + disp_data.noenter = 0; + cmd = convert2mb(cmdw); + curses_stop(); + mouse_restore(); + putchar('\n'); + puts(cmd); + putchar('\n'); + fflush(stdout); + do_it = print_warnings(cmdw,lex) == 0 || tty_dialog('y',"Execute the command?"); + if (do_it) { + fail = execute(cmd,cmdw) < 0; + hist_save(cmdw,fail); + } + mouse_set(); + curses_restart(); + + return do_it; +} diff --git a/src/exec.h b/src/exec.h new file mode 100644 index 0000000..e741527 --- /dev/null +++ b/src/exec.h @@ -0,0 +1,4 @@ +extern void exec_initialize(void); +extern void set_shellprompt(void); +extern void update_shellprompt(void); +extern int execute_cmd(const wchar_t *); diff --git a/src/filepanel.c b/src/filepanel.c new file mode 100644 index 0000000..edb5529 --- /dev/null +++ b/src/filepanel.c @@ -0,0 +1,431 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* errno */ +#include /* log.h */ +#include /* getenv() */ +#include /* strcmp() */ +#include /* chdir() */ + +#include "filepanel.h" + +#include "bookmarks.h" /* get_bookmark() */ +#include "cfg.h" /* cfg_str() */ +#include "completion.h" /* compl_text() */ +#include "control.h" /* err_exit() */ +#include "directory.h" /* filepos_save() */ +#include "edit.h" /* edit_macro() */ +#include "exec.h" /* execute_cmd() */ +#include "history.h" /* cx_hist_prev() */ +#include "inout.h" /* win_panel() */ +#include "log.h" /* msgout() */ +#include "list.h" /* list_directory() */ +#include "mbwstring.h" /* convert2mb() */ +#include "panel.h" /* pan_adjust() */ +#include "undo.h" /* undo_reset() */ +#include "userdata.h" /* userdata_expire() */ +#include "ustringutil.h" /* us_getcwd() */ +#include "util.h" /* base_name() */ + +static void +try_all_parents(const char *directory) +{ + FLAG root; + char *dir, *p; + + dir = estrdup(directory); + for (root = 0; chdir(dir) < 0 || us_getcwd(&ppanel_file->dir) < 0; ) { + if (root) + err_exit("Access to the root directory was denied !"); + if ((p = strrchr(dir + 1,'/')) != 0) + *p = '\0'; + else { + /* last resort */ + strcpy(dir,"/"); + root = 1; + } + } + free(dir); +} + +void +files_initialize(void) +{ + const BOOKMARK *bm; + + static INPUTLINE filter1, filter2; + static PANEL_DESC panel_desc_1 = { 0,0,0,0,PANEL_TYPE_FILE,0,0,&filter1,draw_line_file }; + static PANEL_DESC panel_desc_2 = { 0,0,0,0,PANEL_TYPE_FILE,0,0,&filter2,draw_line_file }; + static PANEL_FILE panel_f1 = { &panel_desc_1 }; + static PANEL_FILE panel_f2 = { &panel_desc_2 }; + + panel_f1.other = &panel_f2; + ppanel_file = panel_f2.other = &panel_f1; + + if ( (bm = get_bookmark(L"DIR1")) && chdir(USTR(bm->dir)) < 0) + msgout(MSG_w,"Bookmark DIR1: Cannot change directory: %s",strerror(errno)); + if (us_getcwd(&panel_f1.dir) < 0) { + msgout(MSG_w,"Cannot get the name of the working directory (%s), it will be changed", + strerror(errno)); + try_all_parents(checkabs(getenv("PWD")) ? getenv("PWD") : user_data.homedir); + } + usw_convert2w(USTR(panel_f1.dir),&panel_f1.dirw); + /* do not call convert_dir() here, because + it could attempt to change the prompt which is not initialized yet */ + + if ( (bm = get_bookmark(L"DIR2")) ) { + usw_copy(&panel_f2.dirw,USTR(bm->dirw)); + us_copy(&panel_f2.dir,USTR(bm->dir)); + } + else { + usw_copy(&panel_f2.dirw,user_data.homedirw); /* tested by checkabs() already */ + us_convert2mb(user_data.homedirw,&panel_f2.dir); + } + + panel_f1.order = panel_f2.order = panel_sort.order; + panel_f1.group = panel_f2.group = panel_sort.group; + panel_f1.hide = panel_f2.hide = panel_sort.hide; +} + +int +files_main_prepare(void) +{ + static FLAG prepared = 0; + + /* + * allow only one initial run of files_main_prepare(), successive calls + * are merely an indirect result of panel exchange commands + */ + if (prepared) + return 0; + + panel = ppanel_file->pd; + list_directory(); + ppanel_file->other->expired = 1; + + textline = &line_cmd; + edit_nu_kill(); + if (user_data.noconfig) + edit_nu_putstr(L"cfg-clex"); + + prepared = 1; + return 0; +} + +/* to be called after each cwd change */ +void +convert_dir(void) +{ + usw_convert2w(USTR(ppanel_file->dir),&ppanel_file->dirw); + update_shellprompt(); +} + +int +file_find(const char *name) +{ + int i; + + for (i = 0; i < ppanel_file->pd->cnt; i++) + if (strcmp(SDSTR(ppanel_file->files[i]->file),name) == 0) + return i; + + return -1; +} + +/* + * this is an error recovery procedure used when the current working + * directory and its name stored in the file panel are not in sync + */ +static void +find_valid_cwd(void) +{ + /* panel contents is invalid in different directory */ + filepanel_reset(); + win_title(); + win_panel(); + msgout(MSG_w,"CHANGE DIR: panel's directory is not accessible, it will be changed"); + + try_all_parents(USTR(ppanel_file->dir)); + convert_dir(); +} + +/* + * changes working directory to 'dir' and updates the absolute + * pathname in the primary file panel accordingly + * + * changedir() returns 0 on success, -1 when the directory could + * not be changed. In very rare case when multiple errors occur, + * changedir() might change to other directory as requested. Note + * that in such case it returns 0 (cwd changed). + */ +int +changedir(const char *dir) +{ + int line; + FLAG parent = 0; + static USTRING savedir_buff = UNULL; + const char *savedir; + + if (chdir(dir) < 0) { + msgout(MSG_w,"CHANGE DIR: %s",strerror(errno)); + return -1; + } + + filepos_save(); /* save the last position in the directory we are leaving now */ + savedir = us_copy(&savedir_buff,USTR(ppanel_file->dir)); + if (us_getcwd(&ppanel_file->dir) < 0) { + /* not sure where we are -> must leave this dir */ + us_copy(&ppanel_file->dir,savedir); + if (chdir(savedir) == 0) { + /* we are back in old cwd */ + msgout(MSG_w,"CHANGE DIR: Cannot change directory"); + return -1; + } + find_valid_cwd(); + } + else { + if (strcmp(dir,"..") == 0) + parent = 1; + if (strcmp(savedir,USTR(ppanel_file->dir))) { + /* panel contents is invalid in different directory */ + filepanel_reset(); + convert_dir(); + } + } + + list_directory(); + /* if 'dir' argument was a pointer to a filepanel entry + list_directory() has just invalidated it */ + + /* special case: set cursor to the directory we have just left + because users prefer it this way */ + if (parent) { + line = file_find(base_name(savedir)); + if (line >= 0) { + ppanel_file->pd->curs = line; + pan_adjust(ppanel_file->pd); + } + } + + return 0; +} + +/* change working directory */ +void +cx_files_cd(void) +{ + FILE_ENTRY *pfe; + + pfe = ppanel_file->files[ppanel_file->pd->curs]; + if (IS_FT_DIR(pfe->file_type)) { + if (changedir(SDSTR(pfe->file)) == 0) { + win_title(); + win_panel(); + } + } + else + msgout(MSG_i,"not a directory"); +} + +/* change working directory and switch panels */ +void +cx_files_cd_xchg(void) +{ + FILE_ENTRY *pfe; + + pfe = ppanel_file->files[ppanel_file->pd->curs]; + if (IS_FT_DIR(pfe->file_type)) { + panel = (ppanel_file = ppanel_file->other)->pd; + if (changedir(SDSTR(pfe->file)) == 0) { + win_title(); + win_panel(); + /* allow control_loop() to detect the 'panel' change */ + next_mode = MODE_FILE; + return; + } + panel = (ppanel_file = ppanel_file->other)->pd; + } + else + msgout(MSG_i,"not a directory"); +} + +void +cx_files_cd_root(void) +{ + changedir("/"); + win_title(); + win_panel(); +} + +void +cx_files_cd_parent(void) +{ + changedir(".."); + win_title(); + win_panel(); +} + +void +cx_files_cd_home(void) +{ + changedir(user_data.homedir); + win_title(); + win_panel(); +} + +void +cx_files_reread(void) +{ + list_directory(); + win_panel(); +} + +/* reread also user account information (users/groups) */ +void +cx_files_reread_ug(void) +{ + userdata_expire(); + list_directory(); + win_panel(); +} + +/* exchange panels */ +void +cx_files_xchg(void) +{ + int exp; + + panel = (ppanel_file = ppanel_file->other)->pd; + + if (chdir(USTR(ppanel_file->dir)) == -1) { + find_valid_cwd(); + list_directory(); + } + else { + update_shellprompt(); /* convert_dir() not necessary */ + exp = ppanel_file->expired ? 0 : PANEL_EXPTIME; + if (list_directory_cond(exp) < 0) + /* filepanel_read() which invokes filepos_save() was not called */ + filepos_save(); /* put the new cwd to the top of the list */ + } + + /* allow control_loop() to detect the 'panel' change */ + next_mode = MODE_FILE; +} + +/* pressed - several functions: exec, chdir and insert */ +void +cx_files_enter(void) +{ + FILE_ENTRY *pfe; + + if (textline->size && (kinp.fkey != 2 || MI_AREA(LINE))) { + if (execute_cmd(USTR(textline->line))) { + cx_edit_kill(); + undo_reset(); + } + } + else if (ppanel_file->pd->cnt && (kinp.fkey != 2 || MI_AREA(PANEL))) { + pfe = ppanel_file->files[ppanel_file->pd->curs]; + if (IS_FT_DIR(pfe->file_type)) { + /* now doing cx_files_cd(); */ + if (changedir(SDSTR(pfe->file)) == 0) { + win_title(); + win_panel(); + } + } + else if (kinp.fkey == 2 && MI_AREA(PANEL)) + control_loop(MODE_PREVIEW); + else if (IS_FT_EXEC(pfe->file_type)) + edit_macro(L"./$F "); + } +} + +/* pressed - also multiple functions: complete and insert */ +void +cx_files_tab(void) +{ + int compl, file_type; + + if (panel->filtering == 1) { + /* cursor is in the filter expression -> no completion in the command line */ + compl = compl_text(COMPL_TYPE_DRYRUN); /* this will fail */ + if (compl == -1 && ppanel_file->pd->cnt + && IS_FT_EXEC(ppanel_file->files[ppanel_file->pd->curs]->file_type)) + edit_macro(L"./$F "); + else if (compl == -2) + edit_macro(L"$F "); + else + msgout(MSG_i,"cannot complete a filter expression"); + return; + } + + /* try completion first, it returns 0 on success */ + compl = compl_text(COMPL_TYPE_AUTO); + if (compl == -1) { + file_type = ppanel_file->pd->cnt ? + ppanel_file->files[ppanel_file->pd->curs]->file_type : FT_NA; + /* -1: nothing to complete, this will be the first word */ + if (IS_FT_EXEC(file_type)) + edit_macro(L"./$F "); + else if (IS_FT_DIR(file_type)) + edit_macro(L"$F/"); + else + /* absolutely clueless ! */ + msgout(MSG_i,"COMPLETION: please type at least the first character"); + } else if (compl == -2) + /* -2: nothing to complete, but not in the first word */ + edit_macro(L"$F "); +} + +void +cx_files_mouse(void) +{ + int compl; + + switch (minp.area) { + case AREA_TITLE: + if (MI_DC(1)) { + if (minp.x <= disp_data.dir1end) + control_loop(MODE_DIR); + else if (minp.x >= disp_data.dir2start) + cx_files_xchg(); + } + break; + case AREA_PANEL: + if (MI_PASTE) { + compl = compl_text(COMPL_TYPE_DRYRUN); + if (compl == -1 && IS_FT_EXEC(ppanel_file->files[ppanel_file->pd->curs]->file_type)) + edit_macro(L"./$F "); + else if (compl == -2) + edit_macro(L"$F "); + else + edit_macro(L" $F "); + } + break; + case AREA_BAR: + if (MI_DC(1) && minp.cursor == 1) { + control_loop(MODE_MAINMENU); + minp.area = AREA_NONE; /* disable further mouse event processing */ + } + break; + case AREA_PROMPT: + if (MI_DC(1)) + control_loop(MODE_HIST); + else if (MI_WHEEL) + MI_B(4) ? cx_hist_prev() : cx_hist_next(); + } +} diff --git a/src/filepanel.h b/src/filepanel.h new file mode 100644 index 0000000..dd54043 --- /dev/null +++ b/src/filepanel.h @@ -0,0 +1,16 @@ +extern void files_initialize(void); +extern int files_main_prepare(void); +extern void convert_dir(void); +extern int file_find(const char *); +extern int changedir(const char *); +extern void cx_files_cd(void); +extern void cx_files_cd_xchg(void); +extern void cx_files_cd_root(void); +extern void cx_files_cd_parent(void); +extern void cx_files_cd_home(void); +extern void cx_files_reread(void); +extern void cx_files_reread_ug(void); +extern void cx_files_xchg(void); +extern void cx_files_enter(void); +extern void cx_files_tab(void); +extern void cx_files_mouse(void); diff --git a/src/filerw.c b/src/filerw.c new file mode 100644 index 0000000..9e05285 --- /dev/null +++ b/src/filerw.c @@ -0,0 +1,392 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* text file read/write functions */ + +#include "clexheaders.h" + +#include /* open() */ +#include /* errno */ +#include /* open() */ +#include /* log.h */ +#include /* fopen() */ +#include /* free() */ +#include /* strerror() */ +#include /* fstat() */ + +#include "filerw.h" + +#include "log.h" /* msgout() */ +#include "util.h" /* emalloc() */ + +/* file read functions */ + +#define TFDESC_CNT 2 /* number of available descriptors (CLEX needs only 1) */ +typedef struct { + FLAG inuse; /* non-zero if valid */ + FLAG truncated; + const char *filename; /* file name */ + char *buff; /* raw file data */ + size_t size; /* file data size */ + char **line; /* result of fr_split() */ + size_t linecnt; /* number of lines */ +} TFDESC; +static TFDESC tfdesc[TFDESC_CNT]; + +/* + * read text file into memory + * output (success): txtfile descriptor >= 0 + * output (failure): error code < 0; error logged + */ +static int +_fr_open(const char *filename, size_t maxsize, int preview) +{ + int i, fd, tfd, errcode; + struct stat stbuf; + size_t filesize; /* do not need off_t here */ + ssize_t rd; + char *buff; + + if ( (fd = open(filename,O_RDONLY | O_NONBLOCK)) < 0) { + if ((errcode = errno) == ENOENT) + return FR_NOFILE; /* File does not exist */ + msgout(MSG_NOTICE,"Could not open \"%s\" for reading",filename); + msgout(MSG_DEBUG," System error: %s",strerror(errcode)); + return FR_ERROR; + } + + tfd = -1; + for (i = 0; i < TFDESC_CNT; i++) + if (!tfdesc[i].inuse) { + tfd = i; + break; + } + if (tfd == -1) { + close(fd); + msgout(MSG_NOTICE,"Internal descriptor table is full in fr_open()"); + return FR_ERROR; + } + + fstat(fd,&stbuf); /* cannot fail with valid descriptor */ + if (!S_ISREG(stbuf.st_mode)) { + close(fd); + msgout(MSG_NOTICE,"File \"%s\" is not a plain file",filename); + return FR_ERROR; + } + if ((stbuf.st_mode & S_IWOTH) == S_IWOTH && !preview) { + close(fd); + msgout(MSG_NOTICE,"File \"%s\" is world-writable, i.e. unsafe",filename); + return FR_ERROR; + } + if ((filesize = stbuf.st_size) <= maxsize) + tfdesc[tfd].truncated = 0; + else { + if (preview) { + tfdesc[tfd].truncated = 1; + filesize = maxsize; + } + else { + close(fd); + msgout(MSG_NOTICE,"File \"%s\" is too big (too many characters)",filename); + return FR_ERROR; + } + } + + /* read into memory */ + buff = emalloc(filesize + 1); /* 1 extra byte for fr_split() */ + rd = read_fd(fd,buff,filesize); + if (rd == -1) + errcode = errno; + close(fd); + if (rd == -1) { + free(buff); + msgout(MSG_NOTICE,"Error reading data from \"%s\"",filename); + msgout(MSG_DEBUG," System error: %s",strerror(errcode)); + return FR_ERROR; + } + + tfdesc[tfd].inuse = 1; + tfdesc[tfd].filename = filename; + tfdesc[tfd].buff = buff; + tfdesc[tfd].size = filesize; + tfdesc[tfd].line = 0; + tfdesc[tfd].linecnt = 0; + return tfd; +} + +int +fr_open(const char *filename, size_t maxsize) +{ + /* reading data for internal use -> strict checking */ + return _fr_open(filename,maxsize,0); +} + +int +fr_open_preview(const char *filename, size_t maxsize) +{ + /* reading data for a preview -> relaxed checking */ + return _fr_open(filename,maxsize,1); +} + +static int +badtfd(int tfd) +{ + if ((tfd) < 0 || (tfd) >= TFDESC_CNT || !tfdesc[tfd].inuse) { + msgout(MSG_NOTICE, + "BUG: fr_xxx() called without a valid descriptor from fr_open()"); + return 1; + } + return 0; +} + +int +fr_close(int tfd) +{ + if (badtfd(tfd)) + return FR_ERROR; + + free(tfdesc[tfd].buff); + efree(tfdesc[tfd].line); + tfdesc[tfd].inuse = 0; + return FR_OK; +} + +int +fr_is_text(int tfd) +{ + size_t filesize; + const char *buff, *ptr; + int ch, ctrl; + + if (badtfd(tfd)) + return FR_ERROR; + + buff = tfdesc[tfd].buff; + filesize = tfdesc[tfd].size; + ctrl = 0; + for (ptr = buff; ptr < buff + filesize; ptr++) { + ch = *ptr & 0xFF; + if (ch == '\0' || (lang_data.utf8 && (ch == 0xFE || ch == 0xFF))) + return 0; + if (ch < 32) { + if (ch != '\n' && ch != '\t' && ch != '\r') + ctrl++; + } + else if (!lang_data.utf8 && ch >= 127) + ctrl++; + } + return 10 * ctrl < 3 * filesize; /* treshold = 30% ctrl + 70% text */ +} + +int +fr_is_truncated(int tfd) +{ + if (badtfd(tfd)) + return FR_ERROR; + + return tfdesc[tfd].truncated; +} + +static int +_fr_split(int tfd, size_t maxlines, int preview) +{ + size_t filesize; + char *buff, *line, *ptr; + int ch, ln; + int comment; /* -1 = don't know yet, 0 = no, 1 = yes */ + + if (badtfd(tfd)) + return FR_ERROR; + + if (tfdesc[tfd].line) + return FR_OK; /* everything is done already */ + + buff = tfdesc[tfd].buff; + filesize = tfdesc[tfd].size; + + /* terminate the last line if necessary */ + if (filesize && buff[filesize - 1] != '\n') + buff[filesize++] = '\n'; /* this is the extra byte allocated in fr_open() */ + + + /* split to lines */ + tfdesc[tfd].line = emalloc(maxlines * sizeof(const char *)); + for (ln = 0, ptr = buff; ptr < buff + filesize; ) { + line = ptr; + comment = preview ? 0 : -1; + while ( (ch = *ptr) && ch != '\r' && ch != '\n') { + if (ch == '\t' && !preview) + ch = *ptr = ' '; + if (comment < 0) { + if (ch == '#') + comment = 1; + else if (ch != ' ') + comment = 0; + } + ptr++; + } + if (ch == '\r' && ptr[1] == '\n') + *ptr++ = '\0'; + *ptr++ = '\0'; + + if (comment == 0) { + if (ln >= maxlines) { + tfdesc[tfd].linecnt = maxlines; + if (preview) { + tfdesc[tfd].truncated = 1; + return FR_OK; + } + msgout(MSG_NOTICE,"File \"%s\" is too big (too many lines)",tfdesc[tfd].filename); + return FR_LINELIMIT; + } + tfdesc[tfd].line[ln++] = line; + } + } + + tfdesc[tfd].linecnt = ln; + return FR_OK; +} + +/* split into lines, strip comments and empty lines */ +int +fr_split(int tfd, size_t maxlines) +{ + return _fr_split(tfd, maxlines,0); +} + +/* split into lines, but do not modify */ +int +fr_split_preview(int tfd, size_t maxlines) +{ + return _fr_split(tfd, maxlines,1); +} + +int +fr_linecnt(int tfd) +{ + return badtfd(tfd) ? -1 : tfdesc[tfd].linecnt; +} + +const char * +fr_line(int tfd, int lnum) +{ + if (badtfd(tfd)) + return 0; + if (tfdesc[tfd].line == 0 || lnum < 0 || lnum >= tfdesc[tfd].linecnt) + return 0; + return tfdesc[tfd].line[lnum]; +} + +/* file write functions */ + +#define WFILE_CNT 2 /* files being written at the same time (CLEX needs only 1) */ +typedef struct { + FILE *fp; /* fp returned by open() or NULL when unused */ + USTRING file, tmpfile; /* destination file name, temporary file name */ +} WFILE; +static WFILE wfile[WFILE_CNT]; + +/* + * output goes to the temporary file opened by fw_open + * + * if everything goes well, data is then moved to the final + * destination file in one atomic operation (fw_close) + * + * in the case of an error, the temporary file is removed + * and the destination file is left untouched + */ +FILE * +fw_open(const char *file) +{ + int i, errcode; + + for (i = 0; i < WFILE_CNT; i++) + if (wfile[i].fp == 0) { + us_copy(&wfile[i].file,file); + us_cat(&wfile[i].tmpfile,file,"-",clex_data.pidstr,".tmp",(char *)0); + break; + } + if (i == WFILE_CNT) { + msgout(MSG_NOTICE,"Internal file table is full in fw_open()"); + return 0; + } + + umask(clex_data.umask | 022); + errno = 0; + wfile[i].fp = fopen(USTR(wfile[i].tmpfile),"w"); + errcode = errno; + umask(clex_data.umask); + + if (wfile[i].fp == 0) { + msgout(MSG_NOTICE,"Cannot open \"%s\" for writing",USTR(wfile[i].tmpfile)); + if (errcode) + msgout(MSG_DEBUG," System error: %s",strerror(errcode)); + us_reset(&wfile[i].tmpfile); + } + return wfile[i].fp; +} + +int +fw_close(FILE *fp) +{ + int i, errcode; + const char *file, *tmpfile; + FLAG errflag; + + if (fp == 0) + return -1; + + for (i = 0; i < WFILE_CNT; i++) + if (wfile[i].fp == fp) + break; + if (i == WFILE_CNT) { + msgout(MSG_NOTICE,"BUG: fw_close() called without fw_open()"); + return -1; + } + file = USTR(wfile[i].file); + tmpfile = USTR(wfile[i].tmpfile); + + /* errflag = ferror(fp) || fclose(fp), but both functions must be called */ + if ( (errno = 0, errflag = ferror(fp), errflag = fclose(fp) || errflag) ) { + errcode = errno; + msgout(MSG_NOTICE,"Could not write data to \"%s\"",tmpfile); + } + else if ( errno = 0, errflag = rename(tmpfile,file) ) { + errcode = errno; + msgout(MSG_NOTICE,"Could not rename \"%s\" to \"%s\"",tmpfile,base_name(file)); + } + else + errcode = 0; + if (errcode) + msgout(MSG_DEBUG," System error: %s",strerror(errcode)); + + unlink(tmpfile); + us_reset(&wfile[i].tmpfile); + wfile[i].fp = 0; + + return errflag ? -1 : 0; +} + +void +fw_cleanup(void) +{ + int i; + const char *tmpfile; + + for (i = 0; i < WFILE_CNT; i++) { + tmpfile = USTR(wfile[i].tmpfile); + if (tmpfile && *tmpfile == '/') + unlink(tmpfile); + } +} diff --git a/src/filerw.h b/src/filerw.h new file mode 100644 index 0000000..b3b3c17 --- /dev/null +++ b/src/filerw.h @@ -0,0 +1,19 @@ +#define FR_OK 0 +#define FR_TRUNCATED 1 /* data exceeding given limits was ignored, otherwise OK */ +#define FR_NOFILE -1 /* file does not exist, caller decides if it is an error */ +#define FR_LINELIMIT -2 /* too many lines; this is partial success, that's why it is not FR_ERROR */ +#define FR_ERROR -9 /* an error has occurred and was logged */ + +extern int fr_open(const char *, size_t); +extern int fr_open_preview(const char *, size_t); +extern int fr_close(int); +extern int fr_is_text(int); +extern int fr_is_truncated(int); +extern int fr_split(int, size_t); +extern int fr_split_preview(int, size_t); +extern int fr_linecnt(int); +extern const char *fr_line(int, int); + +extern FILE *fw_open(const char *); +extern int fw_close(FILE *); +extern void fw_cleanup(void); diff --git a/src/filter.c b/src/filter.c new file mode 100644 index 0000000..c4de059 --- /dev/null +++ b/src/filter.c @@ -0,0 +1,395 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ + +#include "filter.h" + +#include "bookmarks.h" /* bm_panel_data() */ +#include "control.h" /* fopt_change() */ +#include "completion.h" /* compl_panel_data() */ +#include "directory.h" /* dir_panel_data() */ +#include "filepanel.h" /* files_condreread() */ +#include "history.h" /* hist_panel_data() */ +#include "inout.h" /* win_panel() */ +#include "list.h" /* file_panel_data() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr_set() */ +#include "mbwstring.h" /* utf_iscomposing() */ +#include "opt.h" /* opt_changed() */ +#include "panel.h" /* pan_adjust() */ +#include "userdata.h" /* user_panel_data() */ + +int +fopt_prepare(void) +{ + panel_fopt.pd->top = panel_fopt.pd->curs = panel_fopt.pd->min; + panel = panel_fopt.pd; + textline = 0; + return 0; +} + +void +cx_fopt_enter(void) +{ + TOGGLE(FOPT(panel_fopt.pd->curs)); + fopt_change(); + opt_changed(); + win_panel_opt(); +} + +/* write options to a string */ +const char * +fopt_saveopt(void) +{ + int i, j; + static char buff[FOPT_TOTAL_ + 1]; + + for (i = j = 0; i < FOPT_TOTAL_; i++) + if (FOPT(i)) + buff[j++] = 'A' + i; + buff[j] = '\0'; + + return buff; +} + +/* read options from a string */ +int +fopt_restoreopt(const char *opt) +{ + int i; + unsigned char ch; + + for (i = 0; i < FOPT_TOTAL_; i++) + FOPT(i) = 0; + + while ( (ch = *opt++) ) { + if (ch < 'A' || ch >= 'A' + FOPT_TOTAL_) + return -1; + FOPT(ch - 'A') = 1; + } + + return 0; +} + +void +cx_filteredit_begin(void) +{ + panel->filter->curs = 0; +} + +void +cx_filteredit_end(void) +{ + panel->filter->curs = panel->filter->size; +} + +void +cx_filteredit_left(void) +{ + FLAG move = 1; + + while (move && panel->filter->curs > 0) + move = utf_iscomposing(panel->filter->line[--panel->filter->curs]); +} + +void +cx_filteredit_right(void) +{ + FLAG move = 1; + + while (move && panel->filter->curs < panel->filter->size) + move = utf_iscomposing(panel->filter->line[++panel->filter->curs]); +} + +void +cx_filteredit_kill(void) +{ + panel->filter->line[panel->filter->curs = panel->filter->size = 0] = L'\0'; + panel->filter->changed = 1; + win_filter(); +} + +/* delete 'cnt' chars at cursor position */ +static void +delete_chars(int cnt) +{ + int i; + + panel->filter->size -= cnt; + for (i = panel->filter->curs; i <= panel->filter->size; i++) + panel->filter->line[i] = panel->filter->line[i + cnt]; + panel->filter->changed = 1; +} + +void +cx_filteredit_backsp(void) +{ + int pos; + FLAG del = 1; + + if (panel->filter->curs == 0) + return; + + pos = panel->filter->curs; + /* composed UTF-8 characters: delete also the combining chars */ + while (del && panel->filter->curs > 0) + del = utf_iscomposing(panel->filter->line[--panel->filter->curs]); + delete_chars(pos - panel->filter->curs); + win_filter(); +} + +void +cx_filteredit_delchar(void) +{ + int pos; + FLAG del = 1; + + if (panel->filter->curs == panel->filter->size) + return; + + pos = panel->filter->curs; + /* composed UTF-8 characters: delete also the combining chars */ + while (del && panel->filter->curs < panel->filter->size) + del = utf_iscomposing(panel->filter->line[++pos]); + delete_chars(pos - panel->filter->curs); + win_filter(); +} + +/* delete until the end of line */ +void +cx_filteredit_delend(void) +{ + panel->filter->line[panel->filter->size = panel->filter->curs] = L'\0'; + panel->filter->changed = 1; + win_filter(); +} + +/* make room for 'cnt' chars at cursor position */ +static wchar_t * +insert_space(int cnt) +{ + int i; + wchar_t *ins; + + if (panel->filter->size + cnt >= INPUT_STR) + return 0; + + ins = panel->filter->line + panel->filter->curs; /* insert new character(s) here */ + panel->filter->size += cnt; + panel->filter->curs += cnt; + for (i = panel->filter->size; i >= panel->filter->curs; i--) + panel->filter->line[i] = panel->filter->line[i - cnt]; + + panel->filter->changed = 1; + return ins; +} + +void +filteredit_nu_insertchar(wchar_t ch) +{ + wchar_t *ins; + + if ( (ins = insert_space(1)) ) + *ins = ch; +} + +void +filteredit_insertchar(wchar_t ch) +{ + filteredit_nu_insertchar(ch); + win_filter(); +} + +/* * * filter_update functions * * */ + +/* dir_panel_data() wrapper that preserves the cursor position */ +static void +dir_panel_data_wrapper(void) +{ + int i, save_curs_rel; + const char *save_curs_dir; + + /* save cursor */ + if (VALID_CURSOR(panel_dir.pd)) { + save_curs_dir = panel_dir.dir[panel_dir.pd->curs].name; + save_curs_rel = (100 * panel_dir.pd->curs) / panel_dir.pd->cnt; /* percentage */ + } + else { + save_curs_dir = 0; + save_curs_rel = 0; + } + + /* apply filter */ + dir_panel_data(); + + /* restore cursor */ + if (save_curs_dir) + for (i = 0; i < panel_dir.pd->cnt; i++) + if (panel_dir.dir[i].name == save_curs_dir) { + panel_dir.pd->curs = i; + return; + } + panel_dir.pd->curs = (save_curs_rel * panel_dir.pd->cnt) / 100; +} + +/* match the whole help line */ +static int +match_help(int ln) +{ + HELP_LINE *ph; + int i; + + ph = panel_help.line[ln]; + if (match_substr_ic(ph->text)) + return 1; + for (i = 0; i < ph->links; i++) + if (match_substr_ic(ph[i * 3 + 2].text) || match_substr_ic(ph[i * 3 + 3].text)) + return 1; + + return 0; +} + +static void +filter_update_help(void) +{ + int i; + + match_substr_set(panel_help.pd->filter->line); + + /* for "find" function is the 'changed' flag set; for "find next" is the flag unset */ + if (panel->filter->changed && match_help(panel_help.pd->curs)) + return; + for (i = panel_help.pd->curs + 1; i < panel_help.pd->cnt; i++) + if (match_help(i)) { + panel_help.pd->curs = i; + return; + } + msgout(MSG_i,"end of page reached, searching from the top"); + for (i = 0; i <= panel_help.pd->curs; i++) + if (match_help(i)) { + panel_help.pd->curs = i; + return; + } + msgout(MSG_i,"text not found"); +} + +void +filter_update(void) +{ + switch (panel->type) { + case PANEL_TYPE_BM: + bm_panel_data(); + break; + case PANEL_TYPE_COMPL: + compl_panel_data(); + break; + case PANEL_TYPE_DIR: + dir_panel_data_wrapper(); + break; + case PANEL_TYPE_FILE: + file_panel_data(); + break; + case PANEL_TYPE_GROUP: + group_panel_data(); + break; + case PANEL_TYPE_HELP: + filter_update_help(); + break; + case PANEL_TYPE_HIST: + hist_panel_data(); + break; + case PANEL_TYPE_LOG: + log_panel_data(); + break; + case PANEL_TYPE_USER: + user_panel_data(); + break; + default: + ; + } + panel->filter->changed = 0; + pan_adjust(panel); + win_panel(); +} + +void +filter_off(void) +{ + panel->filtering = 0; + if (panel->type == PANEL_TYPE_HELP) + win_panel_opt(); + else + filter_update(); + win_filter(); + if (panel->curs < 0) + win_infoline(); +} + +void +filter_help(void) +{ + if (panel->filtering == 1) + win_sethelp(HELPMSG_OVERRIDE,panel->type != PANEL_TYPE_HELP ? + L"alt-O = filter options" : L"ctrl-F = find next"); + else + win_sethelp(HELPMSG_OVERRIDE,0); +} + +void +cx_filter(void) +{ + if (panel->filter == 0) { + msgout(MSG_i,"this panel does not support filtering"); + return; + } + + if (panel->filtering == 0) { + if (panel->type == PANEL_TYPE_FILE) { + if (list_directory_cond(PANEL_EXPTIME) == 0) + win_panel(); + ppanel_file->filtype = 0; + } + panel->filtering = 1; + cx_filteredit_kill(); + panel->filter->changed = 0; + if (panel->type == PANEL_TYPE_HELP) + win_panel_opt(); + if (panel->curs < 0) + win_infoline(); + } + else if (panel->filter->size == 0) + filter_off(); + else if (panel->type == PANEL_TYPE_HELP) + /* find next */ + filter_update(); + else if (textline == 0) + filter_off(); + else + /* switch focus: filter line (1) <--> input line (2) */ + panel->filtering = 3 - panel->filtering; + + filter_help(); +} + +/* filepanel filter controlled from the main menu */ +void +cx_filter2(void) +{ + panel = ppanel_file->pd; + cx_filter(); + panel = panel_mainmenu.pd; +} diff --git a/src/filter.h b/src/filter.h new file mode 100644 index 0000000..795def4 --- /dev/null +++ b/src/filter.h @@ -0,0 +1,19 @@ +extern int fopt_prepare(void); +extern const char *fopt_saveopt(void); +extern int fopt_restoreopt(const char *); +extern void cx_fopt_enter(void); +extern void cx_filteredit_begin(void); +extern void cx_filteredit_end(void); +extern void cx_filteredit_left(void); +extern void cx_filteredit_right(void); +extern void cx_filteredit_kill(void); +extern void cx_filteredit_backsp(void); +extern void cx_filteredit_delchar(void); +extern void cx_filteredit_delend(void); +extern void filteredit_insertchar(wchar_t); +extern void filteredit_nu_insertchar(wchar_t); +extern void filter_update(void); +extern void filter_off(void); +extern void filter_help(void); +extern void cx_filter(void); +extern void cx_filter2(void); diff --git a/src/help.c b/src/help.c new file mode 100644 index 0000000..f930d35 --- /dev/null +++ b/src/help.c @@ -0,0 +1,526 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* + * Help-file is a plain text file created from the HTML source + * by a conversion utility which is available on request. + * The help-file should not be edited by hand. + * + * Format of the help file: + * #comment (can appear anywhere in the file) + * $V=VERSION + * + * + * .... + * + * + * where is: + * $P=page_name + * $T=title + * + * + * .... + * + * + * where is either: + * literal_text + * or text with one or more links (displayed as one line): + * text_appearing_before_the_first_link + * (repeated 1 or more times) + * + * each consists of three lines: + * $L=page_name + * text_of_the_link (displayed highlighted) + * text_appearing_after_the_link + */ + +#include "clexheaders.h" + +#include /* va_list */ +#include /* vprintf() */ +#include /* free() */ +#include /* strcmp() */ + +#include "help.h" + +#include "cfg.h" /* cfg_str() */ +#include "control.h" /* get_previous_mode() */ +#include "inout.h" /* win_panel() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2mb() */ +#include "panel.h" /* pan_adjust() */ +#include "util.h" /* emalloc() */ + +/* limits to protect resources */ +#define HELP_PAGES_LIMIT 80 /* pages max */ +#define HELP_FILESIZE_LIMIT 125000 /* bytes max */ + +/* error handling in the parse function */ +static FLAG helperror; /* there was an error */ + +/* source help data: internal or external */ +static const char *internal_help[] = { +#include "help.inc" + "" +}; +/* NOTE: support for external help file will be added later */ + +/* processed help data */ +static HELP_LINE *helpline = 0; + +/* help data pages */ +typedef struct { + FLAG valid; + const char *name; /* page name (for links) */ + const wchar_t *title; /* title to be displayed */ + int firstline; /* number the first line */ + int size; /* total number of lines in this page */ +} HELP_PAGE; +static HELP_PAGE *helppage = 0; /* help lines split into pages */ +static int pagecnt; /* number of pages in 'helppage[]' */ + +/* line numbers are helpline[] indices */ +/* page numbers are helppage[] indices */ + +/* the "MAIN" page contains generated list of links */ +static int mainpage; /* "MAIN" page number */ +static int mainsize; /* size of the page excluding the generated links */ +static int mainlink[MAIN_LINKS];/* line numbers of links */ + +/* help history - allows only to go back to previous page */ +#define HELP_HISTORY 16 +static struct { + int pagenum, top, curs; +} history[HELP_HISTORY]; +static int head, tail; /* circular buffer indices */ + +/* used by the help text parser */ +#define HL_TEXT 0 /* regular text */ +#define HL_TEXTLINK 1 /* text of a link (highlighted) */ +#define HL_LINK 2 /* data of a link */ +#define HL_TITLE 3 /* page title */ +#define HL_PAGE 10 /* start of a new page */ +#define HL_VERSION 11 /* help data version */ +#define HL_IGNORE 20 /* invalid input, ignored */ +#define HL_END 99 /* last entry in use */ + +#define IS_HL_CONTENTS(X) ((X) < 10) /* this item is part of a help page */ + +static int +page2num(const char *pagename) +{ + int i; + + for (i = 0; i < pagecnt; i++) + if (helppage[i].valid && strcmp(helppage[i].name,pagename) == 0) + return i; + return -1; /* no such page */ +} + +static void +help_error(const char *format, ...) +{ + va_list argptr; + + helperror = 1; + + va_start(argptr,format); + vmsgout(MSG_NOTICE,format,argptr); + va_end(argptr); +} + +static const char * +help_getline(int n) +{ + /* if (external) return fr_line(tfd,n); */ + return internal_help[n]; +} + +static int +help_linecnt(void) +{ + /* if (external) return fr_linecnt(tfd); */ + return ARRAY_SIZE(internal_help); +} + +/* parse_help() returns -1 on a serious error (help unusable) */ +static int +parse_help(void) +{ + FLAG pagestart; + const char *ptr; + int pg, page, i, j, ml, cnt, max; + char ch; + + cnt = help_linecnt(); + if (helpline) { + for (i = 0; helpline[i].type != HL_END; i++) + efree((void *)helpline[i].text); /* overriding const qualifier */ + free(helpline); + } + helpline = emalloc((cnt + 1) * sizeof(HELP_LINE)); + + /* first pass: read and analyze the input lines, count pages */ + pagecnt = 0; + for (i = 0; i < cnt; i++) { + ptr = help_getline(i); + if (ptr[0] == '$' && (ch = ptr[1]) != '\0' && ptr[2] == '=') { + ptr += 3; + helpline[i].data = ptr; + helpline[i].text = 0; + switch (ch) { + case 'L': + helpline[i].type = HL_LINK; + break; + case 'P': + if (++pagecnt > HELP_PAGES_LIMIT) { + helpline[i].type = HL_END; + help_error("Too many help pages, allowed is ",STR(HELP_PAGES_LIMIT)); + return -1; + } + helpline[i].type = HL_PAGE; + break; + case 'T': + helpline[i].type = HL_TITLE; + helpline[i].text = ewcsdup(convert2w(ptr)); + break; + case 'V': + helpline[i].type = HL_VERSION; + default: + helpline[i].type = HL_IGNORE; + help_error("Invalid control sequence $%c=",ch); + } + } + else { + helpline[i].type = HL_TEXT; + helpline[i].data = 0; + helpline[i].text = ewcsdup(convert2w(ptr)); + } + } + helpline[i].type = HL_END; + helpline[i].data = 0; + helpline[i].text = 0; + + efree(helppage); + helppage = emalloc(pagecnt * sizeof(HELP_PAGE)); + + /* second pass: break help to pages, some checks */ + for (pagestart = 0, page = -1 /* current page */, i = 0; i < cnt; i++) { + if (page < 0 && IS_HL_CONTENTS(helpline[i].type)) { + help_error("Unexpected text before the start of the first page"); + helpline[i].type = HL_IGNORE; + continue; + } + switch (helpline[i].type) { + case HL_TEXT: + if (pagestart) { + helppage[page].firstline = i; + pagestart = 0; + } + helppage[page].size++; + break; + case HL_LINK: + if (pagestart || helpline[i - 1].type != HL_TEXT + || helpline[i + 1].type != HL_TEXT || helpline[i + 2].type != HL_TEXT) { + help_error("Link \"%s\" is not correctly embedded in text",helpline[i].data); + helpline[i].type = HL_IGNORE; + } + else { + helpline[i + 1].type = HL_TEXTLINK; + helppage[page].size--; + } + break; + case HL_PAGE: + for (pg = 0; pg <= page; pg++) + if (helppage[pg].valid && strcmp(helppage[pg].name,helpline[i].data) == 0) { + help_error("Existing page \"%s\" is redefined",helpline[i].data); + helppage[pg].valid = 0; + break; + } + if (pagestart) + help_error("Page \"%s\" is empty",helppage[page].name); + + pagestart = 1; + page++; + helppage[page].name = helpline[i].data; + helppage[page].title = L"Untitled"; + helppage[page].size = 0; + helppage[page].valid = 1; + break; + case HL_TITLE: + helppage[page].title = helpline[i].text; + break; + case HL_VERSION: + if (strcmp(helpline[i].data,VERSION)) + help_error("Help file version \"%s\" does not match the program version.\n" + " Information in the on-line help might be inaccurate or outdated.", + helpline[i].data); + break; + } + } + + /* final pass: check for broken links, count links per line */ + for (i = 0; i < cnt; i++) { + if (helpline[i].type == HL_LINK && page2num(helpline[i].data) < 0) + help_error("Broken link: %s",helpline[i].data); + + if (helpline[i].type == HL_TEXT) { + for (j = 0; helpline[i + 3 * j + 1].type == HL_LINK ;j++) + ; + helpline[i].links = j; + } + } + + /* the MAIN page */ + if ( (mainpage = page2num("MAIN")) < 0) { + help_error("Required page \"MAIN\" is missing"); + return -1; + } + for (i = 0, j = helppage[mainpage].firstline, ml = 0; + i < helppage[mainpage].size && ml < MAIN_LINKS; i++, j++) { + while (helpline[j].type != HL_TEXT || helpline[j - 1].type == HL_TEXTLINK) + j++; + if (helpline[j].links == 1 && strcmp(helpline[j + 1].data,"MAIN") == 0) + mainlink[ml++] = j; + } + if (ml != MAIN_LINKS) { + help_error("The \"MAIN\" page is invalid"); + return -1; + } + mainsize = helppage[mainpage].size - MAIN_LINKS; + + for (max = helppage[0].size, pg = 1; pg < pagecnt; pg++) + if (helppage[pg].size > max) + max = helppage[pg].size; + efree(panel_help.line); + panel_help.line = emalloc(max * sizeof(HELP_LINE)); + + return 0; +} + +void +help_initialize(void) +{ + helperror = 0; + msgout(MSG_HEADING,"HELP: reading the help data"); + parse_help(); + msgout(MSG_HEADING,0); + if (helperror) { + msgout(MSG_w,"Error(s) in the help data detected, details in the log"); + err_exit("BUG: built-in help is incorrect"); + } +} + +static void +set_page(int pg) +{ + int i, j; + FLAG curs_set = 0; + + panel_help.pd->top = panel_help.pd->curs = panel_help.pd->min; + panel_help.pd->cnt = helppage[pg].size; + panel_help.pagenum = pg; + for (i = 0, j = helppage[pg].firstline; i < panel_help.pd->cnt; i++) { + while (helpline[j].type != HL_TEXT || helpline[j - 1].type == HL_TEXTLINK) + j++; + panel_help.line[i] = helpline + j++; + /* place the cursor one line above the first visible link */ + if (!curs_set && i < disp_data.panlines && helpline[j].type == HL_LINK) { + if (i > 0) + panel_help.pd->curs = i - 1; + curs_set = 1; + } + } + panel_help.lnk_act = panel_help.lnk_ln = 0; + + panel_help.title = helppage[pg].title; + win_title(); +} + +static void +help_goto(const char *pagename) +{ + int pg; + + if ( (pg = page2num(pagename)) < 0) { + /* ERROR 404 :-) */ + msgout(MSG_w,"HELP: help-page '%s' not found",pagename); + return; + } + + /* save current page parameters */ + history[head].pagenum = panel_help.pagenum; + history[head].top = panel_help.pd->top; + history[head].curs = panel_help.pd->curs; + head = (head + 1) % HELP_HISTORY; + if (head == tail) + tail = (tail + 1) % HELP_HISTORY; + + set_page(pg); + win_panel(); +} + +static int +link_add(int ln, const char *page) +{ + int pg; + + if (page == 0) { + helpline[mainlink[ln] + 0].type = HL_IGNORE; + helpline[mainlink[ln] + 1].type = HL_IGNORE; + helpline[mainlink[ln] + 2].type = HL_IGNORE; + helpline[mainlink[ln] + 3].type = HL_IGNORE; + return 0; + } + + if ((pg = page2num(page)) < 0) { + msgout(MSG_NOTICE,"Missing help page \"%s\"",page); + return -1; + } + helpline[mainlink[ln] + 0].type = HL_TEXT; + helpline[mainlink[ln] + 1].type = HL_LINK; + helpline[mainlink[ln] + 1].data = page; + helpline[mainlink[ln] + 2].type = HL_TEXTLINK; + helpline[mainlink[ln] + 2].text = helppage[pg].title; + helpline[mainlink[ln] + 3].type = HL_TEXT; + return 0; +} + +int +help_prepare(void) +{ + int i, link; + const char **pages; + + /* context sensitive help */ + link = 0; + if (panel->filtering && link_add(link,"filter") >= 0) + link++; + pages = mode2help(get_previous_mode()); + for (i = 0; i < MAIN_LINKS - 1 && pages[i] != 0; i++) + if (link_add(link,pages[i]) >= 0) + link++; + helppage[mainpage].size = mainsize + link; + for (; link < MAIN_LINKS; link++) + link_add(link,0); + set_page(mainpage); + + head = tail = 0; + panel_help.pd->filtering = 0; + panel = panel_help.pd; + textline = 0; + + return 0; +} + +/* follow link */ +void +cx_help_link(void) +{ + HELP_LINE *ph; + int active; + + ph = panel_help.line[panel_help.pd->curs]; + if (ph->links == 0) + return; + active = panel_help.pd->curs == panel_help.lnk_ln ? panel_help.lnk_act : 0; + help_goto(ph[3 * active + 1].data); +} + +/* display main page */ +void +cx_help_main(void) +{ + if (panel_help.pagenum != mainpage) + help_goto("MAIN"); +} + +/* go back to previous page */ +void +cx_help_back(void) +{ + if (head == tail) { + msgout(MSG_i,"there is no previous help-page"); + return; + } + + head = (head + HELP_HISTORY - 1) % HELP_HISTORY; + set_page(history[head].pagenum); + panel_help.pd->top = history[head].top; + panel_help.pd->curs = history[head].curs; + pan_adjust(panel_help.pd); + win_panel(); +} + +void +cx_help_up(void) +{ + if (panel_help.lnk_ln == panel->curs && panel_help.lnk_act > 0) + panel_help.lnk_act--; + else if (panel->curs <= panel->min) + return; + else { + panel->curs--; + LIMIT_MAX(panel->top,panel->curs); + panel_help.lnk_ln = panel->curs; + panel_help.lnk_act = panel_help.line[panel->curs]->links - 1; + } + win_panel_opt(); +} + +void +cx_help_down(void) +{ + if (panel_help.lnk_ln == panel->curs + && panel_help.lnk_act + 1 < panel_help.line[panel->curs]->links) + panel_help.lnk_act++; + else if (panel_help.lnk_ln != panel->curs && panel_help.line[panel->curs]->links > 1) { + panel_help.lnk_ln = panel->curs; + panel_help.lnk_act = 1; + } + else if (panel->curs >= panel->cnt - 1) + return; + else { + panel->curs++; + LIMIT_MIN(panel->top,panel->curs - disp_data.panlines + 1); + panel_help.lnk_ln = panel->curs; + panel_help.lnk_act = 0; + } + win_panel_opt(); +} + +void +cx_help_mouse(void) +{ + switch (minp.area) { + case AREA_PANEL: + if (MI_CLICK && VALID_CURSOR(panel) && minp.cursor >= 0) { + panel_help.lnk_ln = panel->curs; + panel_help.lnk_act = minp.cursor; + win_panel_opt(); + } + break; + case AREA_BAR: + if (MI_DC(1)) { + if (minp.cursor == 0) { + cx_help_main(); + minp.area = AREA_NONE; + } + else if (minp.cursor == 2) { + cx_help_back(); + minp.area = AREA_NONE; + } + } + } +} diff --git a/src/help.h b/src/help.h new file mode 100644 index 0000000..215c4b3 --- /dev/null +++ b/src/help.h @@ -0,0 +1,10 @@ +extern void help_initialize(void); +extern int help_prepare(void); +extern void cx_help_link(void); +extern void cx_help_back(void); +extern void cx_help_main(void); +extern void cx_help_up(void); +extern void cx_help_down(void); +extern void cx_help_mouse(void); + +#define MAIN_LINKS 5 /* max number of generated links in the MAIN page */ diff --git a/src/help_en.hlp b/src/help_en.hlp new file mode 100644 index 0000000..2646857 --- /dev/null +++ b/src/help_en.hlp @@ -0,0 +1,2903 @@ +# CLEX help file +# Converted from HTML source +# Do not edit; changes will be overwritten +$P=2dirs +$T=Working directories + There are two file panels and two corresponding working + directories: + + 1. The contents of the normal current working directory + are listed in the primary file panel on the screen. + 2. The so-called secondary working directory is used as a + destination for copy and move commands. It is + associated with the secondary file panel which is not + displayed on the screen. + + Initial working directories can be set as +$L=bookmarks +bookmarks +. + + Overview of related functions: + + ctrl-X exchange primary and secondary panels + alt-= +$L=compare +compare files + in both directories and mark + differences + ctrl-E insert the name of the secondary working + directory + ctrl-E insert the name of the current working + directory + mapped to 'COPY' and will copy the files from + the current directory to the secondary + directory + as for but mapped to 'MOVE' +$P=MAIN +$T=Help topics +Help for the currently selected function +======================================== + + * +$L=MAIN +1 + + * +$L=MAIN +2 + + * +$L=MAIN +3 + + * +$L=MAIN +4 + + * +$L=MAIN +5 + + +General help +============ + + * +$L=index +Index + + * Help with keys: +$L=keys_control +CLEX control keys + + * Help with keys: +$L=keys_scroll +panel-scrolling keys + + * Help with keys: +$L=keys_edit +basic editing commands + + * +$L=filter +Using the panel filter + + * +$L=notes +Usage notes + + +Introduction for new users +========================== + + * +$L=screen +The screen + + * +$L=keys +The keyboard + + * +$L=mouse +The mouse + + * +$L=help +How to use the on-line help + + +About CLEX +========== + + * +$L=about +Project CLEX + + * +$L=changelog +What's new in this release + +$P=about +$T=About CLEX + CLEX (pronounced KLEKS) is free software without warranty + of any kind. See the +$L=license +software license + for details. + + The project homepage is https://github.com/xitop/clex. + Please report any errors, typos or other issues there. + + Many thanks to all the people who have taken the time to + submit suggestions for enhancements or problem reports. + + CLEX File Manager + Copyright (C) 2001-2022 Vlado Potisk +$P=accounts +$T=User account data caching + In order to speed up its operations, CLEX internally caches + the data about user and group accounts. This data is mainly + used to obtain the file owners' names. It does not change + very often. + + The cache will be updated before reading contents of a + directory, if: + + * modification of '/etc/passwd' or '/etc/group' is + detected, or + * cached data has expired after five minutes. + + This means that if the account data is not stored in the + standard system files, but e.g. in a 'NIS' network + database, it could take few minutes before CLEX updates its + cache. To overcome this slight limitation you can press + before ctrl-R to force a refresh of the account data + cache. +$P=bookmarks +$T=Bookmark panel + Bookmarks are a list of stored directory names. To change + directory, select one of your bookmarks and press . + + Two bookmarks have special meaning when CLEX starts. If a + bookmark named 'DIR1' exists, this directory will become + the new working directory and will be listed in the primary + panel. If a bookmark named 'DIR2' exists, that directory + will become the working directory of the secondary panel. + + If 'DIR1' is not defined, CLEX will start normally in the + current working directory. If 'DIR2' is not defined, the + home directory will be displayed in the secondary panel. + + There is also a set of maintenance functions in the panel. + + D move the entry down + U move the entry up + N insert new entry (to appear below the cursor bar) + P +$L=bookmarks_edit +edit properties + + remove the bookmark + change into the bookmarked directory + + After finishing your work, select 'Exit this panel'. If you + do not want to save the bookmark list, choose 'Cancel' + instead. + + +$L=filter +Filtering + (ctrl-F) is supported in this panel, but the + maintenance functions are disabled while a panel filter is + active. + + ----------------------------------------------------------- + + Notes: + + * The current directory can be bookmarked from the file + panel with ctrl-D. + * Bookmarks are stored in a file named + '~/.config/clex/bookmarks'. + * Concurrent CLEX sessions: bookmarks changed in another + session are loaded automatically every time the + bookmark panel is entered. + * Both 'DIR1' and 'DIR2' should be absolute directory + names starting with a slash ('/'). +$P=bookmarks_edit +$T=Bookmark properties + Each bookmark has two properties: the directory and an + optional name. Both can be modified if necessary. + + Bookmarked directories must be absolute directories, i.e. + starting from the root directory '/'. +$P=cd +$T=The cd command + The 'cd' command changes the current directory. Simply type + + cd /some/directory + + at the command line and press . + + To make this work, CLEX treats the 'cd' command as a + special case and processes it internally. + + It is required that the command is in the form of a single + command and all special shell characters occurring in the + directory name are quoted, otherwise the command will not + be processed internally. Normal +$L=tilde +tilde substitution + is + supported however. + + The 'cd' command is an alternative to using the + +$L=dir +directory panel + to change directories. + +Examples: +--------- + + This command + + cd /var/log + + will be processed internally and will change the working + directory, while this command + + cd /dev ; ls -l lp* + + will be executed normally, i.e. by a separate process and + the effect of 'cd' will be lost after returning to CLEX. + + ----------------------------------------------------------- + + Notes: + + * The special shell characters mentioned above are listed + here: +$L=cfg_other +'QUOTE' parameter +. +$P=cfg +$T=Configuring CLEX + When running CLEX for the first time or when upgrading from + a previous version, it is recommended to run the 'cfg-clex' + utility. 'cfg-clex' builds the initial configuration file + or migrates existing files from an older version. + + ----------------------------------------------------------- + + There are about +$L=cfg_parameters +30 parameters + controlling the program's + behavior. + + Each user can customize his or her personal CLEX settings + using the configuration panel. Program settings can be (and + should be) saved to a configuration file and thus made + permanent. Unsaved settings are lost when you exit CLEX. + + The configuration file is named 'config' and is located in + the '.config/clex' subdirectory of user's home directory. + Please read this related +$L=security +security advice +. + + Standard configuration values apply to all parameters which + are not explicitly set by the user. These default values + are built into CLEX. + + Help with keys: + + edit parameter + O set parameter to the original value + S reset parameter to the standard (default) value + + Choose one of the exit functions in the top of the panel + (Cancel, Apply, or Apply+Save) to finish the work in the + configuration panel. + + ----------------------------------------------------------- + + Notes: + + * Concurrent CLEX sessions - configuration saved in one + session does not affect other running sessions, only + newly started programs. Every time you save the + configuration, the previous one gets overwritten. +$P=cfg_app +$T=List of configurable parameters 1/3 + The parameters below affect the program's visual + appearance. + + Warning: do not change parameters you do not understand. If + you are unsure, test the new value before saving the + configuration. If any problems occur, reset the parameter + to standard value. + + 'FRAME' + Choose a character for drawing horizontal lines on + the screen. Try all settings and select which one + looks best on your screen. The option 'line + graphics' does not work in some terminals or with + some fonts. + + 'CMD_LINES' + Number of lines (2-4) that the command or input + line occupies on the screen. + + 'XTERM_TITLE' + Change the title of the terminal window when CLEX + runs in an X Window environment. CLEX tries to + restore the original title at exit. + The title is: + + * 'clex: user@host' - ready + * 'clex: command' - executing a command + * '[clex: command]' - execution is finished + + 'PROMPT' + Command line prompt. The default value is (without + the quotes): + + '$s $p ' + + Following substitutions are performed: + + * '$h' --> hostname + * '$p' --> prompt character according to the + shell (e.g. $ or #) + * '$s' --> shell name + * '$u' --> username + * '$w' --> current working directory + (abbreviated) + * '$$' --> literal $ + + If you have superuser privileges, the word 'ROOT' + is included with the prompt to remind you to be + careful. This is a security feature that cannot be + turned off. + + 'LAYOUT1' + File panel layout. Format is: + + | + + where 'fields' is a +$L=layout +list of fields + to be + displayed. + + 'LAYOUT2' + Alternative +$L=layout +file panel layout +. + + 'LAYOUT3' + Alternative +$L=layout +file panel layout +. + + 'LAYOUT_ACTIVE' + Pick one of the layouts 1 to 3. + + 'KILOBYTE' + Select your preferred definition of one kilobyte + (megabyte, etc.) The filesize is displayed + according to this parameter. + + * '1000' - the prefix kilo means thousand + * '1024' - for computer professionals one + kilobyte is traditionally 1024 bytes. This is + now written as 1 KiB according to the IEC + International Standard 60027-2. + + 'TIME_FMT' + +$L=format +strftime-style time format string + or AUTO for + autodetect. + + 'DATE_FMT' + +$L=format +strftime-style date format string + or AUTO for + autodetect. + + 'TIME_DATE' + The short format displays time of the day for + recent timestamps (i.e. same day or not more than + 16 hours ago). Otherwise the date is displayed. The + two long formats display both date and time, date + first or time first. + + ----------------------------------------------------------- + + Notes: + + * The autodetect feature of 'TIME_FMT' and 'DATE_FMT' + requires a properly set +$L=locale +locale +. +$P=cfg_cmd +$T=List of configurable parameters 2/3 + These parameters control the execution of commands. + + Warning: do not change parameters you do not understand. If + you are unsure, test the new value before saving the + configuration. If any problems occur, reset the parameter + to standard value. + + 'CMD_F3' + = view file(s) + + 'CMD_F4' + = edit file(s) + + 'CMD_F5' + = copy file(s) + + 'CMD_F6' + = move file(s) + + 'CMD_F7' + = make directory + + 'CMD_F8' + = remove file(s) + + 'CMD_F9' + = print file(s) + + 'CMD_F10' + = user defined + + 'CMD_F11' + = user defined + + 'CMD_F12' + = user defined + + All these 'CMD_Fx' parameters are templates used to build + commands each time you press one of the function keys + to . + +Substitutions +------------- + + The following substitutions are performed before the + specified text is inserted into the command line: + + * '$1' --> name of the +$L=2dirs +current working directory + + * '$2' --> name of the +$L=2dirs +secondary directory + + * '$F' --> name of the current file + * '$S' --> name(s) of the selected file(s) ('$f' is + preferred) + * '$f' --> like '$S' when was pressed before the + last keystroke and there are some selected files; like + '$F' otherwise + * '$$' --> single '$' character + + All inserted file and directory names are +$L=quoting +quoted + + properly. + +Control sequences +----------------- + + * '$c' sets the cursor position, e.g: + + find / -type f -name '$c' -print + + Normal cursor position (without the '$c') is at the end + of the inserted command. + * '$:' deletes all text (like ctrl-U). It should be put + at the very beginning of the line because everything + before '$:' will be discarded. + * '$!' executes the command like pressing the key + immediately afterward. + * '$~' disables the 'Press enter to continue' prompt. The + control is returned to CLEX immediately after the + command execution terminates provided that: + + * the command has not been modified; and + * the command terminates successfully (exit code + zero). + + CAUTION: '$!' allows automatic execution of commands. Use + it carefully: + + * combine it with '$:' in order to obtain full control + over the command line + * don't use it for destructive actions like deleting + data, etc. + + Automatic execution with '$!' implies a check for a 'rm' + command and suppresses warning about a long command line. + See the +$L=notify +notification panel + for more information on this. +$P=cfg_mouse +$T=List of configurable parameters 3/3 + Warning: do not change parameters you do not understand. If + you are unsure, test the new value before saving the + configuration. If any problems occur, reset the parameter + to standard value. + + 'MOUSE' + This parameter controls the support for a + +$L=mouse +computer mouse +. The values are: 'disabled', + 'enabled:right-handed' (default), and + 'enabled:left-handed'. A left handed mouse is a + mouse with left and right button swapped. + + 'MOUSE_SCROLL' + Mouse wheel scrolls the cursror bar by this number + of panel lines. + + 'DOUBLE_CLICK' + Mouse double click interval in milliseconds. +$P=cfg_other +$T=List of configurable parameters 3/3 + Warning: do not change parameters you do not understand. If + you are unsure, test the new value before saving the + configuration. If any problems occur, reset the parameter + to standard value. + + 'QUOTE' + Some characters have a special meaning for the + shell. If these special characters appear in a + filename being inserted into the command line, they + must be taken literally, without the special + meaning. This is achieved by quoting them with a + preceding backslash. + + These characters are always quoted: + + ( ) < > [ ] { } # $ & \ | ? * ; ' " ` ~ space tab + + Another two characters are added to the list if + your shell is a C-shell (see notes below): + + ! : + + The 'QUOTE' parameter allows you to specify a list + of additional characters that need to be quoted. + + 'C_PANEL_SIZE' + Size of the name completion panel, i.e. its maximum + number of lines. + + 'D_PANEL_SIZE' + Size of the directory panel. The value 'AUTO' + limits the panel size to the actual screen size. + + 'H_PANEL_SIZE' + Size of the command history panel. + + ----------------------------------------------------------- + + Notes: + + * Screen size 'AUTO' for 'D_PANEL_SIZE' parameter leaves + the bottom panel line blank to indicate that there is + no need to scroll. + * Changing 'H_PANEL_SIZE' clears the contents of the + command history list. + * Your shell is considered to be a C-shell when its name + ends with 'csh'. +$P=cfg_parameters +$T=Configuration parameters + +$L=cfg_app +configuring appearance + + FRAME, CMD_LINES, XTERM_TITLE, PROMPT, LAYOUT1, LAYOUT2, + LAYOUT3, LAYOUT_ACTIVE, KILOBYTE, TIME_FMT, DATE_FMT, + TIME_DATE + + +$L=cfg_cmd +configuring command execution + + CMD_F3 - CMD_F12 + + +$L=cfg_mouse +configuring a mouse + + MOUSE, MOUSE_SCROLL, DOUBLE_CLICK + + +$L=cfg_other +other configuration parameters + + QUOTE, C_PANEL_SIZE, D_PANEL_SIZE, H_PANEL_SIZE +$P=changelog +$T=Change Log +4.7 released on 15-AUG-2021 +=========================== + +Important announcement: +----------------------- + + * CLEX has moved to GitHub, all links were updated. The + new project home is https://github.com/xitop/clex + +Problems fixed: +--------------- + + * Fixed a build issue on MacOS. It was related to wide + characters in the ncurses library. Patch provided by a + maintainer from MacPorts. + +4.6.patch10 released on 30-SEP-2020 +=================================== + +Problems fixed: +--------------- + + * Under certain rare circumstances (more than 384 + different directories visited) the displayed directory + names did not match the names stored in the panel. + +New or improved functionality: +------------------------------ + + * The sort panel actions are described more accurately. + * Pressing ctrl-C now leaves the config panel. + +4.6.patch9 released on 08-JUN-2018 +================================== + +New or improved functionality: +------------------------------ + + * Support for GPM mouse and other mice was added when + compiled with NCURSES version 6.0 or newer. + +4.6.patch8 released on 05-MAY-2018 +================================== + +Problems fixed: +--------------- + + * Some typos were corrected. Thanks to Tobias Frost for + sending a patch. + +New or improved functionality: +------------------------------ + + * The 'configure' script was modified so that CLEX + compiles without warnings with recent gcc and glibc + versions. + +4.6.patch7 released on 23-JUN-2017 +================================== + +Problems fixed: +--------------- + + * Non-ASCII - but printable - Unicode characters are now + allowed in the xterm window title. + * A backtick character inside of double quotes is now + properly quoted. + * Attempts to insert an Alt-key combination into the + editing line (i.e. Ctrl-V Alt-X) invoked the function + bound to that Alt-key combination. This is now fixed. + +New or improved functionality: +------------------------------ + + * Mouse clicks on the top (bottom) frame scroll the panel + one page up (down) respectively. Panel filter control + with a mouse was removed in order not to interfere with + the new page down function. + * The file rename function does not replace spaces with + underscores. + * The Unicode non-breaking space (NBSP) is marked as a + special character. Shells do not treat this character + as a separator. Commands containing NBSP written by + mistake usually fail and the error is not easy to find. + * User and group names up to 16 characters are displayed + without truncating in the file panel. (The limit was 9 + characters) + * User names are never truncated in the user panel. + * The RPM spec file is now included in the source code + tarball. Previously this file has to be downloaded + separately or built from a provided template. This + extra step is now unnecessary and an RPM package can be + built simply with: 'rpmbuild -tb clex.X.Y.Z.tar.gz' + +4.6.patch6 released on 31-AUG-2013 +================================== + +Problems fixed: +--------------- + + * Several wide character buffer sizes were computed in + incorrect units. No buffer overflows were actually + occurring, but such code is not usable if compiled with + protection against overflows, e.g. with gcc and + FORTIFY_SOURCE=2. Problem noted and a fix proposed by + Rudolf Polzer. + * A bug in the file I/O error reporting code of the + directory compare function was found by Rudolf Polzer. + +New or improved functionality: +------------------------------ + + * New setting in the sort panel: hidden files can be + excluded from the file list. + +4.6.patch5 released on 19-JUL-2011 +================================== + +Problems fixed: +--------------- + + * Some keys did not work in the log panel's filter. + +4.6.4 released on 21-MAY-2011 +============================= + +Problems fixed: +--------------- + + * Name completion did not expand a single tilde as a home + directory. + * A mouseclick on a certain screen area of the help panel + could lead to a crash. + +New or improved functionality: +------------------------------ + + * The English documentation was proofread and corrected, + the service was kindly contributed by Richard Harris. + * Text file preview function was added. + * The initial working directory for the secondary file + panel is now set by a bookmark named DIR2. This + replaces the configuration parameter DIR2. + * The initial working directory for the primary file + panel can be now set by a bookmark named DIR1. + * New configuration parameter TIME_DATE controls the + display of date and time. + * New layout field $g displays the file age. + * Changes to the mouse control were made. + * The recommendation against using alt-R for normal file + renaming was dropped. + +4.5 released on 24-SEP-2009 +=========================== + +Problems fixed: +--------------- + + * Name completion could not complete user and group names + containing a dot, comma or a dash character. + +New or improved functionality: +------------------------------ + + * A mouse is supported on xterm-compatible terminals. + * The location of configuration files has been moved + again in order to comply with the XDG Specification. + The standard place for these files is from now on the + ~/.config/clex directory. Use the 'cfg-clex' utility to + move the files to the new location. + * There is a new option in the completion panel which + allows completion of a name which is a part of a longer + word. The option has a self-explaining description: + 'name to be completed starts at the cursor position'. + * Configuration parameter C_PANEL_SIZE (completion panel + size) cannot be set to AUTO (screen size) because this + size is often uncomfortably small. + * The Unicode soft hyphen character is displayed as a + control character. + * In the history panel a command separator is + automatically inserted into the input line when a + command is appended to the end of another command. + * Configuration parameters CMD_Fn accept a new control + sequence $~ which disables the 'Press enter to + continue' prompt. The control is returned to CLEX + immediately after the command execution terminates + provided that: + + * the command has not been modified; and + * the command terminates successfully (exit code + zero). + + * The $! control sequence can appear anywhere in a + configuration parameter CMD_Fn, not only at the end. + * New function: alt-Z places the current line to the + center of the panel. People using cluster-ssh might + find it useful. + +4.4 released on 07-APR-2009 +=========================== + +Problems fixed: +--------------- + + * In the help text there were few Unicode characters + which are now eliminated because they could not be + displayed properly in non-Unicode encodings. + +New or improved functionality: +------------------------------ + + * New function was added: change into a subdirectory + showing the contents in the other file panel (alt-X). + This function allows a return into the original + directory simply by switching panels (ctrl-X). + +4.3 released on 29-MAR-2009 +=========================== + +Problems fixed: +--------------- + + * A newly added bookmark did not appear on the screen + immediately. + * A misleading message 'Ignoring the DIR2 configuration + parameter' was logged when the 'DIR2' was set to + 'HOME'. + +New or improved functionality: +------------------------------ + + * The bookmark organizer has been merged with the regular + bookmark panel. + * Bookmarks can have descriptive names. + * The current working directory can be bookmarked from + the file panel (ctrl-D). + * The 'B_PANEL_SIZE' config parameter was removed. + +4.2 released on 15-MAR-2009 +=========================== + +Problems fixed: +--------------- + + * In some cases the 'cfg-clex' utility was generating an + unusable template for the copy command (F5). + * Under certain circumstances a crash was occurring on + exit when CLEX was used over a ssh connection. + +New or improved functionality: +------------------------------ + + * All configuration files now reside in the .clex + subdirectory. Use the 'cfg-clex' utility to move the + files to new location. + +4.1 released on 09-FEB-2009 +=========================== + +Problems fixed: +--------------- + + * Usage of uninitialized memory during the start-up has + been corrected. It caused a crash on the Apple Mac OS X + platform. Systems where CLEX starts normally are not + affected by this bug. + * A compilation problem on Apple Mac OS X was fixed. + * The xterm title change feature did not work on remote + telnet or ssh connections. + +New or improved functionality: +------------------------------ + + * If a directory comparison is restricted to regular + files, then only information about this type of file is + displayed in the summary. + * A small program named 'kbd-test' was added. It is a + utility for +$L=keys_trouble +troubleshooting keyboard related + + problems. + +4.0 released on 22-DEC-2008 +=========================== + + This is the initial release of the CLEX 4 branch. Main new + features are: + + * Unicode support has been added. + * Several configuration parameters have been converted to + options which are saved automatically. + * The log panel and optional logging to a file for + auditing and troubleshooting have been added. + * The 'cfg-clex' utility for initializing or upgrading + settings has been added. + * A built-in function for renaming files with invalid or + unprintable characters has been added. + + Enhancements (compared to previous releases) include: + + * Configuring prompt, time format and date format is more + flexible. + * The help is not limited to one link per line. + * The user interface of the directory compare function + was redesigned. + * Changes in the pattern matching routine were made. + * Panel filtering is now available in two more panels. + * A new tool for inserting control characters into the + input line was added. +$P=compacting +$T=Compacted directory list + If there are two directories in the list named, for + example, '/usr' and '/usr/X11R6/lib/X11/fonts/Type1', the + former is considered to be contained in the latter and only + the longer name appears in the list. + + If you then choose '/usr/X11R6/lib/X11/fonts/Type1' from + the list, CLEX splits it into components like this: + + /usr/X11R6/lib/X11/fonts/Type1 + /usr/X11R6/lib/X11/fonts + /usr/X11R6/lib/X11 + /usr/X11R6/lib + /usr/X11R6 + /usr + / + + and the '/usr' directory reappears this way. +$P=compare +$T=Comparing directories + Files in the current working directory (primary panel) and + files in the secondary working directory (secondary panel) + are compared with each other and the differences are marked + with selection marks. + + Files which are the same in both directories are not + selected and files which do not appear in both directories + or which are different are selected. + + The comparison can be restricted to regular files only. + Files that do not meet this condition (e.g. directories) + are left unselected. + + In the simplest comparison, two files are equal if they + have the same name and the same type. In addition to this, + several other attributes can be selected for comparison: + + * file size: the size is compared only for plain files, + i.e. not for directories, etc. For device special files + their device major and minor numbers are compared + instead of the size. + * file mode, i.e. access rights or permissions + * file ownership, i.e. user and group + * file data, i.e. the contents. Only the data of plain + files is compared. + + Comparing large amounts of data may take a long time. You + can press the ctrl-C key to abort the comparison at any + time. + + A +$L=summary +comparison summary + is displayed afterward. + + ----------------------------------------------------------- + + Notes: + + * Both directories are automatically re-read before + comparison. + * In UTF-8 there are two encoding methods for accented + characters. Two visually-equivalent characters encoded + in different ways do not compare as equal. +$P=completion +$T=Name completion + This function attempts to complete any name (word) found at + the cursor position. TIP: If finer control over the name + completion is required or if the automatic completion + reaches its limits, please use the: +$L=paste +completion/insertion + + panel ( ) instead. + + There are several types of name completion and the + completion method is chosen automatically depending on the + context. Please note that inside some command line + constructs (e.g. loops), the automatic completion may fail + to choose the right type of completion. + + command names + word to be completed is the first word in a + command. If no directory is specified, '$PATH' is + searched. + + directory names + used in the directory panel + + user names + for names beginning with '~' ('~username' = user's + home directory) + + environment variables + for names beginning with '$' sign + + filenames + this general type of completion is performed in all + other cases + + Filename completion tries to recognize certain shell + metacharacters and completes filenames in these situations: + + * 'command < /some/file' - also '>' or '>>' redirections + * 'command ; /other/command' + * 'command & /other/command' + * 'command || /other/command' - also '&&' + * 'command | /other/command' + * '`command`' + + These common situations are handled as well: + + * 'arg=/some/file' + * '--option=/some/file' + * 'host:/some/file' or 'user@host:/some/file' + + The 'arg', 'option', 'user', or 'host' in these examples + must be a single word and the cursor must be positioned + after the ':' or '=' character. + + If there is no completion possibility, a warning is + displayed. If there is exactly one completion possibility, + CLEX completes the name, otherwise a list of completion + candidates appears on the screen. +$P=dir +$T=Changing working directory +Changing directory in the directory panel +========================================= + + This panel allows you to change the working directory using + any of the following methods. If an error occurs (e.g. + directory not found), either correct the directory name and + try again or press ctrl-C (or ctrl-G) to cancel the + operation and exit the panel. + +1. Choosing directory name from the panel +----------------------------------------- + + CLEX maintains a list of recently visited directories to + help you to return to a previously used directory easily. + The list is sorted alphabetically and is +$L=compacting +compacted +. + + Simply highlight the requested directory with the cursor + bar and press . A new list will appear in the panel + showing the chosen directory split into components. Confirm + the requested directory again with . + + The +$L=filter +panel filter + (ctrl-F) can be used for finding + directory names in the panel. + +2. Entering new directory name +------------------------------ + + Either press the key to insert the current name from + the panel into the empty input line or start typing the + name of the new working directory. Enter the directory name + as it is, do not quote anything. + + With the key you can +$L=completion +complete + a partial directory + name. Finally, press . + + When the input line or the panel is being used, the focus + automatically changes accordingly. The focus is indicated + by the cursor bar's visibility and by the highlighting of + the input line prompt. Which directory name has the focus + is important when you press the . + + Note: CLEX performs +$L=tilde +tilde substitution +. + +3. Choosing directory name from the bookmark list +------------------------------------------------- + + Follow the '-->Bookmarks' link (or press alt-K) to switch + to the +$L=bookmarks +bookmark panel + and then select a directory from + the list. + +Changing directory in the file panel +==================================== + + Please note that there are other convenient methods to + change the working directory without using the directory + panel: + + * Make use of the +$L=cd +cd command + in the command line. + * Traverse the directory tree: move the file panel cursor + bar to the requested directory and press . + The can be omitted if the command line is empty. + * Use the shortcuts for frequently used change directory + commands: + + alt-/ change into the root directory + alt-. change into the parent '..' directory + alt-~ change into your home directory +$P=file1 +$T=File panel functions (1/3) - access to other panels + These panels work with files, directories and with the + command line. That's why they are accessible from the file + panel only. + + alt-M +$L=menu +main menu + + alt-W +$L=dir +directory panel + (change working directory) + alt-K +$L=bookmarks +bookmark panel + + alt-S +$L=sort +sort panel + + alt-H +$L=history +command history panel + + alt-U +$L=user +user (group) information panel + + (alt-G) + alt-= +$L=compare +file compare panel + + +$L=paste +completion/insertion panel + + + Panels in the next group are not related to the file panel. + They can be entered from any panel, but only one instance + of each panel is allowed. + + alt-C +$L=cfg +configuration panel + + alt-L +$L=log +log panel + + alt-N +$L=notify +notification panel + + alt-O +$L=filter_opt +filter and pattern matching options + + + To exit a panel, press the cancel key (ctrl-C or the + equivalent ctrl-G). Also, the same key that enters the + panel, exits the panel. The configuration panel + additionally requires confirmation at exit. +$P=file2 +$T=File panel functions (2/3) - files and directories +Changing working directory +-------------------------- + + alt-/ change into the root directory + alt-. change into the parent '..' directory + alt-~ or alt-` change into your home directory + several functions, see below in + Miscellaneous + alt- change into a subdirectory, see below in + Miscellaneous + alt-X change into a subdirectory switching panels, + see below in Miscellaneous + alt-W go to the +$L=dir +directory panel + + alt-K go to the +$L=bookmarks +bookmark panel + + + See also: the +$L=cd +cd command +. + +Selecting files +--------------- + + Selected names are to be used in commands, see the section + +$L=file3 +building commands + in advanced editing. + + or ctrl-T select/deselect the current file + or select/deselect the current file and all + ctrl-T files listed above it that are in the + same selection state. This is used to + select/deselect a block: select the + first file with and then select + the last one with + alt-+ +$L=select +select files + that match pattern + alt-- +$L=select +deselect files + that match pattern + alt-* invert selection + alt-= +$L=compare +compare files + in both directories + +Miscellaneous +------------- + + ctrl-R Re-read the contents of the directory, i.e. + refresh the file panel. + ctrl-R Like ctrl-R, but an update of cached user + account data is forced. In most cases you do + not need this function (see +$L=accounts +details +). + ctrl-X Exchange primary <--> secondary panel, i.e. + switch the +$L=2dirs +working directories +. + Three functions: + + * if there is a command in the command line: + executes the current command + * otherwise if current file is a directory: + changes into this directory + * otherwise if current file is executable: + inserts './cmdfile' into the + command line + alt- If the current file is a directory, change + into this directory. Of the four functions + above this is the second one, useful if the + command line is not empty. + alt-E Display the contents of the current file in + the +$L=preview +preview panel +, if possible. + alt-X If the current file is a directory, change + into this directory, but show the contents in + the other file panel. The original file panel + becomes secondary and remains unchanged. This + allows one to change back into the original + directory simply by switching panels (ctrl-X). + ctrl-D Bookmark the current directory. This function + appends the current directory to the list of + bookmarks and opens the +$L=bookmarks +bookmark panel +. + alt-R +$L=rename +Rename + the current file. +$P=file3 +$T=File panel functions (3/3) - advanced editing and commands +Inserting (pasting) names into the command line +----------------------------------------------- + + insert or complete (see the +$L=tab +tab key + + functions) + insert the name of the current file + insert the names of all selected files + Note: the current directory '.' (dot) and the + parent directory '..' (dot-dot) will be NOT + inserted. This is a protective measure. A + +$L=notify +warning + is also given, but it can be + disabled. + ctrl-A insert the full pathname of the current file: + '/path/to/file' + ctrl-E insert the name of the +$L=2dirs +secondary working + + directory + ctrl-E insert the name of the +$L=2dirs +current working + + directory + ctrl-O insert the target of a symbolic link + go to the +$L=paste +completion/insertion panel +, a + menu of related functions + + All inserted filenames are properly +$L=quoting +quoted +. + +Filename completion +------------------- + + insert or complete, see the +$L=tab +tab key functions + + +Building commands +----------------- + + insert the name of the current file + - insert a command: + + * - view + * - edit + * - copy + * - move + * - make directory + * - delete + * - print + * to - user defined + The commands above normally work with the + current file. If you press before + - then they work with the selected + files. + execute the command + (Advanced users only: +$L=suspend +suspending the + + running command) + +Command history +--------------- + + ctrl-P recall the previous command from the history list + ctrl-N recall the next command + alt-P complete the command, i.e. search the history for + command(s) matching the text in the command line + alt-H go to the +$L=history +history panel + +$P=file_intro +$T=File panel - introduction + Working with the file panel is the basic operation mode. + You can inspect files, change directories, create commands + in the command line and execute them. + + You can see +$L=2dirs +two directory names + on the top of the + screen: the current working directory on the left and the + secondary working directory on the right. + + The contents of the current working directory are listed in + the file panel on the screen. One file is highlighted with + the cursor bar. It is called the current file. Additional + information about it is displayed in the info line. + + The layout of the file panel screen is configurable. The + following data may be shown in the listing of files or in + the info line: + + * name of the file + * where the symbolic link points to (links only) + * time of last modification + * time of last file access + * time of last inode change + * size in bytes, KB, or MB or device minor and major + numbers (devices only) + * whether it is a symbolic link (indicated by '->' ) + * type of file: + + * (none) - plain file + * 'exec' - plain file, executable + * 'suid' - executable file with set-UID + * 'Suid' - set-UID file owned by root + * 'sgid' - executable file with set-GID + * '/DIR' - directory + * '/MNT' - directory, active mount point + * 'Bdev' - block device + * 'Cdev' - character device + * 'FIFO' - FIFO (also known as named pipes) + * 'SOCK' - socket + * 'spec' - special file, i.e. anything else + * '??' - type could not be determined due to missing + access rights or other error + + * whether the file is selected (indicated by '*') + * permission bits (as octal number and/or as a string) + * owner and group + * number of hard links or an indicator of multiple links + + The number of selected files (if any) is shown in the panel + frame along with the line numbers. + + The command line occupies only a few lines on the screen, + but it can contain many lines of text. Here you type and + edit commands to be executed. The prompt is configurable. + + ----------------------------------------------------------- + + Notes: + + * '/DIR' and '/MNT' - CLEX will not detect mount points + if the mounted file hierarchy resides on the same + device ('mount --bind' on Linux). + * Device minor numbers are shown in hexadecimal. + * Owner and group names are limited to 16 characters + here. Visit the +$L=user +user or group information panel + to + see the full names. +$P=filter +$T=Panel filter + This function helps to find an entry in a long listing + quickly. It is available in the file panel (file names), + the completion panel, the directory and the bookmark panels + (directory names), the history panel (commands), the + user/group panels (user names), and the log panel. + + The filter exists also in the help panel in an altered form + of the +$L=help +find text function +. + + Start with pressing the ctrl-F key, then type in some + string appearing in the name(s) you are looking for. While + you type, the panel contents shrink to display only entries + containing the string you have entered. + + There are several +$L=filter_opt +options + affecting the filtering + process. Press Alt-O to display them. + + Finally press: + + * to cancel the panel filter (see the notes + below), or + * ctrl-F to return to the input line while the filter + remains active. Repeated ctrl-F will bring you back to + the filter expression. + + Pasting text (usually with the tab key) from the filtered + panel into the input line moves the cursor from the filter + to the input line. + +Filtering in the file panel +--------------------------- + + In the file panel, a pattern can be used as a filter + expression as well. If the filter expression contains the + wildcards '*' '?' or '[]' then it is automatically used as + a +$L=patterns +pattern +. In order to use those special characters in a + normal filter (substring) you have to quote them, e.g. with + a preceding backslash. + + ----------------------------------------------------------- + + Notes: + + * In the file panel, the directory is re-read when + filtering is activated and the panel's contents are + more than 60 seconds old. + * Executing a command cancels a substring-type file panel + filter, but not a pattern-type filter. + * For symbolic links in the file panel, not only the + name, but also the value is inspected by the filter. + * The filter in the directory panel cannot be canceled + unless the filter expression is empty. This prevents an + undesired interaction between filtering and + +$L=compacting +compacting +. + * Quoting is supported in the file panel only. In all + other panels the filter expression is taken literally. +$P=filter_opt +$T=Filter and pattern matching options + These options control the panel filter and also the pattern + matching routine which is used not only by the panel filter + but also by the +$L=select +select and deselect + functions. + + substring matching: ignore the case of the characters + Check this option to ignore the case of alphabetic + characters. Note: this does not apply to pattern + matching. + + pattern matching: wildcards match the dot in .hidden files + Check this option if you do not want to treat the + hidden files specially. + + file panel filtering: always show directories + If you check this option, the file panel will show + all directories regardless of the filter. This + allows traversal of the directory tree while a + filter is active. + + ----------------------------------------------------------- + + Notes: + + * The term hidden files is used for files with names + beginning with a period (dot) because they are usually + not listed in a directory listing (output of the 'ls' + command). This naming convention is the only difference + between normal and hidden files. + * The option settings are retained between sessions. +$P=format +$T=Setting the date and time format + Below is an extract from the 'strftime' documentation. Make + sure that the length of the resulting date or time string + is constant and does not exceed 21 characters. + +Day +--- + + '%d' the day of the month as a decimal number (01 to 31) + '%e' like '%d', but a leading zero is replaced by a space + +Month +----- + + '%m' the month as a decimal number (01 to 12) + '%b' the abbreviated month name + +Year +---- + + '%Y' the year as a decimal number including the century + '%y' like '%Y' but without a century (00 to 99) + +Hour +---- + + '%H' the hour as a number using a 24-hour clock (00 to 23) + '%k' like '%H', but single digits are preceded by a blank + '%I' the hour as a number using a 12-hour clock (01 to 12) + '%l' (letter el) like '%I' but single digits are preceded + by a blank + either 'AM' or 'PM' according to the given time value, + '%p' or the corresponding strings for the current locale. + Noon is treated as 'pm' and midnight as 'am' + '%P' like '%p' but in lowercase + +Minutes and seconds +------------------- + + '%M' the minute as a decimal number (00 to 59) + '%S' the second as a number (00 to 60) + +Other +----- + + '%%' a literal % character + +Examples +-------- + + '%H:%M' 13:59 + '%l:%M:%S%P' 1:59:07pm + + ----------------------------------------------------------- + + Notes: + + * The standard formats for the current locale are written + to +$L=log +the log + when CLEX starts. Those values are used + if the configuration parameters are set to 'AUTO'. + * See the 'strftime' documentation for more information. +$P=help +$T=Help with on-line help +Keys +---- + + display the help page for the + selected function + ctrl-C or ctrl-G exit the help + and move the cursor bar by lines + and move the cursor bar by pages + or follow a link to other help-pages + (links are highlighted) + or go back to previous help-page + +Find text function +------------------ + + ctrl-F find text: the cursor bar is moved to a line + containing the given string + ctrl-F press again to find the next line + exit the find text function +$P=history +$T=Command history + The history panel offers advanced functions for working + with the command line history list. + + The most common tasks can be accomplished directly from the + +$L=file3 +file panel + (see ctrl-P, ctrl-N, and alt-P). + + In the history panel, you can browse the list of recently + executed commands, insert the command(s) into the command + line and edit them as required. For example two or more + previously executed commands can be easily combined into a + new command. + + Help with keys: + + Return to the file panel. If the command + line is empty, the current command from + the history list will be copied into it. + Insert the text of the current command + from the panel into the command line. A + command separator ';' is automatically + inserted when a command is appended to the + end of another command. + Delete the current entry from the history + list. + Ctrl-F Activate the +$L=filter +panel filter +. + Ctrl-C or Ctrl-G Exit the history panel (changes made in + the command line are preserved). +$P=index +$T=Index + - A - +$L=about +about CLEX + + automatic filename quoting --> quoting + - B - +$L=bookmarks +bookmark panel + + - C - +$L=changelog +change log + + changing working directory: + + * the +$L=cd +cd command + + * the +$L=dir +directory panel + + * the +$L=bookmarks +bookmark panel + + * in the +$L=file2 +file panel + + + command history --> history panel + +$L=options +command line options + + +$L=compare +compare directories + + completion --> name completion + +$L=paste +completion/insertion panel + + +$L=cfg +configuration panel + + +$L=cfg_parameters +configuration parameters + + - D - +$L=dir +directory panel + + - F - file panel: + + * functions 1/3: +$L=file1 +access to other panels + + * functions 2/3: +$L=file2 +files and directories + + * functions 3/3: +$L=file3 +advanced editing and commands + + * +$L=file_intro +introduction + + + filename completion --> name completion + filename quoting --> quoting + +$L=filter +filtering + + +$L=filter_opt +filter and pattern matching options + + - G - +$L=user +group information panel + + - H - +$L=help +help + + +$L=history +history panel + + homepage: https://github.com/xitop/clex + - K - +$L=keys +keyboard + + keys: + + * +$L=keys_control +control keys + + * +$L=keys_scroll +panel-scrolling keys + + * +$L=keys_edit +basic editing functions + + * +$L=keys_trouble +troubleshooting + + - L - +$L=license +license agreement + (GPL v2 or later) + +$L=log +log panel + + - N - +$L=completion +name completion + + +$L=notify +notifications + + - M - +$L=menu +main menu + + message log --> log panel + +$L=mouse +mouse + + - P - panel filter --> filtering + +$L=patterns +pattern matching + + +$L=preview +preview + + primary/secondary panel --> working directories + - Q - +$L=quoting +quoting + of filenames + - R - +$L=rename +rename file + + - S - +$L=screen +screen overview + + secondary/primary panel --> working directories + +$L=file2 +selecting files + in the file panel + +$L=select +selecting files + using patterns + +$L=sort +sorting filenames + + +$L=suspend +suspending the running command + + - T - +$L=tilde +tilde substitution + + - U - +$L=user +user information panel + + - W - +$L=2dirs +working directories + +$P=insert +$T=Inserting special characters + After pressing the alt-I key, one or more characters (space + separated if necessary) can be entered using any of the + following notations for special characters: + + '^X' for ctrl-X + This is written as a two character combination: the + ^ character followed by a letter from A to Z (in + upper or lower case). + + 'DDD' for a decimal value + Any combinations of digits 0 to 9 is replaced by a + character with the corresponding code. + + '\xHHH' or '0xHHH' or 'U+HHH' for a hexadecimal value + A two character prefix followed by a hex number is + replaced by a character with the corresponding + code. These three prefixes are based on various + standards (e.g 'U+' for Unicode code point) and all + have the same meaning in CLEX. + + Null code character cannot be inserted. CLEX does not check + if the code is valid in the current encoding. WARNING: + Inserting characters with invalid codes might have + undesired effects. + +Examples: +--------- + + * ^C - 'ctrl-C', ASCII code 3 + * 98 - letter 'b' - ASCII code 98 + * \x62 - letter 'b' as above + * U+3c8 - greek letter (psi) (Unicode only) +$P=keys +$T=Using a keyboard + * Following notation is used in this help: + + ctrl-X press and + alt-X press and + or press and + or press first and then + (depends on your terminal/keyboard) + + * Working arrow keys are an essential requirement. + * If some function keys are not working properly on your + terminal, please read this +$L=keys_trouble +troubleshooting + + information. +$P=keys_control +$T=Control keys + These are the keys to remember: + + ctrl-C or ctrl-G cancel the current function, exit the + current panel + alt-M invoke main menu (in the file panel only) + ctrl-L redraw screen + on-line help + alt-Q quit +$P=keys_edit +$T=Basic editing functions +Move cursor +----------- + + character left + character right + move to the beginning of + the line + move to the end of the + line + alt-B or shift- or word left (back) + ctrl- + alt-F or shift- or word right (forward) + ctrl- + or ctrl- line up + or ctrl- line down + + Some key combinations from the list above may be + unsupported or not available on some systems. + +Delete +------ + + delete character under the cursor + alt-D delete word at the cursor + or ctrl-H delete character to the left the + cursor + ctrl-K delete to end of line + ctrl-U delete entire line + +Insert +------ + + All regular text that you type is inserted, there is no + overtype mode. + + In order to insert a control character that has a function + in the program (e.g. ctrl-C = exit panel) press ctrl-V + before the character: + + ctrl-V insert the character 'X' + + In order to insert characters by their codes press alt-I + and follow these +$L=insert +instructions +. + +Undo and redo +------------- + + ctrl-Z or alt-ctrl-Y undo (reverse the last insert or + delete operation) + ctrl-Y or alt-ctrl-Z redo (revert to the state before undo) + +Miscellaneous +------------- + + alt-T transform character case (upper to lower and vice + versa) + + ----------------------------------------------------------- + + Notes: + + * The edit functions treat every space as a word + separator. + * Unprintable characters in the input line are displayed + as highlighted question marks. If you put the cursor + over such character, you can see its numeric value. + * Filenames containing unprintable characters can be + renamed using the internal +$L=rename +rename function + alt-R. +$P=keys_fn +$T=Function key problems + If the function keys to do not work on your + terminal as expected, use this procedure until the problem + is fixed: + press alt-1 (or 1) for + press alt-2 (or 2) for + ... + press alt-9 (or 9) for + press alt-0 (or 0) for + + On some systems the keys work properly but + sequences do not work. You have to press the key + twice before pressing to . + + ----------------------------------------------------------- + + Notes: + + * N is not +$P=keys_scroll +$T=Panel scrolling keys + These keystrokes scroll a panel: + + line up + line down + page up + page down + go to the top of the panel + go to the bottom of the panel + alt-Z scroll the current line to the center of the + panel +$P=keys_trouble +$T=Troubleshooting keyboard problems + If alt-KEY does not work, press first and then the + KEY. This is equivalent to alt-KEY usage. + + If a key combination is in use by the window manager you + could try adding another modifier key like the shift or + ctrl. E.g. alt- is often not available to CLEX because + it switches tasks on a graphical desktop, but + ctrl-alt- might work fine. + + If a function key does not work, please run the 'kbd-test' + utility which is bundled with CLEX. Press the key in + question and compare the output with the values in this + table. + + pressed key expected 'kbd-test' notes + output + KEY_LEFT essential key + KEY_RIGHT essential key + KEY_UP essential key + KEY_DOWN essential key + KEY_HOME important key + KEY_PPAGE important key + KEY_NPAGE important key + KEY_DC important key + (\x7F is incorrect, + but might work) + KEY_BACKSPACE ctrl-H has the same + (ctrl-H is function in the input + incorrect, but will line + work fine) + KEY_IC or KEY_IL ctrl-T has the same + function in the input + line + , , ... KEY_F(1), KEY_F(2), essential keys, there + ... is a +$L=keys_fn +workaround + + shift- KEY_SLEFT optional key, alt-B + has the same function + in the input line + shift- KEY_SRIGHT optional key, alt-F + has the same function + in the input line + ctrl- kLFT5 (or other optional key, like + digit at the end) + ctrl- kRIT5 (or other optional key, like + digit at the end) + ctrl- kUP5 (or other digit optional key, like + at the end) + ctrl- kDN5 (or other digit optional key, like + at the end) + + If your results differ from the values above, then CLEX is + not getting the correct input. Your terminal is probably + not set properly or the termcap/terminfo database does not + match the terminal's keyboard output. + + Please do not report keyboard problems as CLEX bugs if + 'kbd-test' output differs from the correct values in the + table. +$P=layout +$T=Configuring the file panel layout + The file panel layout can be configured to match individual + needs. The 'LAYOUT' parameter is a string of the form: + + file_panel_fields|info_line_fields + + where '..._fields' is a list of fields to be displayed, + each field is represented by a dollar sign followed by a + single character: + + '$a' time/date of last access + '$d' time/date of last modification + file age, i.e. the time elapsed since the last + '$g' modification is displayed for files not older + than 100 hours + '$i' time/date of last inode change + '$l' number of hard links + '$L' 'LNK' mark displayed if there are multiple hard + links, otherwise this field is blank + '$m' file mode as an octal number + '$M' like '$m', but blank for normal file modes (i.e. + mode 0666 or 0777 with umask value bits cleared) + '$o' owner (user:group) + '$p' file mode as a string of permission bits + '$P' like '$p', but blank for normal file modes + file size: + + '$s','$r' * '$r' - short form, up to 3 digits + * '$s' - long form, up to 7 digits + + Note: only one form may appear in a layout. + '$S','$R' like '$s' and '$r', but blank for directories + '$t' type of file + '$>' '->' is displayed for symbolic links + '$*' selection mark + '$$' literal dollar sign '$' + '$|' literal vertical bar '|' + + The filename is always displayed after all defined file + panel fields. + + Normal text can be inserted into the list of fields as + well, e.g.: + + links:$l mode:$m + $m($p) + + There are three predefined layouts in the standard + configuration. 'LAYOUT1' is detailed, 'LAYOUT2' is brief, + and the 'LAYOUT3' is similar to the 'ls -l' output: + + LAYOUT1 $d $S $>$t $M $*|$p $o $L + LAYOUT2 $d $R $t $*|$p $o + LAYOUT3 $p $o $s $d $>$t $*|mode=$m atime=$a ctime=$i + links=$l + + All three 'LAYOUT's can be altered and one of them is + selected with the 'LAYOUT_ACTIVE' configuration parameter + to act as the file panel layout. + + ----------------------------------------------------------- + + Notes: + + * Numbers and dates are automatically aligned to the + proper field margin, e.g.: + + * '$d $s' - aligned to the right + * 'mtime=$d size=$s' - aligned to the left + + * You can omit the selection mark '$*' if you accept the + highlighting as a sufficient indicator. + * '$L' field is left blank for all directories as they + always have multiple hard links. +$P=license +$T=Software license agreement + The authors have developed the CLEX File Manager and made + it available by means of electronic distribution at the + Internet web page https://github.com/xitop/clex. + + 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 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more + details. + + You should have received a copy of the GNU General Public + License along with this program; if not, see + https://www.gnu.org/licenses/gpl-2.0.html +$P=locale +$T=The locale + A locale is a set of language and cultural rules like + alphabet order, 12/24 hour clock selection or the date and + time formats. It is defined by environment variables. Most + often the 'LC_ALL' variable controls everything. + + If the 'LC_ALL' is not set, the locale is controlled by a + set of variables for specific locale categories, e.g.: + + 'LC_CTYPE' Character classification and case conversion + 'LC_COLLATE' Collation order + 'LC_TIME' Date and time formats + 'LC_NUMERIC' Non-monetary numeric formats + 'LC_MESSAGES' Formats of messages + 'LANG' Default value for unset variables +$P=log +$T=Message log + The log panel displays the last 50 messages generated by + CLEX. Each message has a timestamp and a severity level + attached: + + * 'AUDIT' messages record the user's activity, mainly the + execution of commands. + * Messages with the severity of 'INFO' and 'WARNING' are + copies of selected messages from the program's user + interface. + * Internal messages for troubleshooting purposes are + tagged with 'DEBUG' and 'NOTICE' severities. + + If the length of a message exceeds the screen width, use + the and arrow keys to scroll the text + horizontally. + + The M key logs a mark, this is just a small help for + developers and otherwise of little use. + + See also the '--log' +$L=options +command line option + for saving all + logged messages to a file. +$P=menu +$T=Main menu + This is a self-explanatory menu of all important file panel + functions. It is available only in the file panel. All you + need to do is to press alt-M to display it. + + All functions listed here are regular file panel functions + described +$L=file1 +here + and +$L=file2 +here +. These functions can be also + activated directly from the file panel using shortcuts + which are displayed in the menu for your convenience. +$P=mouse +$T=Using a mouse + A mouse is supported on some terminals. The mouse support + can be disabled in the +$L=cfg +configuration panel +. + +General usage +============= + +Inside a panel +-------------- + + * A single click sets the cursor bar position. + * A double click is equivalent to pressing the + key. + * A right-click (left-click on a left-handed mouse) + inserts text from the panel into the input line like + the key. + * The mouse wheel moves the cursor bar. The speed is + configurable. + * A click on the top or bottom frame scrolls the cursor + by one page equivalent to or + keys. + +On the input line +----------------- + + * A single click sets the cursor position. + * A double click is like . + * The mouse wheel moves the cursor. + +In the CLEX bar +--------------- + + * The CLEX bar is the line with "CLEX File Manager" title + displayed in reverse video. A double click on a hint + shown there (for example 'F1=help') activates the named + function. + +Panel filter +------------ + + * A single click sets the cursor. + * The mouse wheel moves the cursor. + + Note: In order to enable cut & paste operations, hold the + shift key to bypass mouse tracking. + +File panel specific usage +========================= + +In the title line +----------------- + + * A double click on the current directory name (top-left + corner) invokes the directory panel. + * A double click on the secondary directory name + (top-right corner) switches panels. + +In the list of files +-------------------- + + * A mouse double click does not behave exactly like + pressing enter. A double click on a directory changes + into that directory. A double click on a file activates + the +$L=preview +preview function +. + +In the prompt area of the command line +-------------------------------------- + + * The mouse wheel movement in the command browses the + command history list like ctrl-P and ctrl-N. + * A double click on the command prompt invokes the + command history panel. + +Technical details +================= + + CLEX is compiled either with support for the NCURSES mouse + interface or for the xterm mouse interface. The preferred + interface is the former, because it can communicate with + several types of mice including xterm compatible terminals + and gpm server. The required version 2 of this interface + first appeared in NCURSES release 6.0. + + * Run 'clex --version' to check which mouse interface was + compiled into the CLEX on your system. + * Visit the +$L=log +log panel + after starting CLEX to check if + a mouse was detected. +$P=notes +$T=Usage notes +GENERAL USAGE +============= + + * Special attention should be paid when dealing with + filenames beginning with a hyphen. For example the + correct way to delete a file named '-i' is: + + rm ./-i + + or (on some systems) + + rm -- -i + + You can redefine commands F3 - F9 to include this + end-of-options sign '--'. + * If the screen appears distorted and redrawing (ctrl-L) + does not solve the problem, run this command: + + tput reset + + * It is not possible to change a running CLEX's + environment variables, you must set them before + starting CLEX. + +X WINDOW TERMINAL EMULATORS +=========================== + +General tips +------------ + + * If you cannot see the output of executed commands on + some X terminal emulators, try disabling scrolling. + * In the past, there were several issues with some + terminal emulators. Please make sure you are not using + an outdated version of a terminal emulator. + +XTERM +----- + + * If you want your xterm to display highlighted text in + color, put these lines to your '.Xresources' file: + + XTerm*colorBD: blue3 + XTerm*colorBDMode: true + + Pick your own color instead of 'blue3' and don't forget + to run: + + xrdb -merge .Xresources + + after each change. + * If you experience problems with the Alt key, add this + line: + + XTerm*eightBitInput: false + + run 'xrdb' as mentioned above. + +KDE Konsole +----------- + + * Konsole does not report a right mouse button double + click properly. Problem noted on KDE 4.2.2, and is + still not fixed on 4.5.5. Only left handers are + affected by this bug. + * To display the highlighted text in color in 'Konsole' + you have to change the 'Foreground (Intense)' color of + the current scheme. The procedure differs between + versions. Start with 'Settings' from the menu. +$P=notify +$T=Notifications + All notifications below are normally enabled. You might + want to disable those that annoy you. + + 'Warning: rm command deletes files (not 100% reliable)' + If enabled, a warning is given before executing a + command line containing a 'rm' (delete) command. + CAUTION: It is not fully reliable, because CLEX is + not able to detect a 'rm' command inside certain + command line constructs (e.g. in loops or in + if-then clauses). See "aliasing" in your shell + documentation for an alternative. + + 'Warning: command line is too long to be displayed' + This warning is given before executing a command. + It indicates that the entire command line could not + be shown because of its length. This warning is + suppressed during an automatic command execution. + + 'Reminder: selection marks on . and .. are not honored' + This option enables an informative message saying + that selected directories '.' and '..' were not + automatically inserted. + + 'Reminder: selected file(s) vs. current file' + This message reminds that you have to press the + key before a key when building a command + for all selected files. It should prevent mistakes + when the current file is processed instead of + selected file(s). + + 'Notice: file with a timestamp in the future encountered' + Check your system clock (date and time). If you are + examining files on a medium recorded on another + computer, its clock was possibly not set properly + or was in another timezone. +$P=options +$T=Command line options + These options are recognized when starting CLEX from the + command line: + + '--version' + Display program version with some basic information + and exit. + + '--help' + Display help and exit. + + '--log logfile' + Append log information to the specified 'logfile'. + Useful for troubleshooting and for creating audit + trails. +$P=paste +$T=Completion/insertion panel + This panel summarizes all editing functions that complete + text in the command line or insert text into the line. + + +$L=completion +Name completion + functions: + + * general name completion - type is detected + automatically + * name completion of specified type (6 functions) + + Normally the whole name at the cursor is completed. Check + the panel's checkbox if you need to complete a name which + is part of a longer word. + + Command completion: + + * command completion from the history list + + Insert functions: + + * insert filename(s) (3 functions) + * insert the directory name (2 functions) + * insert the symbolic link target +$P=patterns +$T=Pattern matching + Patterns specify filenames. This concept (sometimes called + globing or wildcard matching) is used in virtually all + command line shells (e.g. 'sh', 'ksh', 'bash' or 'csh'). + See the related documentation for more information. + +Wildcards +--------- + + '?' matches any single character + '*' matches any string (zero or more characters) + matches a single character listed in the bracket + expression. The expression may also contain: + + * ranges like '0-9' or 'a-f' + + '[expr]' and - depending on the library function support: + + * character classes like '[:digit:]' or + '[:lower:]' + * equivalence class expressions like '[=a=]' + * collating symbols like '[.a-acute.]' + '[!expr]' matches a single character NOT matched by + '[expr]' + +Quoting +------- + + '\c' character preceded by backslash loses its special + meaning + +Options +------- + + Following the concept of so-called hidden files, wildcards + normally do not match a dot on the first position of a + filename. This can be changed in the +$L=filter_opt +filter and pattern + + matching options panel. + + ----------------------------------------------------------- + + Notes: + + * Pattern matching is case sensitive. + * Patterns described here are not regular expressions. + * '[^expr]' is a deprecated form of '[!expr]' and should + be avoided. + * Handling of character ranges is affected by the + +$L=locale +locale + setting. + * Names containing characters or byte sequences which are + not valid in the selected locale or encoding can be + unmatchable. +$P=preview +$T=File preview + The file preview panel provides a quick preview of the file + contents. A small data block of the file is displayed in + the text form. The limit is up to 16 kilobytes or up to 400 + lines of text. + + Only regular files can be previewed. Files containing null + bytes or high ratio of control codes are deemed binary and + not displayed. + + Press or ctrl-C to exit the preview panel. There + are no other functions defined. +$P=quoting +$T=Automatic filename quoting + CLEX correctly handles special characters in filenames. For + example: in order to delete a file named '*' (asterisk) it + generates the correct command 'rm \*', and not the + incorrect and disastrous 'rm *'. + + Remember that automatic quoting applies only to filenames + being inserted into the command line ( and keys + to ). + + See also: +$L=cfg_other +configuration parameter 'QUOTE' + +$P=rename +$T=Rename file + The file renaming function is built into CLEX as a tool for + sanitizing file names containing characters or byte + sequences not valid in the current locale or encoding, + because it is impossible to insert such names into the + command line without character conversion errors. + + This function can be used also for regular in-place file + renaming. + + The renaming itself is straightforward: put the cursor bar + on a file to be renamed, press alt-R, enter a new name and + press . +$P=screen +$T=CLEX screen overview + The screen is divided into two areas: the panel, where + various kinds of data are displayed, and the input line, + where a user can enter and edit his or her input. + + PANEL title -----> /usr/local/bin /tmp + frame -----> ======================================== + /-> 07:55 5.120 /DIR . + /--> 23feb07 1.024 /DIR .. + data --<---> >01sep01****650k***exec**img_rename****< + \--> 12oct08 49k exec img_upload + \-> + frame -----> =================================<3/4>== + info line--> 0755 rwxr=xr=x root:root + help line--> alt=M = main menu + *CLEX*file*manager********************** + INPUT LINE -------> shell $ echo "Hello, world!"_ + prompt ------^ ^ + user's input --------' + + There are several types of panels with a corresponding + input line, e.g. the file panel with the command line or + the configuration panel. + + One of the entries in the panel is highlighted with the + cursor bar. It is called the current entry. + + If a text is too long to fit on the screen, it is indicated + by a highlighted character '>' on the right. + + Near the right margin in the lower panel frame there is the + line number of the current entry, and the total number of + entries in the panel. + + The input line occupies only a few lines on the screen, but + can contain many lines of text. +$P=security +$T=Security note + Protect your configuration file! It contains templates for + the commands you execute. Do not allow others to modify + them. Only the owner should have the write access to his or + her personal configuration file. + + To reduce this risk, CLEX ignores world-writable + configuration files. +$P=select +$T=Selecting and deselecting files + File panel keys: + + alt-+ select files that match pattern + alt-- deselect files that match pattern + alt-* invert selection + + To specify files to be (de)selected, enter a filename + pattern at the appropriate prompt. All files that +$L=patterns +match + + the pattern will be selected or deselected respectively. + + The directory is re-read when this function is activated + and the file list is more than 60 seconds old. +$P=sort +$T=Setting the sort order + The sort order affects only the way the filenames are + displayed. + + You can set the sort order for the current panel and the + current directory only or you can set the global sort order + for all panels and directories. The global settings are + retained between sessions. + + On the top of the panel there are settings for hidden files + processing. Hidden files can be excluded from the file list + if it is desired. A special setting hides them in the + user's home directory only, because they traditionally tend + to clutter mainly the home directory. A reminder 'HIDDEN' + is displayed at the lower right panel corner when such + files exist in a directory, but they are excluded from the + panel. + + Files are first grouped and then the groups are sorted. If + grouping is enabled, CLEX will group files of the same type + together. Files will be displayed in this order: + + * directories first (see the note #1) + * then special files + * then plain files + + or alternatively: + + * directories (see the note #1) + * block devices (see the note #2) + * character devices (see the note #2) + * other special files + * plain files + + Notes: + + 1. Directories . and .. will be always on the top of the + display. (1) + 2. Despite the selected sort order, devices will be sorted + by their major and minor numbers. (2) + + Files within a group are sorted using the order selected + from the menu. + + ----------------------------------------------------------- + + Notes: + + * The term hidden files is used for files with names + beginning with a period (dot) because they are usually + not listed in a directory listing (output of the 'ls' + command). This naming convention is the only difference + between normal and hidden files. + * Sorting by name is affected by the +$L=locale +locale +. + * The 'sort by extension' (by filename suffix) considers + a file named '.file' to be a hidden file without an + extension. + * The unusual 'sort by reversed name' option is useful in + sendmail queue directories where files like 'qf1234', + 'df1234', and 'xf1234' belong together. +$P=summary +$T=Comparison summary + In the summary of the directory comparison there is: + + * number of all files in each panel, this total number + consists of: + + * number of files not considered for comparison + because they are unique (files that exist only in + one of the panels) or because they are excluded by + restrictions + * number of files (precisely, pairs of files) which + exist in both panels. They have the same name and + same type. These pairs were compared and the + result is displayed in the form of: + + * number of files that differ + * number of files that are equal + * number of errors occurred (not shown if zero) +$P=suspend +$T=Suspending the running command + Note that this feature is intended for administrators or + advanced users only and normally there is no need to use + it. However, it could help in some unusual situations like + system recovery tasks when another terminal is not + available. + + On systems which support the job control, the running + command (a so-called process group) can be stopped + (suspended) with the ctrl-Z key and then it can be placed + in the background (i.e running without the terminal I/O) + while other commands can be normally started in the + foreground. + + CLEX does not support full job control. It can only resume + (restart) the command suspended with ctrl-Z, but a shell + session can be started before resuming the command. In this + shell session virtually any tasks can be performed. + + ----------------------------------------------------------- + + Notes: + + * In order to check the status of job control support, + see the output of + + clex --version + + * The suspend key can be redefined, ctrl-Z is the normal + setting. +$P=tab +$T=Functions of the tab key + The key has two major functions in the file panel: + + 1. If the cursor stands within or immediately after a + word, attempts to +$L=completion +complete it +. + 2. Otherwise (if there is nothing to complete), + inserts text into the command line: + + * if a command name is expected (it is the first + word in a command) and the current file is an + executable file or a directory, then it inserts: + + ./executable + + or + + directory/ + + * if an argument is expected (not the first word in + a command), it inserts the name of the current + file: + + file + + ----------------------------------------------------------- + + Notes: + + * If you just want to insert filename(s) without any + completion, you might use the key. + * All inserted filenames are correctly +$L=quoting +quoted +. +$P=tilde +$T=Tilde substitution + If the directory name begins with a tilde: + + * '~' - a tilde will be substituted with your home + directory + * '~username' - append a username after the tilde and it + will be substituted with the home directory of the + specified user. If no such user is found, then the + original text is left unchanged. + +Examples: +--------- + + * '~root' - the home directory of the user root + * '~/bin' - the subdirectory bin in your home directory +$P=user +$T=User/group information + These twin panels show the list of user/group IDs with + corresponding names that CLEX has saved during the last + file panel read. The data can be only examined, not + modified. + + +$L=filter +The filtering + (ctrl-F) is supported in this panel. + + You can press the key to insert the current user or + group name into the command line. diff --git a/src/history.c b/src/history.c new file mode 100644 index 0000000..fb38dea --- /dev/null +++ b/src/history.c @@ -0,0 +1,256 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ +#include /* free() */ +#include /* strlen() */ + +#include "history.h" + +#include "cfg.h" /* cfg_num() */ +#include "edit.h" /* edit_putstr() */ +#include "filter.h" /* cx_filter() */ +#include "inout.h" /* win_panel() */ +#include "lex.h" /* cmd2lex() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr() */ +#include "panel.h" /* pan_adjust() */ +#include "util.h" /* emalloc() */ + +static HIST_ENTRY **history;/* command line history */ +static int hs_alloc = 0; /* number of allocated entries */ +static int hs_cnt; /* entries in use */ +static int pn_index; /* index for previous/next cmd */ +static USTRINGW save_line = UNULL; + /* for temporary saving of the command line */ + +void +hist_reconfig(void) +{ + int i; + static HIST_ENTRY *storage; + + if (hs_alloc > 0) { + for (i = 0; i < hs_alloc; i++) + usw_reset(&history[i]->cmd); + free(storage); + free(history); + free(panel_hist.hist); + } + + hs_alloc = cfg_num(CFG_H_SIZE); + storage = emalloc(hs_alloc * sizeof(HIST_ENTRY)); + history = emalloc(hs_alloc * sizeof(HIST_ENTRY *)); + panel_hist.hist = emalloc(hs_alloc * sizeof(HIST_ENTRY *)); + for (i = 0; i < hs_alloc; i++) { + history[i] = storage + i; + US_INIT(history[i]->cmd); + } + + hs_cnt = 0; + hist_reset_index(); +} + +void +hist_panel_data(void) +{ + int i, j; + HIST_ENTRY *curs; + + curs = VALID_CURSOR(panel_hist.pd) ? panel_hist.hist[panel_hist.pd->curs] : 0; + if (panel_hist.pd->filtering) + match_substr_set(panel_hist.pd->filter->line); + + for (i = j = 0; i < hs_cnt; i++) { + if (history[i] == curs) + panel_hist.pd->curs = j; + if (panel_hist.pd->filtering && !match_substr(USTR(history[i]->cmd))) + continue; + panel_hist.hist[j++] = history[i]; + } + panel_hist.pd->cnt = j; +} + +int +hist_prepare(void) +{ + panel_hist.pd->filtering = 0; + panel_hist.pd->curs = -1; + hist_panel_data(); + panel_hist.pd->top = panel_hist.pd->min; + panel_hist.pd->curs = pn_index > 0 ? pn_index : 0; + + panel = panel_hist.pd; + textline = &line_cmd; + return 0; +} + +const HIST_ENTRY * +get_history_entry(int i) +{ + return (i >= 0 && i < hs_cnt) ? history[i] : 0; +} + +void +hist_reset_index(void) +{ + pn_index = -1; +} + +/* + * hist_save() puts the command 'cmd' on the top + * of the command history list. + */ +void +hist_save(const wchar_t *cmd, int failed) +{ + int i; + FLAG new = 1; + HIST_ENTRY *x, *top; + + hist_reset_index(); + + for (top = history[0], i = 0; i < hs_alloc; i++) { + x = history[i]; + history[i] = top; + top = x; + if (i == hs_cnt) { + hs_cnt++; + break; + } + if (wcscmp(USTR(top->cmd),cmd) == 0) { + /* avoid duplicates */ + new = 0; + break; + } + } + if (new) + usw_copy(&top->cmd,cmd); + top->failed = failed; + + history[0] = top; +} + +/* file panel functions */ + +static void +warn_fail(int i) +{ + if (i >= 0 && i < hs_cnt && history[i]->failed) + msgout(MSG_i,"this command failed last time"); +} + +/* copy next (i.e. more recent) command from the history list */ +void +cx_hist_next(void) +{ + if (pn_index == -1) { + msgout(MSG_i,"end of the history list (newest command)"); + return; + } + + if (pn_index-- == 0) + edit_putstr(USTR(save_line)); + else { + edit_putstr(USTR(history[pn_index]->cmd)); + warn_fail(pn_index); + } +} + +/* copy previous (i.e. older) command from the history list */ +void +cx_hist_prev(void) +{ + if (pn_index >= hs_cnt - 1) { + msgout(MSG_i,"end of the history list (oldest command)"); + return; + } + + if (++pn_index == 0) + usw_xchg(&save_line,&line_cmd.line); + edit_putstr(USTR(history[pn_index]->cmd)); + warn_fail(pn_index); +} + +/* history panel functions */ + +void +cx_hist_paste(void) +{ + int i, len; + const char *lex; + + len = textline->size; + if (len > 0 && textline->curs == len) { + /* appending */ + lex = cmd2lex(USTR(textline->line)); + for (i = len - 1; i >= 0 && IS_LEX_SPACE(lex[i]); i--) + ; + if (i >= 0) { + if (i == len - 1) + edit_nu_insertchar(L' '); + if (lex[i] != LEX_CMDSEP) + edit_nu_insertstr(L"; ",QUOT_NONE); + } + + } + edit_insertstr(USTR(panel_hist.hist[panel_hist.pd->curs]->cmd),QUOT_NONE); + if (panel->filtering == 1) + /* move focus from the filter to the command line */ + cx_filter(); +} + +void +cx_hist_mouse(void) +{ + if (MI_PASTE) + cx_hist_paste(); +} + +void +cx_hist_enter(void) +{ + if (line_cmd.size == 0) + cx_hist_paste(); + + next_mode = MODE_SPECIAL_RETURN; +} + +void +cx_hist_del(void) +{ + int i; + HIST_ENTRY *del; + FLAG move; + + del = panel_hist.hist[panel_hist.pd->curs]; + hs_cnt--; + for (move = 0, i = 0; i < hs_cnt; i++) { + if (history[i] == del) { + if (pn_index > i) + pn_index--; + else if (pn_index == i) + hist_reset_index(); + move = 1; + } + if (move) + history[i] = history[i + 1]; + } + history[hs_cnt] = del; + hist_panel_data(); + pan_adjust(panel_hist.pd); + win_panel(); +} diff --git a/src/history.h b/src/history.h new file mode 100644 index 0000000..97b71e2 --- /dev/null +++ b/src/history.h @@ -0,0 +1,13 @@ +#define hist_initialize hist_reconfig +extern void hist_reconfig(void); +extern int hist_prepare(void); +extern void hist_panel_data(void); +extern void hist_save(const wchar_t *, int); +extern void hist_reset_index(void); +extern const HIST_ENTRY *get_history_entry(int i); +extern void cx_hist_prev(void); +extern void cx_hist_next(void); +extern void cx_hist_paste(void); +extern void cx_hist_mouse(void); +extern void cx_hist_enter(void); +extern void cx_hist_del(void); diff --git a/src/inout.c b/src/inout.c new file mode 100644 index 0000000..665a046 --- /dev/null +++ b/src/inout.c @@ -0,0 +1,2142 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* struct timeval */ +#include /* log.h */ +#include /* strcpy() */ +#include /* getenv() */ +#include /* time() */ +#include /* iswdigit() */ +#ifdef HAVE_TERM_H +# include /* enter_bold_mode */ +#endif +#include "curses.h" + +#include "inout.h" + +#include "cfg.h" /* cfg_num() */ +#include "control.h" /* get_current_mode() */ +#include "directory.h" /* dir_split_dir() */ +#include "edit.h" /* edit_adjust() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2w() */ +#include "panel.h" /* pan_adjust() */ +#include "signals.h" /* signal_initialize() */ +#include "tty.h" /* tty_press_enter() */ + +#ifndef A_NORMAL +# define A_NORMAL 0 +#endif + +#ifndef A_BOLD +# define A_BOLD A_STANDOUT +#endif + +#ifndef A_REVERSE +# define A_REVERSE A_STANDOUT +#endif + +#ifndef A_UNDERLINE +# define A_UNDERLINE A_STANDOUT +#endif + +#ifndef ACS_HLINE +# define ACS_HLINE '-' +#endif + +static chtype attrr, attrb; /* reverse and bold or substitutes */ +static const wchar_t *title; /* panel title (if not generated) */ +static int framechar; /* panel frame character (note that ACS_HLINE is an int) */ + +/* win_position() controls */ +static struct { + CODE resize; /* --( COLSxLINES )-- window size */ + CODE wait; /* --< PLEASE WAIT >-- message */ + FLAG wait_ctrlc;/* append "Ctrl-C TO ABORT" to the message above */ + FLAG update; /* --< CURSOR/TOTAL >-- normal info */ + /* resize, wait: + * 2 = msg should be displayed + * 1 = msg is displayed and should be cleared + * 0 = msg is not displayed + * update: + * 1 = data changed --> update the screen + * 0 = no change + */ +} posctl; + +/* line numbers and column numbers for better readability */ +#define LNO_TITLE 0 +#define LNO_FRAME1 1 +#define LNO_PANEL 2 +#define LNO_FRAME2 (disp_data.panlines + 2) +#define LNO_INFO (disp_data.panlines + 3) +#define LNO_HELP (disp_data.panlines + 4) +#define LNO_BAR (disp_data.panlines + 5) +#define LNO_EDIT (disp_data.panlines + 6) +#define MARGIN1 1 /* left or right margin - 1 column */ +#define MARGIN2 2 /* left or right margin - 2 columns */ +#define BOX4 4 /* checkbox or radiobutton */ +#define CNO_FILTER 15 /* user's filter string starting column */ +static int filter_width = 0; /* columns written by win_filter() */ + +/* help line (the second info line) controls */ +#define HELPTMPTIME 5 /* duration of help_tmp in seconds */ +static struct { + const wchar_t *help_base; /* default help string for the current mode */ + /* help_base can be overriden by panel->help */ + const wchar_t *help_tmp; /* temporary message, highlighted, dismissed automatically + after HELPTMPTIME when a key is pressed */ + time_t exp_tmp; /* expiration time for 'help_tmp' */ + const wchar_t *info; /* "-- info message --", + dismissed automatically when a key is pressed */ + const wchar_t *warning; /* "Warning message. Press any key to continue" */ +} helpline; + +/* number of chars written to display lines, used for cursor position calculations */ +static int chars_in_line[MAX_CMDLINES]; + +static wchar_t *bar; /* win_bar() output */ + +static void win_helpline(void); /* defined below */ +static void win_position(void); /* defined below */ + +static char type_symbol[][5] = { + " ", "exec", "suid", "Suid", "sgid", "/DIR", "/MNT", + "Bdev", "Cdev", "FIFO", "sock", "spec", " ??" +}; /* must correspond with FT_XXX */ + +#define CHECKBOX(X) do { addstr(X ? "[x] " : "[ ] "); } while (0) +#define RADIOBUTTON(X) do { addstr(X ? "(x) " : "( ) "); } while (0) + +#define OFFSET0 (textline->offset ? 1 : textline->promptwidth) + +/* draw everything from scratch */ +static void +screen_draw_all(void) +{ + int y, x; + + for (;/* until break */;) { + clear(); + getmaxyx(stdscr,y,x); + disp_data.scrcols = x; + disp_data.scrlines = y; + disp_data.pancols = x - 2 * MARGIN2; + disp_data.panrcol = x - MARGIN2; + disp_data.cmdlines = cfg_num(CFG_CMD_LINES); + disp_data.panlines = y - disp_data.cmdlines - 6; + /* + * there are 2 special positions: the bottom-right corner + * is always left untouched to prevent automatic scrolling + * and the position just before it is reserved for the + * '>' continuation mark + */ + if (x >= MIN_COLS && y >= MIN_LINES) + break; + printw("CLEX: this %dx%d window is too small. " + "Press ctrl-C to exit or enlarge the window to at least " STR(MIN_LINES) "x" STR(MIN_COLS) +#ifndef KEY_RESIZE + " and press a key to continue" +#endif + ". ",y,x); + refresh(); + if (getch() == CH_CTRL('C')) + err_exit("Display window is too small"); + } + attrset(A_NORMAL); + win_frame(); + win_bar(); + if (panel) { + /* panel is NULL only when starting clex */ + win_title(); + pan_adjust(panel); + win_panel(); + win_infoline(); + win_helpline(); + win_filter(); + } + edit_adjust(); + win_edit(); +} + +/* start CURSES */ +void +curses_initialize(void) +{ + int i; + const char *term, *astr; + static const char *compat[] = { "xterm","kterm","Eterm","dtterm","rxvt","aixterm" }; + static const char *not_compat[] = { "ansi","vt","linux","dumb" }; + + if (disp_data.wait) + tty_press_enter(); + + initscr(); /* restores signal dispositions on FreeBSD ! */ + signal_initialize(); /* FreeBSD initscr() bug workaround ! */ + raw(); + nonl(); + noecho(); + keypad(stdscr,TRUE); + notimeout(stdscr,TRUE); + scrollok(stdscr,FALSE); + clear(); + refresh(); + disp_data.curses = 1; + + if (enter_reverse_mode && *enter_reverse_mode) + attrr = A_REVERSE; + else + attrr = A_STANDOUT; + if (enter_bold_mode && *enter_bold_mode) + attrb = A_BOLD; + else if (enter_underline_mode && *enter_underline_mode) + attrb = A_UNDERLINE; + else + attrb = A_STANDOUT; + + win_frame_reconfig(); + screen_draw_all(); + + disp_data.bs177 = key_backspace && strcmp(key_backspace,"\177") == 0; + + astr = "not known"; + if ( (term = getenv("TERM")) ) { + for (i = 0; i < ARRAY_SIZE(compat); i++) + if (strncmp(term,compat[i],strlen(compat[i])) == 0) { + disp_data.xterm = 1; + astr = "yes"; + break; + } + if (!disp_data.xterm) + for (i = 0; i < ARRAY_SIZE(not_compat); i++) + if (strncmp(term,not_compat[i],strlen(not_compat[i])) == 0) { + disp_data.noxterm = 1; + astr = "no"; + break; + } + } + msgout(MSG_DEBUG,"Terminal type: \"%s\", can change the window title: %s", + term ? term : "undefined", astr); + + disp_data.xwin = getenv("WINDOWID") && getenv("DISPLAY"); + msgout(MSG_DEBUG,"X Window: %s", + disp_data.xwin ? "detected" : "not detected (no $DISPLAY and/or no $WINDOWID)"); + +#ifdef KEY_MOUSE +# if NCURSES_MOUSE_VERSION >= 2 + /* version 1 does not work with the mouse wheel */ + if (key_mouse && *key_mouse) { + msgout(MSG_DEBUG,"Mouse interface: ncurses mouse version %d", NCURSES_MOUSE_VERSION); +# else + if (key_mouse && strcmp(key_mouse,"\033[<") == 0) + msgout(MSG_DEBUG,"Mouse interface: xterm SGR 1006 mode is NOT supported by CLEX"); + /* reason: ncurses mouse is the preferred interface */ + if (key_mouse && strcmp(key_mouse,"\033[M") == 0) { + msgout(MSG_DEBUG,"Mouse interface: xterm normal tracking mode"); +# endif + disp_data.mouse = 1; + } + else + msgout(MSG_DEBUG,"Mouse interface: not found"); +#endif /* KEY_MOUSE */ +} + +/* restart CURSES */ +void +curses_restart(void) +{ + if (disp_data.wait) + tty_press_enter(); + + reset_prog_mode(); + touchwin(stdscr); + disp_data.curses = 1; + screen_draw_all(); +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +curses_stop(void) +{ + clear(); + refresh(); + endwin(); + disp_data.curses = 0; +} + +void +curses_cbreak(void) +{ + cbreak(); + posctl.wait_ctrlc = 1; +} + +void +curses_raw(void) +{ + raw(); + posctl.wait_ctrlc = 0; +} + +/* set cursor to the proper position and refresh screen */ +static void +screen_refresh(void) +{ + int i, posx, posy, offset; + + if (posctl.wait || posctl.resize || posctl.update) + win_position(); /* display/clear message */ + + if (panel->filtering == 1) + move(LNO_FRAME2,CNO_FILTER + wc_cols(panel->filter->line,0,panel->filter->curs)); + else { + posy = LNO_EDIT; + posx = 0; + if (textline != 0) { + for (i = 0, offset = textline->offset; i < disp_data.cmdlines - 1 + && textline->curs >= offset + chars_in_line[i]; i++) + offset += chars_in_line[i]; + posx = wc_cols(USTR(textline->line),offset,textline->curs); + if (i == 0) + posx += OFFSET0; + else + posy += i; + } + move(posy,posx); + } + refresh(); +} + +/****** mouse input functions ******/ + +/* which screen area belongs the line 'ln' to ? */ +static int +screen_area(int ln, int col) +{ + if (ln < 0 || col < 0 || ln >= disp_data.scrlines || col >= disp_data.scrcols) + return -1; + if (ln == 0) + return AREA_TITLE; + if (ln == 1) + return AREA_TOPFRAME; + if ((ln -= 2) < disp_data.panlines) + return AREA_PANEL; + if ((ln -= disp_data.panlines) == 0) + return AREA_BOTTOMFRAME; + if (ln == 1) + return AREA_INFO; + if (ln == 2) + return AREA_HELP; + if (ln == 3) + return AREA_BAR; + if (ln == 4 && textline && col < textline->promptwidth) + return AREA_PROMPT; + return AREA_LINE; +} + +/* screen position -> input line cursor */ +static int +scr2curs(int y, int x) +{ + int first, last, i; + wchar_t *line; + + if (y == 0 && (x -= OFFSET0) < 0) /* compensate for the prompt/continuation mark */ + return -1; /* in front of the text */ + + /* first = index of the first character in the line */ + for (first = textline->offset, i = 0; i < y; i++) + first += chars_in_line[i]; + if (first > textline->size) + return -1; /* unused line */ + + /* last = index of the last character in the line (may be the terminating null) */ + last = first + chars_in_line[y] - 1; + LIMIT_MAX(last,textline->size); + + line = USTR(textline->line); + for (i = first; i <= last; i++) { + x -= WCW(line[i]); + if (x <= 0) + return i; + } + return -1; /* after the text */ +} + +/* screen position -> filter line cursor */ +static int +scr2curs_filt(int x) +{ + int i; + wchar_t *line; + + if ((x -= CNO_FILTER) < 0) + return -1; /* in front of the text */ + + line = panel->filter->line; + for (i = 0; i <= panel->filter->size; i++) { + x -= WCW(line[i]); + if (x <= 0) + return i; + } + return -1; /* after the text */ +} + +/* screen position -> help link link number */ +static int +scr2curs_help(int y, int x) +{ + int i, width, curs, links; + HELP_LINE *ph; + + curs = panel->top + y; + if (curs < 0 || curs >= panel->cnt) + return -1; + + ph = panel_help.line[curs]; + links = ph->links; + if (links <= 1) + return links - 1; + + /* multiple links */ + for (width = wc_cols(ph->text,0,-1), i = 0; i < links - 1; i++) { + x -= wc_cols(ph[3 * i + 2].text,0,-1) + width; + width = wc_cols(ph[3 * i + 3].text,0,-1); + if (x <= width / 2) + return i; + } + return i; +} + +/* note: single width characters assumed */ +static int +scr2curs_bar(int x) +{ + int curs; + + x--; /* make 0-based */ + if (x < 0 || x >= wcslen(bar) || bar[x] == L' ') + return -1; + + for (curs = 0; --x > 0; ) { + if (bar[x] == L'|') + return -1; + if (bar[x] == L' ' && bar[x-1] != L' ') + curs++; + } + return curs; +} + +/* read the mouse tracking data */ +static int +mouse_data(void) +{ + FLAG click; + static struct timeval prev, now; + static MOUSE_INPUT miprev; + +#if NCURSES_MOUSE_VERSION >= 2 + mmask_t mstat; + MEVENT mevent; + static CODE btn; /* active real mouse button (i.e. not wheel) or 0 */ + + if (getmouse(&mevent) != OK) + return -1; + minp.x = mevent.x; + minp.y = mevent.y; + mstat = mevent.bstate; + minp.motion = (mstat & REPORT_MOUSE_POSITION) != 0; + if (minp.motion) + minp.button = btn; + else if (mstat & BUTTON1_PRESSED) + btn = minp.button = disp_data.mouse_swap ? 3 : 1; + else if (mstat & BUTTON2_PRESSED) + btn = minp.button = 2; + else if (mstat & BUTTON3_PRESSED) + btn = minp.button = disp_data.mouse_swap ? 1 : 3; + else if (mstat & BUTTON4_PRESSED) + minp.button = 4; + else if (mstat & BUTTON5_PRESSED) + minp.button = 5; + else { + /* button release */ + if (mstat & BUTTON1_RELEASED) { + if (btn == 1) + btn = 0; + } + else if (mstat & BUTTON2_RELEASED) { + if (btn == 2) + btn = 0; + } + else if (mstat & BUTTON3_RELEASED) { + if (btn == 3) + btn = 0; + } + return -1; + } + +#else + /* fallback: XTERM mouse */ + int mstat; + + keypad(stdscr,FALSE); + mstat = getch() - 32; + minp.x = getch() - 33; + minp.y = getch() - 33; + keypad(stdscr,TRUE); + if (mstat < 0) + return -1; + + /* button */ + switch (mstat & 0x43) { + case 0: + minp.button = disp_data.mouse_swap ? 3 : 1; break; + case 1: + minp.button = 2; break; + case 2: + minp.button = disp_data.mouse_swap ? 1 : 3; break; + case 64: + minp.button = 4; break; + case 65: + minp.button = 5; break; + default: + return -1; /* button release or a bogus event */ + } + minp.motion = mstat & 0x20; /* mouse is in motion */ +#endif + + if ((minp.area = screen_area(minp.y, minp.x)) < 0) + return -1; + + /* event accepted */ + miprev = minp; + prev = now; + gettimeofday(&now,0); + + click = minp.button >= 1 && minp.button <= 3; + minp.doubleclick = !miprev.doubleclick && click && minp.button == miprev.button && !minp.motion + && minp.x == miprev.x && minp.y == miprev.y && now.tv_sec - prev.tv_sec < 2 + && 1000 * (int)(now.tv_sec - prev.tv_sec) + (int)(now.tv_usec - prev.tv_usec) / 1000 + <= cfg_num(CFG_DOUBLE_CLICK); + + minp.ypanel = (click && minp.area == AREA_PANEL) ? minp.y - LNO_PANEL : -1; + minp.cursor = -1; + if (click) + switch (minp.area) { + case AREA_LINE: + if (textline) + /* input line cursor */ + minp.cursor = scr2curs(minp.y - LNO_EDIT,minp.x); + break; + case AREA_BAR: + minp.cursor = scr2curs_bar(minp.x); + break; + case AREA_BOTTOMFRAME: + if (panel->filter) + /* filter expression cursor */ + minp.cursor = scr2curs_filt(minp.x); + break; + case AREA_PANEL: + if (panel == panel_help.pd) + /* help link index */ + minp.cursor = scr2curs_help(minp.y - LNO_PANEL,minp.x - MARGIN2); + break; + } + + return 0; +} + +/****** keyboard input functions ******/ + +void +kbd_rawkey(void) +{ + int retries, type; + + screen_refresh(); + + kinp.prev_esc = kinp.fkey == 0 && kinp.key == WCH_ESC; + do { + retries = 10; + do { + if (--retries < 0) + err_exit("Cannot read the keyboard input"); + type = get_wch(&kinp.key); + } while (type == ERR || kinp.key == WEOF); + if (type == KEY_CODE_YES) { +#ifdef KEY_MOUSE + if (kinp.key == KEY_MOUSE) { + kinp.fkey = 2; + kinp.key = 0; /* no more #ifdef KEY_MOUSE */ + } + else +#endif + kinp.fkey = 1; + } + else + kinp.fkey = 0; + } while ((kinp.fkey == 0 && kinp.key == L'\0') || (kinp.fkey == 2 && mouse_data() < 0)); +} + +/* + * get next input char, no processing except screen resize and screen redraw, + * use this input function to get unfiltered input + */ +static wint_t +kbd_getany(void) +{ + for (;/* until return */;) { + kbd_rawkey(); + + if (kinp.fkey == 0 && kinp.key == WCH_CTRL('L')) + wrefresh(curscr); /* redraw screen */ + else if (kinp.fkey == 0 && kinp.key == WCH_ESC) + /* ignore */; +#ifdef KEY_RESIZE + else if (kinp.fkey == 1 && kinp.key == KEY_RESIZE) { + posctl.resize = 2; + screen_draw_all(); + } +#endif + else + return kinp.key; + } +} + +const char * +char_code(int value) +{ + static char buffer[24]; + + if (*buffer == '\0') + strcpy(buffer,lang_data.utf8 ? "U+" : "\\x"); + sprintf(buffer + 2,"%0*X",value > 0xFF ? 4 : 2,value); + return buffer; +} + +static const char * +ascii_code(int value) +{ + static char *ascii[] = { + "", /* null */ + ", ctrl-A, SOH (start of heading)", + ", ctrl-B, STX (start of text)", + ", ctrl-C, ETX (end of text)", + ", ctrl-D, EOT (end of transmission)", + ", ctrl-E, ENQ (enquiry)", + ", ctrl-F, ACK (acknowledge)", + ", ctrl-G, BEL (bell)", + ", ctrl-H, BS (backspace)", + ", ctrl-I, HT (horizontal tab)", + ", ctrl-J, LF (new line) (line feed)", + ", ctrl-K, VT (vertical tab)", + ", ctrl-L, FF (form feed)", + ", ctrl-M, CR (carriage return)", + ", ctrl-N, SO (shift out)", + ", ctrl-O, SI (shift in)", + ", ctrl-P, DLE (data link escape)", + ", ctrl-Q, DC1 (device control 1)", + ", ctrl-R, DC2 (device control 2)", + ", ctrl-S, DC3 (device control 3)", + ", ctrl-T, DC4 (device control 4)", + ", ctrl-U, NAK (negative acknowledgment)", + ", ctrl-V, SYN (synchronous idle)", + ", ctrl-W, ETB (end of transmission block)", + ", ctrl-X, CAN (cancel)", + ", ctrl-Y, EM (end of medium)", + ", ctrl-Z, SUB (substitute)", + ", ESC (escape)", + ", FS (file separator)", + ", GS (group separator)", + ", RS (record separator)", + ", US (unit separator)" + }; + + if (lang_data.utf8 && value == L'\xAD') + return ", SHY (soft hyphen)"; + if (lang_data.utf8 && value == L'\xA0') + return ", NBSP (non-breaking space)"; + return (value > 0 && value < ARRAY_SIZE(ascii)) ? ascii[value] : ""; +} + +/* check for key + shift combination not understood by the curses library */ +static wint_t +shift_key(wint_t key) +{ + int i; + const char *name; + static struct { + const char *n; + wint_t k; + } keytable[] = { + { "LFT", KEY_LEFT }, + { "RIT", KEY_RIGHT }, + { "UP", KEY_UP }, + { "DN", KEY_DOWN }, + { "HOM", KEY_HOME }, + { "END", KEY_END }, + { "IC", KEY_IC }, + { "DC", KEY_DC }, + { "PRV", KEY_PPAGE }, + { "NXT", KEY_NPAGE } + }; + + name = keyname(key); + if (*name != 'k') + return 0; + + name++; + for (i = 0; i < ARRAY_SIZE(keytable); i++) + if (strncmp(name,keytable[i].n,strlen(keytable[i].n)) == 0) + return keytable[i].k; + + return 0; +} + +/* get next input char */ +wint_t +kbd_input(void) +{ + wchar_t ch; + wint_t shkey; + + /* show ASCII code */ + if (helpline.info == 0 + && ((panel->filtering == 1 && (ch = panel->filter->line[panel->filter->curs]) != L'\0') + || (textline != 0 && (ch = USTR(textline->line)[textline->curs]) != L'\0')) + && !ISWPRINT(ch)) + msgout(MSG_i,"special character %s%s",char_code(ch),ascii_code(ch)); + + kbd_getany(); + + /* 1 --> , 2 --> , ... 0 --> */ + if (kinp.prev_esc && kinp.fkey == 0 && iswdigit(kinp.key)) { + kinp.prev_esc = 0; + kinp.fkey = 1; + kinp.key = KEY_F(((kinp.key - L'0') + 9) % 10 + 1); + } + + if (kinp.fkey == 1 && (shkey = shift_key(kinp.key))) { + kinp.prev_esc = 1; + kinp.key = shkey; + } + + /* dismiss remarks (if any) */ + if (helpline.info) + win_sethelp(HELPMSG_INFO,0); + else if (helpline.help_tmp && time(0) > helpline.exp_tmp) + win_sethelp(HELPMSG_TMP,0); + + return kinp.key; +} + +/****** output functions ******/ + +/* + * following functions write to these screen areas: + * + * win_title, win_settitle /usr/local + * win_frame ---------- + * win_panel DIR bin + * win_panel >DIR etc < + * win_panel DIR man + * win_frame, win_filter, + * win_waitmsg, win_position -< filt >----< 3/9 >- + * win_infoline 0755 rwxr-xr-x + * win_sethelp alt-M for menu + * win_bar [ CLEX file manager ] + * win_edit shell $ ls -l_ + */ + +#define BLANK(X) do { char_line(' ',X); } while (0) +/* write a line of repeating chars */ +static void +char_line(int ch, int cnt) +{ + while (cnt-- > 0) + addch(ch); +} + +/* + * putwcs_trunc() writes wide string 'str' padding or truncating it + * to the total size of 'maxwidth' display columns. Its behavior + * may be altered by OPT_XXX options (may be OR-ed together). + * Non-printable characters are replaced by a question mark + */ +#define OPT_NOPAD 1 /* do not pad */ +#define OPT_NOCONT 2 /* truncate without the continuation mark '>' */ +#define OPT_SQUEEZE 4 /* squeeze long string in the middle: abc...xyz */ +/* + * return value: + * if OPT_SQUEEZE is used, the return value is: + * -1 if the string had to be squeezed in the middle + * >= 0 if the string was short enough to display it normally + * if OPT_NOPAD is given, it returns the number of display columns left unused + * otherwise it returns the number of characters written including padding, + * but excluding the continuation mark + */ +static int +putwcs_trunc(const wchar_t *str, int maxwidth, int options) +{ + wchar_t ch; + int i, chcnt, remain, width, p2, len, dots, part1, part2; + FLAG printable; + + if (maxwidth <= 0) + return 0; + if (str == 0) + str = L"(null!)"; /* should never happen */ + + if (options & OPT_SQUEEZE) { + len = wcslen(str); + if (wc_cols(str,0,len) > maxwidth) { + dots = maxwidth >= 6 ? 4 : 1; + part1 = 3 * (maxwidth - dots) / 8; + part2 = maxwidth - dots - part1; + part2 += putwcs_trunc(str,part1,OPT_NOCONT | OPT_NOPAD); + /* fine-tune the width */ + p2 = len - part2; + LIMIT_MIN(p2,0); + width = wc_cols(str,p2,len); + while (width < part2 && p2 > 0) { + p2--; + width += WCW(str[p2]); + } + while (width > part2 && p2 < len) { + width -= WCW(str[p2]); + p2++; + } + while (utf_iscomposing(str[p2])) + p2++; + char_line('.',dots + part2 - width); + putwcs_trunc(str + p2,part2,0); + return -1; + } + /* else: SQUEEZE option is superfluous --> ignored */ + } + + chcnt = 0; /* char counter */ + for (remain = maxwidth, i = 0; (ch = str[i]) != L'\0';) { + width = (printable = ISWPRINT(ch)) ? wcwidth(ch) : 1; + if (width > 0 && width == remain && !(options & OPT_NOCONT) && str[i + 1] != L'\0') + break; + if (width > remain) + break; + remain -= width; + if (printable) + i++; + else { + if (i > 0) + addnwstr(str,i); + addnwstr(&lang_data.repl,1); + str += i + 1; + chcnt += i + 1; + i = 0; + } + } + if (i > 0) { + addnwstr(str,i); + chcnt += i; + } + + if (ch == L'\0') + /* more space than text -> padding */ + chcnt += remain; + else + /* more text than space -> truncating */ + if (!(options & OPT_NOCONT)) { + addch('>' | attrb); /* continuation mark in bold font */ + remain--; + } + + if (remain && !(options & OPT_NOPAD)) + BLANK(remain); + + return (options & OPT_NOPAD) ? remain : chcnt; +} + +/* + * normal (not wide) char version of putwcs_trunc() + * - does not support OPT_SQUEEZE + * - does not detect unprintable characters + * - assumes 1 char = 1 column, should not be used for internationalized text + */ +static int +putstr_trunc(const char *str, int maxwidth, int options) +{ + int chcnt, remain; + + if (maxwidth <= 0) + return 0; + + chcnt = strlen(str); + if (chcnt < maxwidth) { + addstr(str); + remain = maxwidth - chcnt; + } + else { + if (options & OPT_NOCONT) + addnstr(str,chcnt = maxwidth); + else { + addnstr(str,chcnt = maxwidth - 1); + addch('>' | attrb); /* continuation mark in bold font */ + } + remain = 0; + } + if (remain && !(options & OPT_NOPAD)) + BLANK(remain); + + return (options & OPT_NOPAD) ? remain : chcnt; +} + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" + +/* like putwcs_trunc, but stopping just before the 'endcol' screen column */ +static int +putwcs_trunc_col(const wchar_t *str, int endcol, int options) +{ + int y, x; + + getyx(stdscr,y,x); + return putwcs_trunc(str,endcol - x,options); +} + +static int +putstr_trunc_col(const char *str, int endcol, int options) +{ + int y, x; + + getyx(stdscr,y,x); + return putstr_trunc(str,endcol - x,options); +} + +#pragma GCC diagnostic pop + +void +win_frame_reconfig(void) +{ + switch(cfg_num(CFG_FRAME)) { + case 0: + framechar = '-'; + break; + case 1: + framechar = '='; + break; + default: + framechar = ACS_HLINE; + } +} + +void +win_frame(void) +{ + move(LNO_FRAME1,0); + char_line(framechar,disp_data.scrcols); + move(LNO_FRAME2,0); + char_line(framechar,disp_data.scrcols); +} + +/* file panel title: primary + secondary panel's directory */ +static void +twodirs(void) +{ + int w1, w2, rw1, opt1, opt2, width; + const wchar_t *dir1, *dir2; + + /* directory names are separated by 2 spaces; their width is kept in ratio 5:3 */ + width = disp_data.scrcols - 2; /* available space */ + dir1 = USTR(ppanel_file->dirw); + rw1 = w1 = wc_cols(dir1,0,-1); + dir2 = USTR(ppanel_file->other->dirw); + w2 = wc_cols(dir2,0,-1); + opt1 = opt2 = 0; + if (w1 + w2 <= width) + w1 = width - w2; /* enough space */ + else if (w1 <= (5 * width) / 8) { + w2 = width - w1; /* squeeze second */ + opt2 = OPT_SQUEEZE; + } + else if (w2 <= (3 * width) / 8) { + w1 = width - w2; /* squeeze first */ + opt1 = OPT_SQUEEZE; + } + else { + w1 = (5 * width) / 8; /* squeeze both */ + w2 = width - w1; + opt1 = opt2 = OPT_SQUEEZE; + } + attron(attrb); + putwcs_trunc(dir1,w1,opt1); + attroff(attrb); + addstr(" "); + putwcs_trunc(dir2,w2,opt2); + + disp_data.dir1end = w1 < rw1 ? w1 : rw1; + disp_data.dir2start = w1 + 2; /* exactly +3, but +2 feels more comfortable */ +} + +/* panel title - top screen line */ +void +win_title(void) +{ + move(LNO_TITLE,0); + switch (get_current_mode()) { + case MODE_COMPL: + addch(' '); + putwcs_trunc(panel_compl.title,disp_data.scrcols,0); + break; + case MODE_FILE: + twodirs(); + break; + case MODE_HELP: + addstr(" HELP: "); + attron(attrb); + putwcs_trunc_col(panel_help.title,disp_data.scrcols,0); + attroff(attrb); + break; + case MODE_PREVIEW: + addstr(" PREVIEW: "); + attron(attrb); + putwcs_trunc_col(panel_preview.title,disp_data.scrcols,0); + attroff(attrb); + break; + default: + /* static title */ + addch(' '); + putwcs_trunc(title,disp_data.scrcols,0); + } +} + +void +win_settitle(const wchar_t *newtitle) +{ + title = newtitle; + win_title(); +} + +static void +print_position(const wchar_t *msg, int bold) +{ + static int prev_pos_start = 0; + int pos_start, filter_stop; + + pos_start = disp_data.scrcols - wc_cols(msg,0,-1) - MARGIN2; + + filter_stop = MARGIN2 + filter_width; + if (filter_stop > pos_start) { + /* clash with the filter -> do not display position */ + move (LNO_FRAME2,filter_stop); + char_line(framechar,disp_data.scrcols - filter_stop - MARGIN2); + return; + } + + if (pos_start > prev_pos_start) { + move(LNO_FRAME2,prev_pos_start); + char_line(framechar,pos_start - prev_pos_start); + } + else + move(LNO_FRAME2,pos_start); + prev_pos_start = pos_start; + + if (bold) + attron(attrb); + addwstr(msg); + if (bold) + attroff(attrb); +} + +static void +win_position(void) +{ + wchar_t buffer[64], selected[32], *hidden; + + if (posctl.resize == 2) { + swprintf(buffer,ARRAY_SIZE(buffer),L"( %dx%d )",disp_data.scrcols,disp_data.scrlines); + print_position(buffer,1); + posctl.resize = 1; + return; + } + + if (posctl.wait == 2) { + if (posctl.wait_ctrlc) + print_position(L"< PLEASE WAIT - CTRL-C TO ABORT >",1); + else + print_position(L"< PLEASE WAIT >",1); + posctl.wait = 1; + return; + } + + posctl.wait = posctl.resize = posctl.update = 0; + + if (panel->cnt == 0) { + print_position(L"< NO DATA >",1); + return; + } + + if (panel->curs < 0) { + print_position(L"",0); + return; + } + + if (panel->type == PANEL_TYPE_FILE && ppanel_file->selected) + swprintf(selected,ARRAY_SIZE(selected),L" [%d]",ppanel_file->selected); + else + selected[0] = L'\0'; + hidden = panel->type == PANEL_TYPE_FILE && ppanel_file->hidden ? L"HIDDEN " : L""; + + swprintf(buffer,ARRAY_SIZE(buffer),L"<%ls %d/%d %ls>", + selected,panel->curs + 1,panel->cnt,hidden); + print_position(buffer,0); +} + +void +win_waitmsg(void) +{ + if (disp_data.curses && posctl.wait <= 1) { + posctl.wait = 2; + screen_refresh(); + } +} + +void +win_filter(void) +{ + int width; + const wchar_t *label, *close; + FLAG filepanel; + + move(LNO_FRAME2,MARGIN2); + if (!panel->filtering) + width = 0; + else { + width = CNO_FILTER + wc_cols(panel->filter->line,0,-1); + filepanel = panel->type == PANEL_TYPE_FILE; + + if (panel->type == PANEL_TYPE_HELP) { + label = L"( find text: "; + close = L" )"; + } + else if (filepanel && ppanel_file->filtype) { + label = L"[ pattern: "; + close = L" ]"; + } + else { + label = L"< filter: "; + close = L" >"; + } + char_line(framechar,CNO_FILTER - MARGIN2 - wc_cols(label,0,-1)); + addwstr(label); + attron(attrb); + putwcs_trunc(panel->filter->line,width - CNO_FILTER,0); + attroff(attrb); + addwstr(close); + } + + if (width < filter_width) + char_line(framechar,filter_width - width); + filter_width = width; +} + +/* "0644" -> "rw-r--r--" */ +static const char * +print_perms(const char *octal) +{ + static const char + *set1[8] = + { "---","--x","-w-","-wx","r--","r-x","rw-","rwx" }, + *set2[8] = + { "--S","--s","-wS","-ws","r-S","r-s","rwS","rws" }, + *set3[8] = + { "--T","--t","-wT","-wt","r-T","r-t","rwT","rwt" }; + static char perms[10]; + + strcpy(perms + 0,((octal[0] - '0') & 4 ? set2 : set1)[(octal[1] - '0') & 7]); + strcpy(perms + 3,((octal[0] - '0') & 2 ? set2 : set1)[(octal[2] - '0') & 7]); + strcpy(perms + 6,((octal[0] - '0') & 1 ? set3 : set1)[(octal[3] - '0') & 7]); + return perms; +} + +/* see CFG_LAYOUT1 for more information about 'fields' */ +static void +print_fields(FILE_ENTRY *pfe, int width, const wchar_t *fields) +{ + const char *txt; + const wchar_t *wtxt; + FLAG field, left_align; + wchar_t ch; + int i, fw; + + for (field = left_align = 0; width > 0 && (ch = *fields++); ) { + if (field == 0) { + if (ch == L'$') { + field = 1; + continue; + } + if (!ISWPRINT(ch)) { + ch = lang_data.repl; + fw = 1; + left_align = 1; + } + else { + fw = wcwidth(ch); + if (fw > width) + return; + /* choose proper alignment (left or right) */ + left_align = (ch != L' '); + } + addnwstr(&ch,1); + width -= fw; + } + else { + field = 0; + txt = 0; + wtxt = 0; + switch (ch) { + case L'a': /* access date/time */ + fw = disp_data.date_len; + wtxt = pfe->atime_str; + break; + case L'd': /* modification date/time */ + fw = disp_data.date_len; + wtxt = pfe->mtime_str; + break; + case L'g': /* file age */ + fw = FE_AGE_STR - 1 - ppanel_file->cw_age; + txt = pfe->age_str; + if (txt[0]) + txt += ppanel_file->cw_age; + break; + case L'i': /* inode change date/time */ + fw = disp_data.date_len; + wtxt = pfe->ctime_str; + break; + case L'l': /* links (total number) */ + fw = FE_LINKS_STR - 1 - ppanel_file->cw_ln1; + txt = pfe->links_str; + if (txt[0]) + txt += ppanel_file->cw_ln1; + break; + case L'L': /* links (flag) */ + fw = ppanel_file->cw_lnh; + txt = pfe->links ? "LNK" : ""; + break; + case L'm': /* file mode */ + fw = FE_MODE_STR - 1; + txt = pfe->mode_str; + break; + case L'M': /* file mode (alternative format) */ + fw = ppanel_file->cw_mod; + txt = pfe->normal_mode ? "" : pfe->mode_str; + break; + case L'o': /* owner */ + fw = ppanel_file->cw_ow2; + wtxt = pfe->owner_str; + if (wtxt[0]) + wtxt += ppanel_file->cw_ow1; + break; + case L'P': /* permissions (alternative format) */ + if (pfe->normal_mode) { + fw = ppanel_file->cw_mod ? 9 : 0; + txt = ""; + break; + } + /* no break */ + case L'p': /* permissions */ + fw = 9; /* rwxrwxrwx */ + txt = pfe->file_type == FT_NA ? "" : print_perms(pfe->mode_str); + break; + case L'r': /* file size (or device major/minor) */ + case L'R': + case L's': + case L'S': + fw = ppanel_file->cw_sz2; + txt = pfe->size_str; + if (txt[0]) + txt += ppanel_file->cw_sz1; + break; + case L't': /* file type */ + fw = 4; + txt = type_symbol[pfe->file_type]; + break; + case L'>': /* symbolic link */ + fw = ppanel_file->cw_lns; + txt = pfe->symlink ? "->" : ""; + break; + case L'*': /* selection mark */ + fw = 1; + txt = pfe->select ? "*" : " "; + break; + case L'$': /* literal $ */ + fw = 1; + txt = "$"; + break; + case L'|': /* literal | */ + fw = 1; + txt = "|"; + break; + default: /* syntax error */ + fw = 2; + txt = "$?"; + } + + if (fw > width) + return; + + if (txt) { + if (*txt == '\0') + /* txt == "" - leave the field blank */ + BLANK(fw); + else if (left_align && *txt == ' ') { + /* change alignment from right to left */ + for (i = 1; txt[i] == ' '; i++) + ; + addstr(txt + i); + BLANK(i); + } + else + addstr(txt); + } + else { + if (*wtxt == '\0') + /* txt == "" - leave the field blank */ + BLANK(fw); + else if (left_align && *wtxt == L' ') { + /* change alignment from right to left */ + for (i = 1; wtxt[i] == L' '; i++) + ; + addwstr(wtxt + i); + BLANK(i); + } + else + addwstr(wtxt); + } + width -= fw; + } + } +} + +/* information line */ +void +win_infoline(void) +{ + static const wchar_t + *info_cmp[] = { + 0, 0, + L"The mode is also known as access rights or permissions" + }, + *info_sort[] = { + 0, + 0, + 0, + 0, /* ------ */ + 0, + L"Note: directories . and .. are always on the top, despite the sort order", + L"Notes: . and .. always on the top, devices sorted by device number", + 0, /* ------ */ + L"Example: file42.txt comes after file9.txt, because 42 > 9", + 0, + L"The extension is also known as a file name suffix", + 0,0,0,0, + L"Useful in a sendmail queue directory" + }; + FILE_ENTRY *pfe; + const wchar_t *msg, *ts; + wchar_t *pch; + int curs; + + move(LNO_INFO,0); + addstr(" "); /* MARGIN2 */ + + /* extra panel lines */ + if (panel->curs < 0 && panel->min < 0 && !panel->filtering + && (msg = panel->extra[panel->curs - panel->min].info) != 0) { + putwcs_trunc_col(msg,disp_data.scrcols,0); + return; + } + + if (!VALID_CURSOR(panel)) { + clrtoeol(); + return; + } + + /* regular panel lines */ + curs = panel->curs; + msg = 0; + switch (panel->type) { + case PANEL_TYPE_CFG: + msg = panel_cfg.config[curs].help; + break; + case PANEL_TYPE_COMPL: + if ((msg = panel_compl.cand[curs]->aux) != 0 && panel_compl.aux != 0) + addwstr(panel_compl.aux); + break; + case PANEL_TYPE_FILE: + pfe = ppanel_file->files[curs]; + if (pfe->file_type == FT_NA) + msg = L"no status information available"; + else + print_fields(pfe,disp_data.scrcols - 2 * MARGIN2,disp_data.layout_line); + break; + case PANEL_TYPE_LOG: + putwcs_trunc(convert2w(panel_log.line[curs]->levelstr),16,0); + ts = convert2w(panel_log.line[curs]->timestamp); + /* some locales use non-breaking spaces, replace them in-place by regular spaces */ + for (pch = (wchar_t *)ts; *pch; pch++) + if (*pch == L'\xa0') + *pch = L' '; + putwcs_trunc_col(ts,disp_data.scrcols - MARGIN2,0); + break; + case PANEL_TYPE_CMP: + if (curs < ARRAY_SIZE(info_cmp)) + msg = info_cmp[curs]; + break; + case PANEL_TYPE_SORT: + if (curs < ARRAY_SIZE(info_sort)) + msg = info_sort[curs]; + break; + default: + ; + } + + if (msg) + putwcs_trunc(msg,disp_data.scrcols - MARGIN2,0); + else + clrtoeol(); +} + +/* information line */ +static void +win_helpline(void) +{ + const wchar_t *msg; + FLAG bold = 0; + + move(LNO_HELP,0); + + /* warnmsg has greater priority than a remark */ + if (helpline.warning) { + flash(); + msg = L" Press any key."; + attron(attrb); + putwcs_trunc(helpline.warning,disp_data.scrcols - wc_cols(msg,0,-1) - 1 /* dot */,OPT_NOPAD); + addch('.'); + attroff(attrb); + putwcs_trunc_col(msg,disp_data.scrcols,0); + return; + } + + if (helpline.info) { + attron(attrb); + addstr("-- "); + putwcs_trunc(helpline.info,disp_data.scrcols - 6 /* dashes */,OPT_NOPAD); + putstr_trunc_col(" --",disp_data.scrcols,0); + attroff(attrb); + return; + } + + if ((msg = helpline.help_tmp) != 0) { + bold = 1; + if (helpline.exp_tmp == 0) + helpline.exp_tmp = time(0) + HELPTMPTIME; + } + else if ((msg = panel->help) == 0 && (msg = helpline.help_base) == 0) { + clrtoeol(); + return; + } + + addch(' '); + BLANK(disp_data.scrcols - wc_cols(msg,0,-1) - 2 * MARGIN1); + if (bold) + attron(attrb); + putwcs_trunc_col(msg,disp_data.scrcols,0); + if (bold) + attroff(attrb); +} + +/* HELPMSG_XXX defined in inout.h */ +void +win_sethelp(int type, const wchar_t *msg) +{ + switch (type) { + case HELPMSG_BASE: + if (msg != 0 && helpline.help_base != 0) + return; /* must reset to 0 first! */ + helpline.help_base = msg; + break; + case HELPMSG_OVERRIDE: + panel->help = msg; + break; + case HELPMSG_TMP: + if ((helpline.help_tmp = msg) == 0) + helpline.exp_tmp = 0; + /* exception: HELPMSG_TMP can be called while curses is disabled */ + if (!disp_data.curses) + return; + break; + case HELPMSG_INFO: + helpline.info = msg; + break; + case HELPMSG_WARNING: + helpline.warning = msg; + win_helpline(); + kbd_getany(); + helpline.warning = 0; + } + win_helpline(); +} + +void +win_bar(void) +{ + int pad; + static int len = 0; + + if (len == 0) + len = wc_cols(user_data.loginw,0,-1) + 1 /* at sign */ + + wc_cols(user_data.hostw,0,-1) + MARGIN1 ; + + attron(attrr); + move(LNO_BAR,0); + + switch (get_current_mode()) { + case MODE_FILE: + bar = L" F1=help alt-M=menu | CLEX file manager " ; + break; + case MODE_HELP: + bar = L" F1=help ctrl-C=exit <-- | CLEX file manager "; + break; + default: + bar = L" F1=help ctrl-C=exit | CLEX file manager "; + } + pad = putwcs_trunc_col(bar, disp_data.scrcols,OPT_NOPAD) - len; + if (pad < 0) + char_line(' ',len + pad); + else { + BLANK(pad); + putwcs_trunc(user_data.loginw,len,OPT_NOPAD); + addch('@'); + putwcs_trunc_col(user_data.hostw,disp_data.scrcols,0); + } + attroff(attrr); +} + +void +win_edit(void) +{ + int len, width, i, last, written; + const wchar_t *str; + + /* special case: no textline */ + if (textline == 0) { + move(LNO_EDIT,0); + clrtobot(); + return; + } + + str = USTR(textline->line) + textline->offset; + len = wcslen(str); + for (i = 0; i < disp_data.cmdlines; i++) { + move(LNO_EDIT + i,0); + last = i == disp_data.cmdlines - 1; /* 1 or 0 */ + width = disp_data.scrcols - last; /* avoid writing to the bottom right corner */ + + if (i == 0) { + /* prompt */ + if (textline->offset == 0) { + if (textline->size > 0 + && ( (panel->type != PANEL_TYPE_DIR_SPLIT + && panel->type != PANEL_TYPE_DIR) || panel->norev) ) + attrset(attrb); + addwstr(USTR(textline->prompt)); + width -= textline->promptwidth; + } + else { + attrset(attrb); + addch('<'); + width--; + } + attroff(attrb); + } + + if (len == 0) { + clrtoeol(); + written = width; /* all spaces */ + } + else { + written = putwcs_trunc(str,width,last ? 0 : OPT_NOCONT); + if (written > len) + len = 0; + else { + str += written; + len -= written; + } + } + chars_in_line[i] = written; + } +} + +/* number of textline characters written to by win_edit() */ +int +sum_linechars(void) +{ + int i, sum; + + sum = chars_in_line[0]; + for (i = 1; i < disp_data.cmdlines; i++) + sum += chars_in_line[i]; + + return sum; +} + +/****** win_panel() and friends ******/ + +void +draw_line_bm(int ln) +{ + putwcs_trunc(SDSTR(panel_bm.bm[ln]->name),panel_bm.cw_name,0); + addstr(" "); + putwcs_trunc_col(USTR(panel_bm.bm[ln]->dirw),disp_data.panrcol,OPT_SQUEEZE); +} + +void +draw_line_bm_edit(int ln) +{ + wchar_t *tag, *msg; + + if (ln == 0) { + tag = L" name: "; + msg = SDSTR(panel_bm_edit.bm->name); + } else { + /* ln == 1 */ + tag = L"directory: "; + msg = USTR(panel_bm_edit.bm->dir) ? USTR(panel_bm_edit.bm->dirw) : L""; + } + + addwstr(tag); + if (*msg == L'\0') + msg = L"-- none --"; + putwcs_trunc_col(msg,disp_data.panrcol,0); +} + +void +draw_line_cfg(int ln) +{ + putstr_trunc(panel_cfg.config[ln].var,CFGVAR_LEN,0); + addstr(" = "); + putwcs_trunc_col(cfg_print_value(ln),disp_data.panrcol,0); +} + +void +draw_line_cfg_menu(int ln) +{ + putwcs_trunc(panel_cfg_menu.desc[ln],disp_data.pancols,0); +} + +void +draw_line_cmp(int ln) +{ + /* see also win_infoline() */ + static const wchar_t *description[CMP_TOTAL_ + 1] = { + L"restrict to regular files only", + L"compare file size", + L"compare file mode", + L"compare file ownership (user and group)", + L"compare file data (contents)", + L"--> Compare name, type and attributes selected above" + }; + + if (ln < CMP_TOTAL_) + CHECKBOX(COPT(ln)); + putwcs_trunc_col(description[ln],disp_data.panrcol,0); +} + +void +draw_line_cmp_sum(int ln) +{ + static const wchar_t *description[] = { + L"total number of files in panels", + L"\\_ UNIQUE FILENAMES ", + L"\\_ pairs of files compared ", + L"\\_ DIFFERING", + L"\\_ ERRORS ", /* line #4 is hidden if there are no errors */ + L"\\_ equal " + }; + wchar_t *txt, buf[64]; + int p1, p2; + FLAG marked; + + if (ln >= 4 && panel->cnt != ARRAY_SIZE(description)) + ln++; + + txt = buf; + marked = 0; + switch (ln) { + case 0: + p1 = ppanel_file->pd->cnt - panel_cmp_sum.nonreg1; + p2 = ppanel_file->other->pd->cnt - panel_cmp_sum.nonreg2; + swprintf(txt,ARRAY_SIZE(buf),L"%4d + %d%ls",p1,p2, + COPT(CMP_REGULAR) ? L" (regular files only)" : L""); + break; + case 1: + p1 = ppanel_file->pd->cnt - panel_cmp_sum.nonreg1 - panel_cmp_sum.names; + p2 = ppanel_file->other->pd->cnt - panel_cmp_sum.nonreg2 - panel_cmp_sum.names; + if ( (marked = p1 > 0 || p2 > 0) ) + swprintf(txt,ARRAY_SIZE(buf),L"%4d + %d",p1,p2); + else + txt = L" -"; + break; + case 2: + swprintf(txt,ARRAY_SIZE(buf),L" %4d",panel_cmp_sum.names); + break; + case 3: + p1 = panel_cmp_sum.names - panel_cmp_sum.equal - panel_cmp_sum.errors; + if ( (marked = p1 > 0) ) + swprintf(txt,ARRAY_SIZE(buf),L" %4d",p1); + else + txt = L" -"; + break; + case 4: + swprintf(txt,ARRAY_SIZE(buf),L" %4d",panel_cmp_sum.errors); + marked = 1; + break; + case 5: + swprintf(txt,ARRAY_SIZE(buf),L" %4d",panel_cmp_sum.equal); + break; + } + BLANK(32 - wc_cols(description[ln],0,-1)); + addwstr(description[ln]); + addstr(": "); + if (marked) + attron(attrb); + putwcs_trunc_col(txt,disp_data.panrcol,0); + if (marked) + attroff(attrb); +} + +void +draw_line_compl(int ln) +{ + COMPL_ENTRY *pcc; + + pcc = panel_compl.cand[ln]; + if (panel_compl.filenames) { + addstr(pcc->is_link ? "-> " : " " ); /* 3 */ + addstr(type_symbol[pcc->file_type]); /* 4 */ + BLANK(2); + } + else + BLANK(9); + putwcs_trunc(SDSTR(pcc->str),disp_data.pancols - 9,0); +} + +void +draw_line_dir(int ln) +{ + int shlen, width; + const wchar_t *dir; + + dir = panel_dir.dir[ln].namew; + shlen = panel_dir.dir[ln].shlen; + + if (shlen && (ln == panel_dir.pd->top || shlen >= disp_data.pancols)) + shlen = 0; + if (shlen == 0) + width = 0; + else { + width = wc_cols(dir,0,shlen); + BLANK(width - 2); + addstr("__"); + } + putwcs_trunc_col(dir + shlen,disp_data.panrcol,0); +} + +void +draw_line_dir_split(int ln) +{ + putwcs_trunc(convert2w(dir_split_dir(ln)),disp_data.pancols,0); +} + +void +draw_line_file(int ln) +{ + FILE_ENTRY *pfe; + + pfe = ppanel_file->files[ln]; + if (pfe->select && !pfe->dotdir) + attron(attrb); + + /* 10 columns reserved for the filename */ + print_fields(pfe,disp_data.pancols - 10,disp_data.layout_panel); + if (!pfe->symlink) + putwcs_trunc_col(SDSTR(pfe->filew),disp_data.panrcol,0); + else { + putwcs_trunc_col(SDSTR(pfe->filew),disp_data.panrcol,OPT_NOPAD); + putwcs_trunc_col(L" -> ",disp_data.panrcol,OPT_NOPAD); + putwcs_trunc_col(USTR(pfe->linkw),disp_data.panrcol,0); + } + + if (pfe->select) + attroff(attrb); +} + +void +draw_line_fopt(int ln) +{ + static const wchar_t *description[FOPT_TOTAL_] = { + L"substring matching: ignore the case of the characters", + L"pattern matching: wildcards match the dot in hidden .files", + L"file panel filtering: always show directories", + /* must correspond with FOPT_XXX */ + }; + + CHECKBOX(FOPT(ln)); + putwcs_trunc(description[ln],disp_data.pancols - BOX4,0); +} + +void +draw_line_group(int ln) +{ + printw("%6u ",(unsigned int)panel_group.groups[ln].gid); + putwcs_trunc_col(panel_group.groups[ln].group,disp_data.panrcol,0); +} + +void +draw_line_help(int ln) +{ + HELP_LINE *ph; + int i, active, links; + + ph = panel_help.line[ln]; + links = ph->links; + putwcs_trunc(ph->text,disp_data.pancols,links ? OPT_NOPAD : 0); + if (links == 0) + return; + + active = (ln != panel->curs || panel->filtering) ? -1 + : ln == panel_help.lnk_ln ? panel_help.lnk_act : 0; + for (i = 0; i < links; i++) { + attron(i == active ? attrr : attrb); + putwcs_trunc_col(ph[3 * i + 2].text,disp_data.panrcol,OPT_NOPAD); + attroff(i == active ? attrr : attrb); + putwcs_trunc_col(ph[3 * i + 3].text,disp_data.panrcol,i == links - 1 ? 0 : OPT_NOPAD); + } +} + +void +draw_line_hist(int ln) +{ + static const wchar_t *failstr; + static int faillen = 0; + + if (faillen == 0) { + failstr = L"failed: "; + faillen = wc_cols(failstr,0,-1); + } + if (panel_hist.hist[ln]->failed) + addwstr(failstr); + else + BLANK(faillen); + putwcs_trunc(USTR(panel_hist.hist[ln]->cmd),disp_data.pancols - faillen,0); +} + +void +draw_line_log(int ln) +{ + int i, len; + const wchar_t *msg; + FLAG warn; + + msg = USTR(panel_log.line[ln]->msg); + if ( (warn = panel_log.line[ln]->level == MSG_W) ) + attron(attrb); + if (panel_log.scroll == 0) + putwcs_trunc(msg,disp_data.pancols,0); + else { + addch('<' | attrb); + for (i = len = 0; msg[i] != L'\0' && len < panel_log.scroll; i++) + len += WCW(msg[i]); + while (utf_iscomposing(msg[i])) + i++; + putwcs_trunc(msg + i,disp_data.pancols - 1,0); + } + if (warn) + attroff(attrb); +} + +void +draw_line_mainmenu(int ln) +{ + static const wchar_t *description[] = { + L"help ", + L"change working directory alt-W", + L" change into root directory alt-/", + L" change into parent directory alt-.", + L" change into home directory alt-~ or alt-`", + L" bookmarks alt-K", + L"Bookmark the current directory ctrl-D", + L"command history alt-H", + L"sort order for filenames alt-S", + L"re-read the current directory ctrl-R", + L"compare directories alt-=", + L"filter on/off ctrl-F", + L"select files: select using pattern alt-+", + L" deselect using pattern alt--", + L" invert selection alt-*", + L"filtering and pattern matching options alt-O", + L"user (group) information alt-U (alt-G)", + L"message log alt-L", + L"notifications alt-N", + L"configure CLEX alt-C", + L"program version alt-V", + L"quit alt-Q" + /* must correspond with tab_mainmenu[] in control.c */ + }; + + putwcs_trunc(description[ln],disp_data.pancols,0); +} + +void +draw_line_notif(int ln) +{ + static const wchar_t *description[NOTIF_TOTAL_] = { + L"Warning: rm command deletes files (not 100% reliable)", + L"Warning: command line is too long to be displayed", + L"Reminder: selection marks on . and .. are not honored", + L"Reminder: selected file(s) vs. current file", + L"Notice: file with a timestamp in the future encountered" + /* must correspond with NOPT_XXX */ + }; + + CHECKBOX(!NOPT(ln)); + putwcs_trunc(description[ln],disp_data.pancols - BOX4,0); +} + +void +draw_line_paste(int ln) +{ + static const wchar_t *description[] = { + L"the name to be completed starts at the cursor position", + L"complete a name: automatic", + L" file: any type", + L" file: directory", + L" file: executable", + L" user", + L" group", + L" environment variable", + L"complete a command from the command history alt-P", + L"insert: the current filename ", + L" all selected filenames ", + L" the full pathname of current file ctrl-A", + L" the secondary working directory name ctrl-E", + L" the current working directory name ctrl-E", + L" the target of a symbolic link ctrl-O" + /* must correspond with tab_pastemenu[] in control.c */ + }; + if (ln == 0) + CHECKBOX(panel_paste.wordstart); + putwcs_trunc_col(description[ln],disp_data.panrcol,0); +} + +void +draw_line_preview(int ln) +{ + if (ln >= panel_preview.realcnt) { + attron(attrb); + putwcs_trunc(L" --- end of preview ---",disp_data.pancols,0); + attroff(attrb); + return; + } + + putwcs_trunc(USTR(panel_preview.line[ln]),disp_data.pancols,0); +} + +void +draw_line_sort(int ln) +{ + int size; + + /* see also win_infoline() */ + static const wchar_t + *description0[HIDE_TOTAL_] = { + L"show hidden .files", + L"show hidden .files, but not in the home directory", + L"do not show hidden .files" + /* must correspond with HIDE_XXX */ + }, + *description1[GROUP_TOTAL_] = { + L"do not group files by type", + L"group: directories, special files, plain files", + L"group: directories, devices, special files, plain files" + /* must correspond with GROUP_XXX */ + }, + *description2[SORT_TOTAL_] = { + L"sort by name and number", + L"sort by name", + L"sort by filename.EXTENSION", + L"sort by size [small -> large]", + L"sort by size [large -> small]", + L"sort by time of last modification [recent -> old]", + L"sort by time of last modification [old -> recent]", + L"sort by reversed name" + /* must correspond with SORT_XXX */ + }, + *description3[3] = { + L"--> save & apply globally", + L"--> apply temporarily to the current file panel's contents" + }; + + size = ARRAY_SIZE(description0); + if (ln < size) { + RADIOBUTTON(panel_sort.newhide == ln); + putwcs_trunc(description0[ln],disp_data.pancols - BOX4,0); + return; + } + if (ln == size) { + putstr_trunc("----------------",disp_data.pancols,0); + return; + } + ln -= size + 1; + + size = ARRAY_SIZE(description1); + if (ln < size) { + RADIOBUTTON(panel_sort.newgroup == ln); + putwcs_trunc(description1[ln],disp_data.pancols - BOX4,0); + return; + } + if (ln == size) { + putstr_trunc("----------------",disp_data.pancols,0); + return; + } + ln -= size + 1; + + size = ARRAY_SIZE(description2); + if (ln < size) { + RADIOBUTTON(panel_sort.neworder == ln); + putwcs_trunc(description2[ln],disp_data.pancols - BOX4,0); + return; + } + ln -= size; + + putwcs_trunc(description3[ln],disp_data.pancols,0); + return; +} + +#define MIN_GECOS 10 /* columns reserved for the gecos field */ + +void +draw_line_user(int ln) +{ + int col; + + printw("%6u ",(unsigned int)panel_user.users[ln].uid); + col = MARGIN2 + 8 /*printw above*/ + panel_user.maxlen; + if (col > disp_data.panrcol - MIN_GECOS - 1 /* 1 space */) + col = disp_data.panrcol - MIN_GECOS - 1; + putwcs_trunc_col(panel_user.users[ln].login,MARGIN2 + 8 + panel_user.maxlen,0); + addch(' '); + putwcs_trunc_col(panel_user.users[ln].gecos,disp_data.panrcol,0); +} + +static void +draw_panel_line(int curs) +{ + const wchar_t *msg; + + move(LNO_PANEL + curs - panel->top,0); + if (curs >= panel->cnt) { + clrtoeol(); + return; + } + + if (panel->curs == curs) { + addch('>'); + if (!panel->norev) + attron(attrr); + addch(' '); + } + else + addstr(" "); + if (curs < 0) { + /* extra line */ + addstr("--> "); + msg = panel->extra[curs - panel->min].text; + putwcs_trunc(msg ? msg : L"Exit this panel",disp_data.pancols - 4 /* arrow */,0); + } + else + (*panel->drawfn)(curs); + if (panel->curs == curs) { + addch(' '); + attroff(attrr); + addch('<'); + } + else + addstr(" "); +} + +static void +draw_panel(int optimize) +{ + static int save_top, save_curs, save_ptype = -1; + int curs; + + if (panel->type != save_ptype) { + /* panel type has changed */ + optimize = 0; + save_ptype = panel->type; + } + + if (optimize && save_top == panel->top) { + /* redraw only the old and new current lines */ + draw_panel_line(save_curs); + if (save_curs != panel->curs) { + posctl.update = 1; + draw_panel_line(panel->curs); + save_curs = panel->curs; + } + } + else { + posctl.update = 1; + /* redraw all lines */ + for (curs = panel->top; curs < panel->top + disp_data.panlines; curs++) + draw_panel_line(curs); + save_top = panel->top; + save_curs = panel->curs; + } + + win_infoline(); +} + +/* win_panel() without optimization */ +void +win_panel(void) +{ + draw_panel(0); +} + +/* + * win_panel() with optimization + * + * use this win_panel() version if the only change made since last + * win_panel() call is a cursor movement or a modification of the + * current line + */ +void +win_panel_opt(void) +{ + draw_panel(1); +} diff --git a/src/inout.h b/src/inout.h new file mode 100644 index 0000000..88ef1d7 --- /dev/null +++ b/src/inout.h @@ -0,0 +1,49 @@ +extern void curses_initialize(void); +extern void curses_stop(void); +extern void curses_restart(void); +extern void curses_cbreak(void); +extern void curses_raw(void); + +extern void kbd_rawkey(void); +extern wint_t kbd_input(void); + +extern const char *char_code(int); + +extern void win_frame_reconfig(void); +extern void win_settitle(const wchar_t *); +extern void win_bar(void); +extern void win_edit(void); +extern int sum_linechars(void); +extern void win_filter(void); +extern void win_frame(void); +extern void win_title(void); +extern void win_infoline(void); +extern void win_panel(void); +extern void win_panel_opt(void); +extern void win_waitmsg(void); +enum HELPMSG_TYPE { + HELPMSG_BASE, HELPMSG_OVERRIDE, HELPMSG_TMP, HELPMSG_INFO, HELPMSG_WARNING +}; +extern void win_sethelp(int, const wchar_t *); + +extern void draw_line_bm(int); +extern void draw_line_bm_edit(int); +extern void draw_line_cfg(int); +extern void draw_line_cfg_menu(int); +extern void draw_line_cmp(int); +extern void draw_line_cmp_sum(int); +extern void draw_line_compl(int); +extern void draw_line_dir(int); +extern void draw_line_dir_split(int); +extern void draw_line_file(int); +extern void draw_line_fopt(int); +extern void draw_line_group(int); +extern void draw_line_help(int); +extern void draw_line_hist(int); +extern void draw_line_log(int); +extern void draw_line_mainmenu(int); +extern void draw_line_notif(int); +extern void draw_line_paste(int); +extern void draw_line_preview(int); +extern void draw_line_sort(int); +extern void draw_line_user(int); diff --git a/src/inschar.c b/src/inschar.c new file mode 100644 index 0000000..f862d86 --- /dev/null +++ b/src/inschar.c @@ -0,0 +1,192 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ + +#include "inschar.h" + +#include "edit.h" /* edit_nu_insertchar() */ +#include "filter.h" /* filteredit_nu_insertchar() */ +#include "inout.h" /* win_filter() */ +#include "log.h" /* msgout() */ + +#define MAXCHAR 0x10FFFF /* enough for Unicode, who needs more ? */ + +/* insert literal character */ +void +cx_edit_inschar(void) +{ + win_sethelp(HELPMSG_INFO,L"NOW PRESS THE KEY TO BE INSERTED "); + kbd_rawkey(); + win_sethelp(HELPMSG_INFO,0); + + if (kinp.fkey != 0) + msgout(MSG_i,"Function key code cannot be inserted"); + else { + (panel->filtering == 1 ? filteredit_insertchar : edit_insertchar)(kinp.key); + if (kinp.key == WCH_ESC) + kinp.key = 0; /* clear the escape from Alt-X */ + } +} + +/* destination: this line or current panel's filter expression (if null) */ +static TEXTLINE *dest; + +void +inschar_initialize(void) +{ + edit_setprompt(&line_inschar,L"Insert characters: "); +} + +int +inschar_prepare(void) +{ + if (panel->filtering == 1) { + dest = 0; + panel->filtering = 2; + } + else + dest = textline; + textline = &line_inschar; + edit_nu_kill(); + return 0; +} + + +/* convert a decimal digit */ +static int +dec_value(wchar_t ch) +{ + if (ch >= L'0' && ch <= L'9') + return ch - L'0'; + return -1; +} + +/* convert a hex digit */ +static int +hex_value(wchar_t ch) +{ + if (ch >= L'0' && ch <= L'9') + return ch - L'0'; + if (ch >= L'a' && ch <= L'f') + return ch - L'a' + 10; + if (ch >= L'A' && ch <= L'F') + return ch - L'A' + 10; + return -1; +} + +/* convert ctrl-X */ +static int +ctrl_value(wchar_t ch) +{ + if (ch >= L'a' && ch <= L'z') + return ch - L'a' + 1; + if (ch >= L'A' && ch <= L'Z') + return ch - L'A' + 1; + return -1; +} + +static void +insert_dest(wchar_t ch) +{ + if (ch <= 0 || ch > MAXCHAR) { + msgout(MSG_NOTICE,"Insert character: value out of bounds"); + return; + } + if (dest) + edit_nu_insertchar(ch); + else + filteredit_nu_insertchar(ch); +} + +void +cx_ins_enter(void) +{ + int mode, value, conv; + const wchar_t *str; + wchar_t ch; + + if (dest) + textline = dest; + + mode = 0; + value = 0; + for (str = USTR(line_inschar.line); /* until break */; str++) { + ch = *str; + if (mode == 1) { + /* ^ + X = ctrl-X */ + if ((conv = ctrl_value(ch)) >= 0) + insert_dest(conv); + else { + insert_dest(L'^'); + insert_dest(ch); + } + mode = 0; + continue; + } + else if (mode == 2) { + /* decimal value */ + if ((conv = dec_value(ch)) >= 0) { + value = 10 * value + conv; + continue; + } + if (value) + insert_dest(value); + /* ch will be processed below */ + } + else if (mode == 3) { + /* hex value */ + if ((conv = hex_value(ch)) >= 0) { + value = 16 * value + conv; + continue; + } + if (value) + insert_dest(value); + /* ch will be processed below */ + } + + if (ch == L'\0') + break; + + if (ch == L'^') + mode = 1; + else if (((ch == L'0' || ch == L'\\') && str[1] == L'x') + || ((ch == L'U' || ch == L'u') && str[1] == L'+')) { + /* prefix \x or 0x or U+ or even u+ */ + str++; + mode = 3; + value = 0; + } + else if ((conv = dec_value(ch)) >= 0) { + mode = 2; + value = conv; + } + else { + mode = 0; + if (ch != L' ') + insert_dest(ch); + } + } + + if (dest) + textline = 0; + else { + panel->filtering = 1; + win_filter(); + } + + next_mode = MODE_SPECIAL_RETURN; +} diff --git a/src/inschar.h b/src/inschar.h new file mode 100644 index 0000000..9f9d5b1 --- /dev/null +++ b/src/inschar.h @@ -0,0 +1,4 @@ +extern void inschar_initialize(void); +extern int inschar_prepare(void); +extern void cx_ins_enter(void); +extern void cx_edit_inschar(void); diff --git a/src/kbd-test.1 b/src/kbd-test.1 new file mode 100644 index 0000000..a830016 --- /dev/null +++ b/src/kbd-test.1 @@ -0,0 +1,8 @@ +.TH KBD-TEST 1 +.SH "NAME" +kbd-test \- CURSES keyboard test utility +.SH "SYNOPSIS" +.B kbd-test +.SH "DESCRIPTION" +kbd-test prints the values reported by the keyboard input function from +the CURSES library. Press ctrl-C twice in a row to quit. diff --git a/src/kbd-test.c b/src/kbd-test.c new file mode 100644 index 0000000..57ae1fd --- /dev/null +++ b/src/kbd-test.c @@ -0,0 +1,114 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "../config.h" + +#include "clexheaders.h" + +#include +#include +#include +#include +#include + +#include "curses.h" + +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +int +main(void) +{ + int type, y, x, ymax, xmax, ctrlc; + const char *term; + wint_t key; + wchar_t keystr[2]; + time_t last, now; + + setlocale(LC_ALL,""); + + initscr(); /* restores signal dispositions on FreeBSD ! */ + raw(); + nonl(); + noecho(); + keypad(stdscr,TRUE); + raw(); + scrollok(stdscr,FALSE); + getmaxyx(stdscr,ymax,xmax); + + clear(); + move(0,0); + addstr("====== CURSES KEYBOARD TEST ======\n\n" + "Terminal type ($TERM) is "); + term = getenv("TERM"); + addstr(term ? term : "undefined!"); + addstr("\n\n> Press a key (ctrl-C ctrl-C to exit) <\n\n"); + refresh(); + + ctrlc = 0; + keystr[1] = L'\0'; + for (last = 0; /* until break */;) { + type = get_wch(&key); + now = time(0); + if (now >= last + 2) { + move(6,0); + clrtobot(); + } + last = now; + + if (type == OK) { + if (key != L'\x3') + ctrlc = 0; + if (iswprint(key)) { + addstr(" character: "); + if (key == L' ') + addstr("SPACE"); + else { + keystr[0] = key; + addwstr(keystr); + } + } else { + addstr(" unprintable code: "); + if (key >= L'\x1' && key <= L'\x1A') + printw("ctrl-%c",'A' + (key - L'\x1')); + else if (key == L'\x1B') + addstr("ESC"); + else + printw("\\x%X",key); + if (key == L'\x3') { + if (ctrlc) + break; + addstr(" (press again to exit)"); + ctrlc = 1; + } + } + } + else if (type == KEY_CODE_YES) { + addstr(" function key: "); + addstr(keyname(key)); + } + else + addstr(" ERROR"); + addch('\n'); + refresh(); + + getyx(stdscr,y,x); + if (y >= ymax - 2) + last = 0; + } + + clear(); + refresh(); + endwin(); + + return 0; +} diff --git a/src/lang.c b/src/lang.c new file mode 100644 index 0000000..b0707e9 --- /dev/null +++ b/src/lang.c @@ -0,0 +1,70 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* nl_langinfo() */ +#include /* setlocale() */ +#include /* log.h */ +#include /* strlen() */ + +#include "lang.h" + +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2w() */ +#include "util.h" /* ewcsdup() */ + +/* thousands separator */ +static wchar_t +sep000(void) +{ + const char *info; + + info = nl_langinfo(THOUSEP); + if (strlen(info) == 1) { + if (info[0] == '.') + return L'.'; + if (info[0] == ',') + return L','; + } + msgout(MSG_DEBUG,"LOCALE: the thousands separator is neither dot nor comma, " + "CLEX will use the opposite of the radix character"); + info = nl_langinfo(RADIXCHAR); + if (strlen(info) == 1) { + if (info[0] == '.') + return L','; + if (info[0] == ',') + return L'.'; + } + msgout(MSG_NOTICE,"LOCALE: the radix character is neither dot nor comma"); + return L'.'; +} + +void +locale_initialize(void) +{ + const char *tf, *df; + + if (setlocale(LC_ALL,"") == 0) + msgout(MSG_W,"LOCALE: cannot set locale"); + + lang_data.utf8 = strcmp(nl_langinfo(CODESET),"UTF-8") == 0; + lang_data.repl = lang_data.utf8 ? L'\xFFFD' : L'?'; + lang_data.sep000 = sep000(); + tf = nl_langinfo(T_FMT); + df = nl_langinfo(D_FMT); + lang_data.time_fmt = ewcsdup(convert2w(tf)); + lang_data.date_fmt = ewcsdup(convert2w(df)); + msgout(MSG_DEBUG,"LOCALE: standard time format: \"%s\", standard date format: \"%s\"",tf,df); +} diff --git a/src/lang.h b/src/lang.h new file mode 100644 index 0000000..a1de643 --- /dev/null +++ b/src/lang.h @@ -0,0 +1 @@ +extern void locale_initialize(void); diff --git a/src/lex.c b/src/lex.c new file mode 100644 index 0000000..f8f3e8e --- /dev/null +++ b/src/lex.c @@ -0,0 +1,239 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include "lex.h" + +#include "edit.h" /* edit_isspecial() */ + +/* + * perform a lexical analysis of the input line 'cmd' mainly for the purpose + * of name completion. Bear in mind that it is only a guess how an external shell + * will parse the command. + */ +const char * +cmd2lex(const wchar_t *cmd) +{ + static USTRING buffer = UNULL; + wchar_t ch; + char *lex; + size_t len; + int i; + FLAG sq = 0, dq = 0, bt = 0; /* 'single quote', "double quote", `backticks` */ + + len = wcslen(cmd); + us_setsize(&buffer,len + 2); + lex = USTR(buffer) + 1; + lex[-1] = LEX_BEGIN; + lex[len] = LEX_END_OK; + + for (i = 0; i < len; i++) { + ch = cmd[i]; + if (sq) { + if (ch == L'\'') { + lex[i] = LEX_QMARK; + sq = 0; + } + else + lex[i] = LEX_PLAINTEXT; + } + else if (dq) { + if (ch == L'\\' && (cmd[i + 1] == L'\"' || cmd[i + 1] == L'$')) { + lex[i++] = LEX_QMARK; + lex[i] = LEX_PLAINTEXT; + } + else if (ch == L'\"') { + lex[i] = LEX_QMARK; + dq = 0; + } + else if (ch == L'$') { + lex[i] = LEX_VAR; + if (cmd[i + 1] == L'{') + lex[++i] = LEX_VAR; + } + else + lex[i] = LEX_PLAINTEXT; + } + else + switch (ch) { + case L'\\': + lex[i++] = LEX_QMARK; + lex[i] = i == len ? LEX_END_ERR_BQ : LEX_PLAINTEXT; + break; + case L'\'': + lex[i] = LEX_QMARK; + sq = 1; + break; + case L'\"': + lex[i] = LEX_QMARK; + dq = 1; + break; + case L'~': + /* only special in a ~user construct */ + lex[i] = LEX_PLAINTEXT; + break; + case L' ': + case L'\t': + lex[i] = LEX_SPACE; + break; + case L'$': + lex[i] = LEX_VAR; + if (cmd[i + 1] == L'{') + lex[++i] = LEX_VAR; + break; + case L'>': + case L'<': + lex[i] = LEX_IO; + break; + case L'&': + case L'|': + case L';': + case L'(': + lex[i] = LEX_CMDSEP; + break; + case L'`': + lex[i] = TOGGLE(bt) ? LEX_CMDSEP /* opening */ + : LEX_OTHER /* closing */; + break; + default: + lex[i] = edit_isspecial(ch) ? LEX_OTHER : LEX_PLAINTEXT; + } + } + + /* open quote is an error */ + if (sq) + lex[len] = LEX_END_ERR_SQ; + else if (dq) + lex[len] = LEX_END_ERR_DQ; + return lex; +} + +/* is it a pattern containing wildcards ? */ +int +ispattern(const wchar_t *cmd) +{ + wchar_t ch; + const wchar_t *list = 0; /* [ ..list.. ] */ + FLAG sq = 0, dq = 0, bq = 0; + /* 'single quote', "double quote", \backslash */ + + while ((ch = *cmd++) != L'\0') { + if (ch == L']' && list && !bq + /* check for empty (invalid) lists: [] [!] and [^] */ + && (cmd - list > 2 || (cmd - list == 2 && *list != L'!' && *list != L'^'))) + /* ignoring sq and dq here because there is only backslash quoting + * inside the brackets [ ... ] */ + return 1; + if (sq) { + if (ch == L'\'') + sq = 0; + } + else if (dq) { + if (bq) + bq = 0; + else if (ch == L'\\') + bq = 1; + else if (ch == L'\"') + dq = 0; + } + else if (bq) + bq = 0; + else + switch (ch) { + case L'\\': + bq = 1; + break; + case L'\'': + sq = 1; + break; + case L'\"': + dq = 1; + break; + case L'[': + if (list == 0) + list = cmd; /* --> after the '[' */ + break; + case L'?': + case L'*': + return 1; + } + } + + return 0; +} + +/* is it quoted ? */ +int +isquoted(const wchar_t *cmd) +{ + wchar_t ch; + + while ((ch = *cmd++) != L'\0') + if (ch == L'\\' || ch == L'\'' || ch == L'\"') + return 1; + return 0; +} + +/* + * dequote quoted text 'src', max. 'len' characters + * return value: the output string length + */ +int +usw_dequote(USTRINGW *pustr, const wchar_t *src, size_t len) +{ + wchar_t ch, *dst; + size_t i, j; + FLAG bq = 0, sq = 0, dq = 0; + + /* dequoted text cannot be longer than the quoted original */ + usw_setsize(pustr,len + 1); + dst = PUSTR(pustr); + + for (i = j = 0; i < len; i++) { + ch = src[i]; + if (sq) { + if (ch == L'\'') + sq = 0; + else + dst[j++] = ch; + } + else if (dq) { + if (TCLR(bq)) { + if (ch != L'\"' && ch != L'\'' && ch != L'$' && ch != L'\n') + dst[j++] = L'\\'; + dst[j++] = ch; + } + else if (ch == L'\\') + bq = 1; + else if (ch == L'\"') + dq = 0; + else + dst[j++] = ch; + } + else if (TCLR(bq)) + dst[j++] = ch; + else if (ch == L'\\') + bq = 1; + else if (ch == L'\'') + sq = 1; + else if (ch == L'\"') + dq = 1; + else + dst[j++] = ch; + } + dst[j] = L'\0'; + + return j; +} diff --git a/src/lex.h b/src/lex.h new file mode 100644 index 0000000..0831375 --- /dev/null +++ b/src/lex.h @@ -0,0 +1,42 @@ +extern const char *cmd2lex(const wchar_t *); +extern int ispattern(const wchar_t *); +extern int isquoted(const wchar_t *); +extern int usw_dequote(USTRINGW *, const wchar_t *, size_t); + +enum LEX_TYPE { + /* 1X = space */ + LEX_SPACE = 10, /* white space */ + + /* 2X = word */ + LEX_PLAINTEXT = 20, /* text */ + LEX_QMARK, /* quotation mark */ + LEX_VAR, /* $ symbol (variable substitution) */ + + /* 3X special characters */ + LEX_IO = 30, /* > or < */ + LEX_CMDSEP, /* ; & | ( or opening backtick */ + LEX_OTHER, /* e.g. } or closing backtick */ + + /* 4X begin and end */ + LEX_BEGIN = 40, /* begin of text */ + /* end of text + exit code */ + LEX_END_OK, /* ok */ + LEX_END_ERR_BQ, /* trailing backslash */ + LEX_END_ERR_SQ, /* open single quote */ + LEX_END_ERR_DQ /* open double quote */ +}; + +/* basic categories and basic tests */ +#define LEX_TYPE_SPACE 1 +#define LEX_TYPE_WORD 2 +#define LEX_TYPE_SPECIAL 3 +#define LEX_TYPE_END 4 +#define LEX_TYPE(X) ((X) / 10) +#define IS_LEX_SPACE(X) (LEX_TYPE(X) == LEX_TYPE_SPACE) +#define IS_LEX_WORD(X) (LEX_TYPE(X) == LEX_TYPE_WORD) +#define IS_LEX_SPECIAL(X) (LEX_TYPE(X) == LEX_TYPE_SPECIAL) +#define IS_LEX_END(X) (LEX_TYPE(X) == LEX_TYPE_END) + +/* extended tests */ +#define IS_LEX_EMPTY(X) (LEX_TYPE(X) == LEX_TYPE_END || LEX_TYPE(X) == LEX_TYPE_SPACE) +#define IS_LEX_CMDSEP(X) ((X) == LEX_CMDSEP || (X) == LEX_BEGIN) diff --git a/src/list.c b/src/list.c new file mode 100644 index 0000000..6e8695b --- /dev/null +++ b/src/list.c @@ -0,0 +1,1108 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* iswprint() */ +#include /* readdir() */ +#include /* errno */ +#include /* log.h */ +#include /* sprintf() */ +#include /* strcmp() */ +#include /* time() */ +#include /* stat() */ + +/* major() */ +#ifdef MAJOR_IN_MKDEV +# include +#endif +#ifdef MAJOR_IN_SYSMACROS +# include +#endif + +#ifndef S_ISLNK +# define S_ISLNK(X) (0) +#endif + +#include "list.h" + +#include "cfg.h" /* cfg_num() */ +#include "control.h" /* err_exit() */ +#include "directory.h" /* filepos_save() */ +#include "inout.h" /* win_waitmsg() */ +#include "filter.h" /* filter_update() */ +#include "lex.h" /* ispattern() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_pattern_set() */ +#include "mbwstring.h" /* convert2w() */ +#include "sort.h" /* sort_files() */ +#include "userdata.h" /* lookup_login() */ +#include "ustringutil.h" /* us_readlink() */ +#include "util.h" /* emalloc() */ + +/* + * additional FILE_ENTRIES to be allocated when the file panel is full, + * i.e. when all existing entries in 'all_files' are occupied + */ +#define FE_ALLOC_UNIT 128 + +/* pre-formatted user/group name cache */ +/* important: gcd(CACHE_SIZE,CACHE_REPL) must be 1 */ +#define CACHE_SIZE 24 +#define CACHE_REPL 7 +static int ucache_cnt = 0, gcache_cnt = 0; + +/* layout */ +static FLAG do_gt, do_a, do_d, do_i, do_l, do_L, do_g, + do_m, do_m_blank, do_o, do_s, do_s_short, do_s_nodir; /* fields in use */ +static mode_t normal_file, normal_dir; + +/* kilobyte definition */ +static int K2, K995; /* kilobyte/2, kilobyte*9.95 */ + +/* time & date output */ +static time_t now; /* current time */ +static int now_day; /* current day of month */ +static struct tm testtm; /* tm struct for format string checks */ +static FLAG future; +static FLAG td_both; /* both time and date are displayed */ +#define TD_TIME 0 +#define TD_DATE 1 +#define TD_BOTH 2 +static const wchar_t *td_fmt[3]; /* time/date formats (man strftime, man wcsftime) */ +static int td_pad[3], td_len[3]; /* time/date padding + output string lengths */ + /* padding + length = disp_data.date_len */ +static const wchar_t *td_fmt_fail; /* failed time/date output format */ + +#define FAILSAFE_TIME L"%H:%M" +#define FAILSAFE_DATE L"%Y-%m-%d" +#define FAILSAFE_TIMEDATE L"%H:%M %Y-%m-%d" +#define FAILSAFE_DATETIME L"%Y-%m-%d %H:%M" + +/* directory listing */ +static FLAG mm_change; /* minor/major format changed during the operation */ +static FLAG use_pathname = 0; /* hack for listing a directory other than the cwd */ +static dev_t dirdev; /* filesystem device of the inspected directory */ + +static int +check_str(const wchar_t *str) +{ + wchar_t ch; + + if (str == 0) + return -1; + + while ((ch = *str++) != L'\0') + if (!ISWPRINT(ch) || wcwidth(ch) != 1) + return -1; + return 0; +} + +/* return value: output string length if it does fit, -1 otherwise */ +static int +check_format(const wchar_t *format) +{ + int len; + wchar_t tmp[80]; + + if (check_str(format) < 0) { + msgout(MSG_NOTICE,"Date/time output format \"%ls\" rejected " + "because of non-standard characters",format); + return -1; + } + + len = wcsftime(tmp,ARRAY_SIZE(tmp),format,&testtm); + if (len == 0 || len >= FE_TIME_STR) { + msgout(MSG_NOTICE,"Date/time output format \"%ls\" rejected " + "because it produces output longer than a limit of %d characters", + format,FE_TIME_STR - 1); + return -1; + } + return len; +} + +void +td_fmt_reconfig(void) +{ + int order, fmt_fail, len, diff; + const wchar_t *fmt; + static USTRINGW buff = UNULL; + + fmt_fail = 0; + + fmt = cfg_str(CFG_FMT_TIME); + if (*fmt == L'\0') + fmt = lang_data.time_fmt; + if ((len = check_format(fmt)) < 0) { + fmt_fail = 1; + fmt = FAILSAFE_TIME; + len = check_format(fmt); + } + td_fmt[TD_TIME] = fmt; + td_pad[TD_TIME] = 0; + td_len[TD_TIME] = len; + + fmt = cfg_str(CFG_FMT_DATE); + if (*fmt == L'\0') + fmt = lang_data.date_fmt; + if ((len = check_format(fmt)) < 0) { + fmt_fail = 1; + fmt = FAILSAFE_DATE; + len = check_format(fmt); + } + td_fmt[TD_DATE] = fmt; + td_pad[TD_DATE] = 0; + td_len[TD_DATE] = len; + + td_both = cfg_num(CFG_TIME_DATE) != 0; + if (td_both) { + order = cfg_num(CFG_TIME_DATE) == 1; + usw_cat(&buff, td_fmt[order ? TD_TIME : TD_DATE], L" ", + td_fmt[order ? TD_DATE : TD_TIME], (wchar_t *)0); + fmt = USTR(buff); + if ((len = check_format(fmt)) < 0) { + fmt_fail = 1; + fmt = order ? FAILSAFE_TIMEDATE : FAILSAFE_DATETIME; + len = check_format(fmt); + } + td_fmt[TD_BOTH] = fmt; + td_pad[TD_BOTH] = 0; + disp_data.date_len = td_len[TD_BOTH] = len; + } + else { + diff = td_len[TD_DATE] - td_len[TD_TIME]; + if (diff > 0) { + td_pad[TD_TIME] = diff; + disp_data.date_len = td_len[TD_DATE]; + } + else { + td_pad[TD_DATE] = -diff; + disp_data.date_len = td_len[TD_TIME]; + } + } + + if (fmt_fail) + msgout(MSG_w,"Problem with time/date output format, details in log"); +} + +void +kb_reconfig(void) +{ + if (cfg_num(CFG_KILOBYTE)) { + /* 1000 */ + K2 = 500; + K995 = 9950; + } else { + /* 1024 */ + K2 = 512; + K995 = 10189; + } +} + +/* split layout to panel fields and line fields */ +static void +split_layout(void) +{ + static USTRINGW layout = UNULL; + FLAG fld; + wchar_t ch, *pch; + + pch = disp_data.layout_panel = usw_copy(&layout,cfg_layout); + for (fld = 0; (ch = *pch); pch++) + if (!TCLR(fld)) { + if (ch == L'$') + fld = 1; + else if (ch == L'|') { + *pch = L'\0'; + disp_data.layout_line = pch + 1; + return; /* success */ + } + } + msgout(MSG_NOTICE,"CONFIG: Incomplete layout definition: \"%ls\"", + disp_data.layout_panel); + disp_data.layout_line = L""; +} + +void +layout_reconfig(void) +{ + FLAG field; + const wchar_t *layout; + wchar_t ch; + + split_layout(); + + /* which fields are going to be displayed ? */ + do_gt = do_a = do_d = do_i = do_g = do_L = do_l = 0; + do_m = do_m_blank = do_o = do_s = do_s_short = do_s_nodir = 0; + for (field = 0, layout = cfg_layout /*macro*/; (ch = *layout++); ) + if (!TCLR(field)) { + if (ch == L'$') + field = 1; + } + else { + switch (ch) { + case L'>': do_gt = 1; break; + case L'a': do_a = 1; break; + case L'd': do_d = 1; break; + case L'g': do_g = 1; break; + case L'i': do_i = 1; break; + case L'L': do_L = 1; break; + case L'l': do_l = 1; break; + case L'P': /* $P -> $M */ + case L'M': do_m_blank = 1; /* $M -> $m */ + case L'p': /* $p -> $m */ + case L'm': do_m = 1; break; + case L'o': do_o = 1; break; + /* do_s (size), do_s_short (short form), do_s_nodir (not for dirs) */ + case L'S': do_s_nodir = 1; + case L's': do_s = 1; break; + case L'R': do_s_nodir = 1; + case L'r': do_s_short = do_s = 1; break; + /* default: ignore unknown formatting character */ + } + } +} + +void +list_initialize(void) +{ + time_t sometime = 1234567890; /* an arbitrary value */ + + testtm = *gmtime(&sometime); /* struct copy */ + + normal_dir = 0777 & ~clex_data.umask; /* dir or executable file */ + normal_file = 0666 & ~clex_data.umask; /* any other file */ + + kb_reconfig(); + layout_reconfig(); + td_fmt_reconfig(); +} + +/* + * when calling any of the stat2xxx() functions always make sure + * the string provided as 'str' argument can hold the result. + * Check the FE_XXX_STR #defines in clex.h for proper sizes. + */ + +static void +stat2time(wchar_t *str, time_t tm) +{ + int i, td, len; + + if (td_both) + td = TD_BOTH; + else if (tm <= now) { + if (tm < now - 86400 /* more than 1 day ago */) + td = TD_DATE; + else if (tm > now - 57600 /* less than 16 hours ago */) + td = TD_TIME; + else + td = localtime(&tm)->tm_mday != now_day ? TD_DATE: TD_TIME; + } + else { + if (tm > now + 300 /* 5 minutes tolerance */) + future = 1; + td = (tm > now + 86400 || localtime(&tm)->tm_mday != now_day) ? TD_DATE: TD_TIME; + } + + for (i = 0; i < td_pad[td]; i++) + *str++ = L' '; + len = wcsftime(str,FE_TIME_STR - td_pad[td],td_fmt[td],localtime(&tm)); + if (len == td_len[td]) + return; + + /* problem with the format: output length is not constant */ + if (len > 0 && len < td_len[td]) { + /* small problem: the string is shorter */ + for (i = len; i < td_len[td]; i++) + str[i] = L' '; + str[i] = L'\0'; + } + else { + /* big problem: the string does not fit */ + td_fmt_fail = td_fmt[td]; + for (i = 0; i < td_len[td]; i++) + str[i] = L'-'; + str[i] = L'\0'; + } +} + +static void +stat2age(char *str, time_t tm) +{ + time_t age; + int h, m, s; + + age = now - tm; + + if (age < 0) { + strcpy(str," future!"); + return; + } + if (age >= 360000 /* 100 hours */) { + strcpy(str," "); + return; + } + + h = age / 3600; + age -= h * 3600; + m = age / 60; + s = age - m * 60; + if (h) + sprintf(str,"%3d:%02d:%02d",-h,m,s); + else if (m) + sprintf(str," %3d:%02d",-m,s); + else if (s) + sprintf(str," %3d",-s); + else + strcpy(str," -0"); +} + +static void +stat2size_7(char *str, off_t size) +{ + int exp, roundup; + + for (exp = roundup = 0; size + roundup > 9999999 /* 9.999.999 */; exp++) { + /* size = size / 1K */ + size /= K2; + roundup = size % 2; + size /= 2; + } + + if (K2 != 512) + *str++ = ' '; + sprintf(str," %7ld%c",(long int)(size + roundup)," KMGTPEZY"[exp]); + /* insert thousands separators: 1.234.567 */ + if (str[5] != ' ') { + if (str[2] != ' ') { + str[0] = str[2]; + str[1] = lang_data.sep000; + } + str[2] = str[3]; + str[3] = str[4]; + str[4] = str[5]; + str[5] = lang_data.sep000; + } + if (K2 == 512) + str[10] = exp ? 'i' : ' '; +} + +static void +stat2size_3(char *str, off_t size) +{ + int exp, roundup; + FLAG dp; /* decimal point */ + + for (exp = roundup = 0, dp = 0; size + roundup > 999; exp++) { + if ( (dp = size < K995) ) + size *= 10; + size /= K2; + roundup = size % 2; + size /= 2; + } + + if (K2 != 512) + *str++ = ' '; + sprintf(str," %3d%c",(int)(size + roundup)," KMGTPEZY"[exp]); + if (dp) { + str[6] = str[7]; + str[7] = lang_data.sep000; + } + if (K2 == 512) + str[10] = exp ? 'i' : ' '; +} + +/* + * stat2dev() prints device major:minor numbers + * + * FE_SIZE_DEV_STR is 12 ==> total number of digits is 10, + * from these 10 digits are 2 to 7 used for minor device + * number (printed in hex) and the rest is used for major + * device number (printed in dec) + * + * some major:minor splits + * 8 : 8 bits - Linux 2.4 + * 14 : 18 bits - SunOS 5 + * 8 : 24 bits - FreeBSD + * 12 : 20 bits - Linux 2.6 + */ + +#define MIN_MINOR_DIGITS 2 +#define MAX_MINOR_DIGITS 7 + +static void +stat2dev(char *str, unsigned int dev_major, unsigned int dev_minor) +{ + static unsigned int digits_minor[] = { + 0, + 0xF, + 0xFF, /* 2 digits, 8 bits */ + 0xFFF, /* 3 digits, 12 bits */ + 0xFFFF, /* 4 digits, 16 bits */ + 0xFFFFF, /* 5 digits, 20 bits */ + 0xFFFFFF, /* 6 digits, 24 bits */ + 0xFFFFFFF,/* 7 digits, 28 bits */ + 0xFFFFFFFF + }; + static unsigned int digits_major[] = { + 0, + 9, + 99, + 999, /* 3 digits, 9 bits */ + 9999, /* 4 digits, 13 bits */ + 99999, /* 5 digits, 16 bits */ + 999999, /* 6 digits, 19 bits */ + 9999999, /* 7 digits, 23 bits */ + 99999999, /* 8 digits, 26 bits */ + 999999999 + }; + static int minor_len = MIN_MINOR_DIGITS; + static int major_len = FE_SIZE_DEV_STR - MIN_MINOR_DIGITS - 2; + int minor_of; /* overflow */ + + /* determine the major digits / minor digits split */ + while ( (minor_of = dev_minor > digits_minor[minor_len]) + && minor_len < MAX_MINOR_DIGITS) { + minor_len++; + major_len--; + mm_change = 1; + } + + /* print major */ + if (dev_major > digits_major[major_len]) + sprintf(str,"%*s",major_len,".."); + else + sprintf(str,"%*d",major_len,dev_major); + + /* print minor */ + if (minor_of) + sprintf(str + major_len,":..%0*X", + minor_len - 2,dev_minor & digits_minor[minor_len - 2]); + else + sprintf(str + major_len,":%0*X",minor_len,dev_minor); +} + +int +stat2type(mode_t mode, uid_t uid) +{ + if (S_ISREG(mode)) { + if ( (mode & S_IXUSR) != S_IXUSR + && (mode & S_IXGRP) != S_IXGRP + && (mode & S_IXOTH) != S_IXOTH) + return FT_PLAIN_FILE; + if ((mode & S_ISUID) == S_ISUID) + return uid ? FT_PLAIN_SUID : FT_PLAIN_SUID_ROOT; + if ((mode & S_ISGID) == S_ISGID) + return FT_PLAIN_SGID; + return FT_PLAIN_EXEC; + } + if (S_ISDIR(mode)) + return FT_DIRECTORY; + if (S_ISBLK(mode)) + return FT_DEV_BLOCK; + if (S_ISCHR(mode)) + return FT_DEV_CHAR; + if (S_ISFIFO(mode)) + return FT_FIFO; +#ifdef S_ISSOCK + if (S_ISSOCK(mode)) + return FT_SOCKET; +#endif + return FT_OTHER; +} + +/* format for the file panel */ +static void +id2name(wchar_t *dst, int leftalign, const wchar_t *name, unsigned int id) +{ + int i, len, pad; + wchar_t number[16]; + + if (check_str(name) < 0) { + swprintf(number,ARRAY_SIZE(number),L"%u",id); + name = number; + } + + len = wcslen(name); + pad = FE_NAME_STR - 1 - len; + if (pad >= 0) { + if (!leftalign) + for (i = 0 ; i < pad; i++) + *dst++ = L' '; + wcscpy(dst,name); + if (leftalign) { + dst += len; + for (i = 0 ; i < pad; i++) + *dst++ = L' '; + *dst = L'\0'; + } + } + else { + for (i = 0 ; i < (FE_NAME_STR - 1) / 2; i++) + *dst++ = name[i]; + *dst++ = L'>'; + for (i = len - FE_NAME_STR / 2 + 1; i <= len ;i++) + *dst++ = name[i]; + } +} + +static const wchar_t * +uid2name(uid_t uid) +{ + static int pos = 0, replace = 0; + static struct { + uid_t uid; + wchar_t name[FE_NAME_STR]; + } cache[CACHE_SIZE]; + + if (pos < ucache_cnt && uid == cache[pos].uid) + return cache[pos].name; + + for (pos = 0; pos < ucache_cnt; pos++) + if (uid == cache[pos].uid) + return cache[pos].name; + + if (ucache_cnt < CACHE_SIZE) + pos = ucache_cnt++; + else + pos = replace = (replace + CACHE_REPL) % CACHE_SIZE; + cache[pos].uid = uid; + id2name(cache[pos].name,0,lookup_login(uid),(unsigned int)uid); + + return cache[pos].name; +} + +static const wchar_t * +gid2name(gid_t gid) +{ + static int pos = 0, replace = 0; + static struct { + gid_t gid; + wchar_t name[FE_NAME_STR]; + } cache[CACHE_SIZE]; + + if (pos < gcache_cnt && gid == cache[pos].gid) + return cache[pos].name; + + for (pos = 0; pos < gcache_cnt; pos++) + if (gid == cache[pos].gid) + return cache[pos].name; + + if (gcache_cnt < CACHE_SIZE) + pos = gcache_cnt++; + else + pos = replace = (replace + CACHE_REPL) % CACHE_SIZE; + cache[pos].gid = gid; + id2name(cache[pos].name,1,lookup_group(gid),(unsigned int)gid); + + return cache[pos].name; +} + +static void +stat2owner(wchar_t *str, uid_t uid, gid_t gid) +{ + wcscpy(str,uid2name(uid)); + str[FE_NAME_STR - 1] = L':'; + wcscpy(str + FE_NAME_STR,gid2name(gid)); +} + +static void +stat2links(char *str, nlink_t nlink) +{ + if (nlink <= 999) + sprintf(str,"%3d",(int)nlink); + else + strcpy(str,"max"); +} + +/* + * get the extension "ext" from "file.ext" + * an exception: ".file" is a hidden file without an extension + */ +static const char * +get_ext(const char *filename) +{ + const char *ext; + char ch; + + if (*filename++ == '\0') + return ""; + + for (ext = ""; (ch = *filename); filename++) + if (ch == '.') + ext = filename + 1; + return ext; +} + +/* this file does exist, but no other information is available */ +static void +nofileinfo(FILE_ENTRY *pfe) +{ + pfe->mtime = 0; + pfe->size = 0; + pfe->extension = get_ext(SDSTR(pfe->file)); + pfe->file_type = FT_NA; + pfe->size_str[0] = '\0'; + pfe->atime_str[0] = L'\0'; + pfe->mtime_str[0] = L'\0'; + pfe->ctime_str[0] = L'\0'; + pfe->links_str[0] = '\0'; + pfe->age_str[0] = '\0'; + pfe->links = 0; + pfe->mode_str[0] = '\0'; + pfe->normal_mode = 1; + pfe->owner_str[0] = L'\0'; +} + +/* fill-in all required information about a file */ +static void +fileinfo(FILE_ENTRY *pfe, struct stat *pst) +{ + pfe->mtime = pst->st_mtime; + pfe->size = pst->st_size; + pfe->extension = get_ext(SDSTR(pfe->file)); + pfe->file_type = stat2type(pst->st_mode,pst->st_uid); + if (IS_FT_DEV(pfe->file_type)) +#ifdef HAVE_STRUCT_STAT_ST_RDEV + pfe->devnum = pst->st_rdev; +#else + pfe->devnum = 0; +#endif + /* special case: active mounting point */ + if (pfe->file_type == FT_DIRECTORY && !pfe->symlink + && !pfe->dotdir && pst->st_dev != dirdev) + pfe->file_type = FT_DIRECTORY_MNT; + + if (do_a) + stat2time(pfe->atime_str,pst->st_atime); + if (do_d) + stat2time(pfe->mtime_str,pst->st_mtime); + if (do_g) + stat2age(pfe->age_str,pst->st_mtime); + if (do_i) + stat2time(pfe->ctime_str,pst->st_ctime); + if (do_l) + stat2links(pfe->links_str,pst->st_nlink); + if (do_L) + pfe->links = pst->st_nlink > 1 && !IS_FT_DIR(pfe->file_type); + pfe->mode12 = pst->st_mode & 07777; + if (do_m) { + sprintf(pfe->mode_str,"%04o",pfe->mode12); + if (do_m_blank) { + if (S_ISREG(pst->st_mode)) + pfe->normal_mode = pfe->mode12 == normal_file + || pfe->mode12 == normal_dir /* same as exec */; + else if (S_ISDIR(pst->st_mode)) + pfe->normal_mode = pfe->mode12 == normal_dir; + else + pfe->normal_mode = pfe->mode12 == normal_file; + } + } + pfe->uid = pst->st_uid; + pfe->gid = pst->st_gid; + if (do_o) + stat2owner(pfe->owner_str,pst->st_uid,pst->st_gid); + if (do_s) { + if (IS_FT_DEV(pfe->file_type)) + stat2dev(pfe->size_str,major(pfe->devnum),minor(pfe->devnum)); + else if (do_s_nodir && IS_FT_DIR(pfe->file_type)) + /* stat2size_blank() - blank x FE_SIZE_DEV_STR */ + strcpy(pfe->size_str," "); + else + (do_s_short ? stat2size_3 : stat2size_7) + (pfe->size_str,pst->st_size); + } +} + +/* set column widths */ +static void +set_cw(void) +{ + int i, mod, lns, lnh, ln1, sz1, ow1, sz2, ow2, age; + FILE_ENTRY *pfe; + + mod = do_m_blank; + lns = do_gt; + lnh = do_L; + age = FE_AGE_STR - 2; + ln1 = FE_LINKS_STR - 3; + sz1 = FE_SIZE_DEV_STR - 4; + ow1 = FE_NAME_STR - 3; + sz2 = FE_SIZE_DEV_STR - 3; + ow2 = FE_NAME_STR + 1; + + for (i = 0; i < ppanel_file->all_cnt; i++) { + pfe = ppanel_file->all_files[i]; + if (mod && !pfe->normal_mode) + mod = 0; + if (lns && pfe->symlink) + lns = 0; + if (lnh && pfe->links) + lnh = 0; + if (do_g && *pfe->age_str) + while (age >= 0 && pfe->age_str[age] != ' ') + age--; + if (do_l && *pfe->links_str) + while (ln1 >= 0 && pfe->links_str[ln1] != ' ') + ln1--; + if (do_s && *pfe->size_str) { + while (sz1 >= 0 && pfe->size_str[sz1] != ' ') + sz1--; + while (sz2 < FE_SIZE_DEV_STR - 1 && pfe->size_str[sz2] != ' ') + sz2++; + } + if (do_o && *pfe->owner_str) { + while (ow1 >= 0 && pfe->owner_str[ow1] != L' ') + ow1--; + while (ow2 < FE_OWNER_STR - 1 && pfe->owner_str[ow2] != L' ') + ow2++; + } + } + + if (sz2 < FE_SIZE_DEV_STR - 1 || ow2 < FE_OWNER_STR - 1) + for (i = 0; i < ppanel_file->all_cnt; i++) { + pfe = ppanel_file->all_files[i]; + /* one of these assignments might be superfluous */ + pfe->size_str[sz2] = '\0'; + pfe->owner_str[ow2] = '\0'; + } + + ppanel_file->cw_mod = mod ? 0 : FE_MODE_STR - 1; + ppanel_file->cw_lns = lns ? 0 : 2; /* strlen("->") */ + ppanel_file->cw_lnh = lnh ? 0 : 3; /* strlen("LNK") */ + ppanel_file->cw_ln1 = ln1 + 1; + ppanel_file->cw_sz1 = sz1 + 1; + ppanel_file->cw_ow1 = ow1 + 1; + ppanel_file->cw_age = age + 1; + ppanel_file->cw_sz2 = sz2 - sz1 - 1; + ppanel_file->cw_ow2 = ow2 - ow1 - 1; +} + +/* build the FILE_ENTRY '*pfe' describing the file named 'name' */ +static int +describe_file(const char *name, FILE_ENTRY *pfe) +{ + struct stat stdata; + + if (use_pathname) + name = pathname_join(name); + + if (lstat(name,&stdata) < 0) { + if (errno == ENOENT) + return -1; /* file deleted in the meantime */ + pfe->symlink = 0; + nofileinfo(pfe); + return 0; + } + + if ( (pfe->symlink = S_ISLNK(stdata.st_mode)) ) { + if (us_readlink(&pfe->link,name) < 0) { + us_copy(&pfe->link,"??"); + usw_copy(&pfe->linkw,L"??"); + } + else + usw_convert2w(USTR(pfe->link),&pfe->linkw); + /* need stat() instead of lstat() */ + if (stat(name,&stdata) < 0) { + nofileinfo(pfe); + return 0; + } + } + + fileinfo(pfe,&stdata); + return 0; +} + +#define DOT_NONE 0 /* not a .file */ +#define DOT_DIR 1 /* dot directory */ +#define DOT_DOT_DIR 2 /* dot-dot directory */ +#define DOT_HIDDEN 3 /* hidden .file */ + +static int +dotfile(const char *name) +{ + if (name[0] != '.') + return DOT_NONE; + if (name[1] == '\0') + return DOT_DIR; + if (name[1] == '.' && name[2] == '\0') + return DOT_DOT_DIR; + return DOT_HIDDEN; +} + +static void +directory_read(void) +{ + int i, cnt1, cnt2; + DIR *dd; + FILE_ENTRY *pfe; + FLAG hide; + struct stat st; + struct dirent *direntry; + const char *name; + + name = USTR(ppanel_file->dir); + if (stat(name,&st) < 0 || (dd = opendir(name)) == 0) { + ppanel_file->all_cnt = ppanel_file->pd->cnt = 0; + ppanel_file->selected = ppanel_file->selected_out = 0; + msgout(MSG_w,"FILE LIST: cannot list the contents of the directory"); + return; + } + dirdev = st.st_dev; + + win_waitmsg(); + mm_change = future = 0; + td_fmt_fail = 0; + ppanel_file->hidden = 0; + hide = ppanel_file->hide == HIDE_ALWAYS + || (ppanel_file->hide == HIDE_HOME + && strcmp(USTR(ppanel_file->dir),user_data.homedir) == 0); + + /* + * step #1: process selected files already listed in the panel + * in order not to lose their selection mark + */ + cnt1 = 0; + ppanel_file->selected += ppanel_file->selected_out; + ppanel_file->selected_out = 0; + for (i = 0; cnt1 < ppanel_file->selected; i++) { + pfe = ppanel_file->all_files[i]; + if (!pfe->select) + continue; + name = SDSTR(pfe->file); + if ((hide && dotfile(name) == DOT_HIDDEN) || describe_file(name, pfe) < 0) + /* this entry is no more valid */ + ppanel_file->selected--; + else { + /* OK, move it to the end of list we have so far */ + /* by swapping pointers: [cnt1] <--> [i] */ + ppanel_file->all_files[i] = ppanel_file->all_files[cnt1]; + ppanel_file->all_files[cnt1] = pfe; + cnt1++; + } + } + + /* step #2: add data about new files */ + cnt2 = cnt1; + while ( (direntry = readdir(dd)) ) { + name = direntry->d_name; + if (hide && dotfile(name) == DOT_HIDDEN) { + ppanel_file->hidden = 1; + continue; + } + + /* didn't we process this file already in step #1 ? */ + if (cnt1) { + for (i = 0; i < cnt1; i++) + if (strcmp(SDSTR(ppanel_file->all_files[i]->file),name) == 0) + break; + if (i < cnt1) + continue; + } + + /* allocate new bunch of FILE_ENTRies if needed */ + if (cnt2 == ppanel_file->all_alloc) { + ppanel_file->all_alloc += FE_ALLOC_UNIT; + ppanel_file->all_files = erealloc(ppanel_file->all_files, + ppanel_file->all_alloc * sizeof(FILE_ENTRY *)); + pfe = emalloc(FE_ALLOC_UNIT * sizeof(FILE_ENTRY)); + for (i = 0; i < FE_ALLOC_UNIT; i++) { + SD_INIT(pfe[i].file); + SD_INIT(pfe[i].filew); + US_INIT(pfe[i].link); + US_INIT(pfe[i].linkw); + ppanel_file->all_files[cnt2 + i] = pfe + i; + } + } + + pfe = ppanel_file->all_files[cnt2]; + sd_copy(&pfe->file,name); + sdw_copy(&pfe->filew,convert2w(name)); + pfe->dotdir = dotfile(name); + if (pfe->dotdir == DOT_HIDDEN) + pfe->dotdir = DOT_NONE; + if (describe_file(name, pfe) < 0) + continue; + pfe->select = 0; + cnt2++; + } + ppanel_file->all_cnt = cnt2; + + closedir(dd); + + if (mm_change) + for (i = 0; i < cnt2; i++) { + pfe = ppanel_file->all_files[i]; + if (IS_FT_DEV(pfe->file_type)) + stat2dev(pfe->size_str,major(pfe->devnum),minor(pfe->devnum)); + } + + if (td_fmt_fail) { + msgout(MSG_NOTICE,"Time/date format \"%ls\" produces output of variable length, " + "check the configuration",td_fmt_fail); + msgout(MSG_w,"Problem with date/time output format, details in log"); + } + if (future && !NOPT(NOTIF_FUTURE)) + msgout(MSG_i | MSG_NOTIFY,"FILE LIST: timestamp in the future encountered"); + + set_cw(); +} + +/* invalidate file panel contents after a directory change */ +void +filepanel_reset(void) +{ + ppanel_file->all_cnt = ppanel_file->pd->cnt = 0; + ppanel_file->selected = ppanel_file->selected_out = 0; + ppanel_file->order = panel_sort.order; + ppanel_file->group = panel_sort.group; + ppanel_file->hide = panel_sort.hide; +} + +void +file_panel_data(void) +{ + const wchar_t *filter; + static USTRINGW dequote = UNULL; + FILE_ENTRY *pfe, *curs; + int i, j, selected_in, selected_out; + FLAG type; /* type 0 = substring, type 1 = pattern */ + + if (ppanel_file->all_cnt == 0) { + /* panel is empty */ + ppanel_file->pd->cnt = 0; + return; + } + + if (!ppanel_file->pd->filtering || ppanel_file->pd->filter->size == 0) { + /* panel is not filtered */ + ppanel_file->pd->cnt = ppanel_file->all_cnt; + ppanel_file->selected += ppanel_file->selected_out; + ppanel_file->selected_out = 0; + if (ppanel_file->files != ppanel_file->all_files) { + if (ppanel_file->files == 0 /* start-up */ || !VALID_CURSOR(ppanel_file->pd)) { + ppanel_file->files = ppanel_file->all_files; + return; + } + curs = ppanel_file->files[ppanel_file->pd->curs]; + ppanel_file->files = ppanel_file->all_files; + for (i = 0; i < ppanel_file->all_cnt; i++) + if (ppanel_file->all_files[i] == curs) { + ppanel_file->pd->curs = i; + break; + } + } + return; + } + + /* panel is filtered */ + if (ppanel_file->filt_alloc < ppanel_file->all_cnt) { + efree(ppanel_file->filt_files); + ppanel_file->filt_files + = emalloc((ppanel_file->filt_alloc = ppanel_file->all_cnt) * sizeof(FILE_ENTRY *)); + } + + filter = ppanel_file->pd->filter->line; + type = ispattern(filter); + if (ppanel_file->filtype != type) { + ppanel_file->filtype = type; + win_filter(); + } + if (type) + match_pattern_set(filter); + else { + if (isquoted(filter)) { + usw_dequote(&dequote,filter,wcslen(filter)); + filter = USTR(dequote); + } + match_substr_set(filter); + } + + curs = VALID_CURSOR(ppanel_file->pd) ? ppanel_file->files[ppanel_file->pd->curs] : 0; + for (i = j = selected_in = selected_out = 0; i < ppanel_file->all_cnt; i++) { + pfe = ppanel_file->all_files[i]; + if (pfe == curs) + ppanel_file->pd->curs = j; + if ((FOPT(FOPT_SHOWDIR) && IS_FT_DIR(pfe->file_type)) + || (type ? match_pattern(SDSTR(pfe->file)) : match_substr(SDSTR(pfe->filew))) + || (pfe->symlink && + (type ? match_pattern(USTR(pfe->link)) : match_substr(USTR(pfe->linkw))))) { + ppanel_file->filt_files[j++] = pfe; + if (pfe->select) + selected_in++; + } + else if (pfe->select) + selected_out++; /* selected, but filtered out */ + } + ppanel_file->pd->cnt = j; + ppanel_file->selected = selected_in; + ppanel_file->selected_out = selected_out; + ppanel_file->files = ppanel_file->filt_files; +} + +static void +filepanel_read(void) +{ + filepos_save(); + directory_read(); + sort_files(); + /* sort_files() calls file_panel_data() */ + filepos_set(); + ppanel_file->timestamp = now; + ppanel_file->expired = 0; +} + +int +list_directory_cond(int expiration_time) +{ + now = time(0); + + /* password data change invalidates data in both panels */ + if (userdata_refresh()) { + ppanel_file->other->expired = 1; + ucache_cnt = gcache_cnt = 0; + } + else if (expiration_time && now < ppanel_file->timestamp + expiration_time) + return -1; + + now_day = localtime(&now)->tm_mday; + filepanel_read(); + return 0; +} + +void +list_directory(void) +{ + list_directory_cond(0); +} + +void +list_both_directories(void) +{ + list_directory(); + + /* + * warning: during the re-reading of the secondary panel it becomes the primary panel, + * but it does not correspond with the current working directory + */ + ppanel_file = ppanel_file->other; + pathname_set_directory(USTR(ppanel_file->dir)); + use_pathname = 1; /* must prepend directory name */ + filepanel_read(); + use_pathname = 0; + ppanel_file = ppanel_file->other; +} diff --git a/src/list.h b/src/list.h new file mode 100644 index 0000000..f0ae787 --- /dev/null +++ b/src/list.h @@ -0,0 +1,10 @@ +extern void kb_reconfig(void); +extern void layout_reconfig(void); +extern void td_fmt_reconfig(void); +extern void list_initialize(void); +extern void list_directory(void); +extern int list_directory_cond(int); +extern void list_both_directories(void); +extern void filepanel_reset(void); +extern void file_panel_data(void); +extern int stat2type(mode_t, uid_t); diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..cad56a3 --- /dev/null +++ b/src/log.c @@ -0,0 +1,312 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* errno */ +#include /* va_list */ +#include /* fprintf() */ +#include /* strerror() */ +#include /* strftime() */ + +#include "log.h" + +#include "control.h" /* err_exit() */ +#include "inout.h" /* win_sethelp() */ +#include "match.h" /* match_substr() */ +#include "mbwstring.h" /* usw_convert2w() */ +#include "panel.h" /* panel_adjust() */ +#include "ustringutil.h" /* us_vprintf() */ + +static FILE *logfp = 0; +static LOG_ENTRY logbook[LOG_LINES]; /* circular buffer */ +static int base = 0, cnt = 0; + +static int +find_nl(const char *str) +{ + const char *nl; + + nl = strchr(str,'\n'); + return nl ? nl - str : -1; +} + +static const char * +strip_nl(const char *str) +{ + static USTRING nonl = UNULL; + char *p; + int nl; + + if ((nl = find_nl(str)) < 0) + return str; + + p = us_copy(&nonl,str); + do { + p += nl; + *p++ = ' '; + nl = find_nl(p); + } while (nl >= 0); + return USTR(nonl); +} + +static void +append_record(const char *timestamp, const char *levelstr, const char *msg) +{ + fprintf(logfp,"%s %-15s %s\n",timestamp,levelstr,msg); + fflush(logfp); +} + +void +logfile_open(const char *logfile) +{ + int i; + LOG_ENTRY *plog; + + if ( (logfp = fopen(logfile,"a")) == 0) + msgout(MSG_W,"Could not open the logfile \"%s\" (%s)",logfile,strerror(errno)); + else { + /* write records collected so far */ + for (i = 0; i < cnt; i++) { + plog = logbook + (base + i) % LOG_LINES; + append_record(plog->timestamp,plog->levelstr,convert2mb(USTR(plog->msg))); + } + msgout(MSG_DEBUG,"Logfile: \"%s\"",logfile); + } +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +logfile_close(void) +{ + if (logfp) { + fclose(logfp); + logfp = 0; + } +} + +static void +store_timestamp(char *dst) +{ + static FLAG format_ok = 1; + time_t now; + + now = time(0); + if (format_ok && strftime(dst,TIMESTAMP_STR,"%c",localtime(&now)) == 0) { + format_ok = 0; + msgout(MSG_NOTICE,"LOG: Using YYYY-MM-DD HH:MM:SS date/time format " + "because the default format (defined by locale) is too long"); + } + if (!format_ok) + strftime(dst,TIMESTAMP_STR,"%Y-%m-%d %H:%M:%S",localtime(&now)); +} + +static void +log_record(int level, const char *logmsg) +{ + static struct { + const char *str; /* should not exceed 15 chars */ + FLAG panel; /* insert into panel + append to the logfile */ + FLAG screen; /* display on the screen */ + FLAG iswarning; /* is a warning */ + } levdef [_MSG_TOTAL_] = { + { /* heading */ 0,0,0,0 }, + { "DEBUG", 1, 0, 0 }, + { "NOTICE", 1, 0, 1 }, + { "AUDIT", 1, 0, 0 }, + { "INFO", 1, 1, 0 }, + { "INFO", 0, 1, 0 }, + { "WARNING", 1, 1, 1 }, + { "WARNING", 0, 1, 1 } + }; + static USTRING heading_buff = UNULL; + static int notify_hint = 2; /* display the hint twice */ + USTRINGW wmsg_buff; + const wchar_t *wmsg; + const char *msg, *heading; + LOG_ENTRY *plog; + FLAG notify = 0; + int i; + + /* modifiers */ + if ((level & MSG_NOTIFY) && notify_hint) + notify = 1; + level &= MSG_MASK; + + if (level < 0 || level >= _MSG_TOTAL_) + err_exit("BUG: invalid message priority level %d",level); + + if (level == MSG_HEADING) { + /* if 'msg' is a null ptr, the stored heading is invalidated */ + us_copy(&heading_buff,logmsg); + return; + } + if (levdef[level].iswarning && (heading = USTR(heading_buff)) && logmsg != heading) { + /* emit the heading string first */ + log_record(level,heading); + us_reset(&heading_buff); + } + + msg = strip_nl(logmsg); + US_INIT(wmsg_buff); + wmsg = usw_convert2w(msg,&wmsg_buff); + + /* append it to the log panel and log file */ + if (levdef[level].panel) { + if (cnt < LOG_LINES) + /* adding new record */ + plog = logbook + (base + cnt++) % LOG_LINES; + else { + /* replacing the oldest one */ + plog = logbook + base; + base = (base + 1) % LOG_LINES; + if (plog->cols == panel_log.maxcols) + /* replacing the longest message -> must recalculate the max */ + panel_log.maxcols = 0; + } + + plog->level = level; + plog->levelstr = levdef[level].str; + usw_copy(&plog->msg,wmsg); + store_timestamp(plog->timestamp); + plog->cols = wc_cols(wmsg,0,-1); + + /* max length */ + if (cnt < LOG_LINES || panel_log.maxcols > 0) { + if (plog->cols > panel_log.maxcols) + panel_log.maxcols = plog->cols; + } + else + for (i = 0; i < LOG_LINES; i++) + if (logbook[i].cols > panel_log.maxcols) + panel_log.maxcols = logbook[i].cols; + + /* live view */ + if (get_current_mode() == MODE_LOG) { + log_panel_data(); + panel->curs = panel->cnt - 1; + pan_adjust(panel); + win_panel(); + } + + if (logfp) + append_record(plog->timestamp,plog->levelstr,msg); + } + + /* display it on the screen */ + if (levdef[level].screen) { + if (disp_data.curses) + win_sethelp(levdef[level].iswarning ? HELPMSG_WARNING : HELPMSG_INFO,wmsg); + else { + puts(logmsg); /* original message possibly with newlines */ + fflush(stdout); + disp_data.wait = 1; + } + if (notify) { + notify_hint--; /* display this hint only N times */ + win_sethelp(HELPMSG_TMP,L"alt-N = notification panel"); + } + } +} + +void +vmsgout(int level, const char *format, va_list argptr) +{ + static USTRING buff = UNULL; + + us_vprintf(&buff,format,argptr); + log_record(level,USTR(buff)); +} + +void +msgout(int level, const char *format, ...) +{ + va_list argptr; + + if (format && strchr(format,'%')) { + va_start(argptr,format); + vmsgout(level,format,argptr); + va_end(argptr); + } + else + log_record(level,format); +} + +void +log_panel_data(void) +{ + int i, j; + LOG_ENTRY *plog, *curs; + + curs = VALID_CURSOR(panel_log.pd) ? panel_log.line[panel_log.pd->curs] : 0; + if (panel_log.pd->filtering) + match_substr_set(panel_log.pd->filter->line); + + for (i = j = 0; i < cnt; i++) { + plog = logbook + (base + i) % LOG_LINES; + if (plog == curs) + panel_log.pd->curs = j; + if (panel_log.pd->filtering && !match_substr(USTR(plog->msg))) + continue; + panel_log.line[j++] = plog; + } + panel_log.pd->cnt = j; +} + +int +log_prepare(void) +{ + panel_log.pd->filtering = 0; + panel_log.pd->curs = -1; + log_panel_data(); + panel_log.pd->top = panel_user.pd->min; + panel_log.pd->curs = panel_log.pd->cnt - 1; + + panel = panel_log.pd; + textline = 0; + return 0; +} + +#define SCROLL_UNIT 12 + +void +cx_log_right(void) +{ + if (panel_log.scroll < panel_log.maxcols - disp_data.scrcols / 2) { + panel_log.scroll += SCROLL_UNIT; + win_panel(); + } +} + +void +cx_log_left(void) +{ + if (panel_log.scroll >= SCROLL_UNIT) { + panel_log.scroll -= SCROLL_UNIT; + win_panel(); + } +} + +void +cx_log_mark(void) +{ + msgout(MSG_DEBUG,"-- mark --"); +} + +void +cx_log_home(void) +{ + panel_log.scroll = 0; + win_panel(); +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..94a1c47 --- /dev/null +++ b/src/log.h @@ -0,0 +1,40 @@ +enum MSG_TYPE { /* logged (Y or N), displayed (Y or N) */ + MSG_HEADING = 0, /* -- - heading: valid only if a warning message follows */ + MSG_DEBUG, /* YN - debug info */ + MSG_NOTICE, /* YN - notice */ + MSG_AUDIT, /* YN - user actions (audit trail) */ + MSG_I, /* YY - informational message */ + MSG_i, /* NY - informational message not to be logged */ + MSG_W, /* YY - warning */ + MSG_w, /* NY - warning not to be logged */ + _MSG_TOTAL_ +}; + +#define MSG_MASK 15 /* 4 bits for MSG_TYPE */ + +/* modifiers which may be ORed with MSG_TYPE */ +#define MSG_NOTIFY 16 /* mention the notification panel */ + +/* + * MSG_HEADING usage example: + * msgout(MSG_HEADING,"Reading the xxx file"); + * .... + * if (err1) + * msgout(MSG_W,"read error"); + * .... + * if (err2) + * msgout(MSG_W,"parse error"); + * .... + * msgout(MSG_HEADING,0); + */ + +extern void logfile_open(const char *); +extern void logfile_close(void); +extern int log_prepare(void); +extern void log_panel_data(void); +extern void cx_log_right(void); +extern void cx_log_left(void); +extern void cx_log_mark(void); +extern void cx_log_home(void); +extern void msgout(int, const char *, ...); +extern void vmsgout(int, const char *, va_list); diff --git a/src/match.c b/src/match.c new file mode 100644 index 0000000..dc8b7f3 --- /dev/null +++ b/src/match.c @@ -0,0 +1,86 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* fnmatch */ +#include /* towlower() */ + +#include "match.h" + +#include "mbwstring.h" /* us_convert2mb() */ + +/* match pattern */ + +static USTRING pattern = UNULL; + +void +match_pattern_set(const wchar_t *expr) +{ + us_convert2mb(expr,&pattern); +} + +int +match_pattern(const char *word) +{ + return fnmatch(USTR(pattern),word,FOPT(FOPT_ALL) ? 0 : FNM_PERIOD) == 0; +} + +/* match substring */ + +static USTRINGW substr_orig = UNULL; /* original */ +static USTRINGW substr_lc = UNULL; /* lowercase copy */ +static FLAG lc; /* substr_lc is valid */ + +static void +inplace_tolower(wchar_t *p) +{ + wchar_t ch; + + for (; (ch = *p) != L'\0'; p++) + if (iswupper(ch)) + *p = towlower(ch); +} + +void +match_substr_set(const wchar_t *expr) +{ + usw_copy(&substr_orig,expr); + lc = 0; +} + +int +match_substr(const wchar_t *str) +{ + if (FOPT(FOPT_IC)) + return match_substr_ic(str); + + return wcsstr(str,USTR(substr_orig)) != 0; +} + +int +match_substr_ic(const wchar_t *str) +{ + static USTRINGW buff = UNULL; + wchar_t *str_lc; + + if (!lc) { + inplace_tolower(usw_copy(&substr_lc,USTR(substr_orig))); + lc = 1; + } + + str_lc = usw_copy(&buff,str); + inplace_tolower(str_lc); + return wcsstr(str_lc,USTR(substr_lc)) != 0; +} diff --git a/src/match.h b/src/match.h new file mode 100644 index 0000000..e85d1f6 --- /dev/null +++ b/src/match.h @@ -0,0 +1,5 @@ +extern void match_pattern_set(const wchar_t *); +extern int match_pattern(const char *); +extern void match_substr_set(const wchar_t *); +extern int match_substr(const wchar_t *); +extern int match_substr_ic(const wchar_t *); diff --git a/src/mbwstring.c b/src/mbwstring.c new file mode 100644 index 0000000..9accbbe --- /dev/null +++ b/src/mbwstring.c @@ -0,0 +1,216 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* multibyte and wide string functions */ + +#include "clexheaders.h" + +#include /* mbstowcs() */ +#include /* strlen() */ +#include /* iswprint() in WCW macro */ + +#include "mbwstring.h" + +/* wc_cols() returns width (in display columns) of a substring */ +int +wc_cols(const wchar_t *str, int from, int to /* negative = till end */) +{ + int i, cols; + wchar_t ch; + + for (cols = 0, i = from; to < 0 || i < to; i++) { + if ((ch = str[i]) == L'\0') + break; + cols += WCW(ch); + } + return cols; +} + +/* + * multibyte to wide string conversion with error recovery, + * the result is returned as exit value and also stored in + * the USTRINGW structure 'dst' +*/ +const wchar_t * +usw_convert2w(const char *str, USTRINGW *dst) +{ + int len, max, i, conv; + const char *src; + mbstate_t mbstate; + + /* try the easy way first */ + len = mbstowcs(0,str,0); + if (len >= 0) { + usw_setsize(dst,len + 1); + mbstowcs(PUSTR(dst),str,len + 1); + return PUSTR(dst); + } + + /* there was an error, make a char-by-char conversion with error recovery */ + src = str; + max = usw_setsize(dst,strlen(src) + 1); + memset(&mbstate,0,sizeof(mbstate)); + for (i = 0; /* until return */; i++) { + if (i == max) + max = usw_resize(dst,max + ALLOC_UNIT); + conv = mbsrtowcs(PUSTR(dst) + i,&src,1,&mbstate); + if (conv == -1) { + /* invalid sequence */ + src++; + PUSTR(dst)[i] = lang_data.repl; + memset(&mbstate,0,sizeof(mbstate)); + } + else if (src == 0) + return PUSTR(dst); /* conversion completed */ + } + + /* NOTREACHED */ + return 0; +} + +/* NOTE: the result is overwritten with each successive function call */ +const wchar_t * +convert2w(const char *str) +{ + static USTRINGW local = UNULL; + + return usw_convert2w(str,&local); +} + +/* wide to multibyte string conversion with error recovery */ +const char * +us_convert2mb(const wchar_t *str,USTRING *dst) +{ + int len, max, i, conv; + const wchar_t *src; + mbstate_t mbstate; + + /* try the easy way first */ + len = wcstombs(0,str,0); + if (len >= 0) { + us_setsize(dst,len + 1); + wcstombs(PUSTR(dst),str,len + 1); + return PUSTR(dst); + } + + /* there was an error, make a char-by-char conversion with error recovery */ + src = str; + max = us_setsize(dst,wcslen(src) + 1); + memset(&mbstate,0,sizeof(mbstate)); + for (i = 0; /* until return */; i++) { + if (i == max) + max = us_resize(dst,max + ALLOC_UNIT); + conv = wcsrtombs(PUSTR(dst) + i,&src,1,&mbstate); + if (conv == -1) { + /* invalid sequence */ + src++; + PUSTR(dst)[i] = '?'; + memset(&mbstate,0,sizeof(mbstate)); + } + else if (src == 0) + return PUSTR(dst); /* conversion completed */ + } + + /* NOTREACHED */ + return 0; +} + +/* NOTE: the result is overwritten with each successive function call */ +const char * +convert2mb(const wchar_t *str) +{ + static USTRING local = UNULL; + + return us_convert2mb(str,&local); +} + +/* + * CREDITS: the utf_iscomposing() code including the intable() function + * was taken with small modifications from the VIM text editor + * written by Bram Moolenaar (www.vim.org) + */ + +typedef struct { + unsigned int first, last; +} INTERVAL; + +/* return 1 if 'c' is in 'table' */ +static int +intable (INTERVAL *table, size_t size, int c) +{ + int mid, bot, top; + + /* first quick check */ + if (c < table[0].first) + return 0; + + /* binary search in table */ + bot = 0; + top = size - 1; + while (top >= bot) { + mid = (bot + top) / 2; + if (table[mid].last < c) + bot = mid + 1; + else if (table[mid].first > c) + top = mid - 1; + else + return 1; + } + return 0; +} + +/* + * Return 1 if "ch" is a composing UTF-8 character. This means it will be + * drawn on top of the preceding character. + * Based on code from Markus Kuhn. + */ +int +utf_iscomposing(wchar_t ch) +{ + /* sorted list of non-overlapping intervals */ + static INTERVAL combining[] = + { + {0x0300, 0x034f}, {0x0360, 0x036f}, {0x0483, 0x0486}, {0x0488, 0x0489}, + {0x0591, 0x05a1}, {0x05a3, 0x05b9}, {0x05bb, 0x05bd}, {0x05bf, 0x05bf}, + {0x05c1, 0x05c2}, {0x05c4, 0x05c4}, {0x0610, 0x0615}, {0x064b, 0x0658}, + {0x0670, 0x0670}, {0x06d6, 0x06dc}, {0x06de, 0x06e4}, {0x06e7, 0x06e8}, + {0x06ea, 0x06ed}, {0x0711, 0x0711}, {0x0730, 0x074a}, {0x07a6, 0x07b0}, + {0x0901, 0x0903}, {0x093c, 0x093c}, {0x093e, 0x094d}, {0x0951, 0x0954}, + {0x0962, 0x0963}, {0x0981, 0x0983}, {0x09bc, 0x09bc}, {0x09be, 0x09c4}, + {0x09c7, 0x09c8}, {0x09cb, 0x09cd}, {0x09d7, 0x09d7}, {0x09e2, 0x09e3}, + {0x0a01, 0x0a03}, {0x0a3c, 0x0a3c}, {0x0a3e, 0x0a42}, {0x0a47, 0x0a48}, + {0x0a4b, 0x0a4d}, {0x0a70, 0x0a71}, {0x0a81, 0x0a83}, {0x0abc, 0x0abc}, + {0x0abe, 0x0ac5}, {0x0ac7, 0x0ac9}, {0x0acb, 0x0acd}, {0x0ae2, 0x0ae3}, + {0x0b01, 0x0b03}, {0x0b3c, 0x0b3c}, {0x0b3e, 0x0b43}, {0x0b47, 0x0b48}, + {0x0b4b, 0x0b4d}, {0x0b56, 0x0b57}, {0x0b82, 0x0b82}, {0x0bbe, 0x0bc2}, + {0x0bc6, 0x0bc8}, {0x0bca, 0x0bcd}, {0x0bd7, 0x0bd7}, {0x0c01, 0x0c03}, + {0x0c3e, 0x0c44}, {0x0c46, 0x0c48}, {0x0c4a, 0x0c4d}, {0x0c55, 0x0c56}, + {0x0c82, 0x0c83}, {0x0cbc, 0x0cbc}, {0x0cbe, 0x0cc4}, {0x0cc6, 0x0cc8}, + {0x0cca, 0x0ccd}, {0x0cd5, 0x0cd6}, {0x0d02, 0x0d03}, {0x0d3e, 0x0d43}, + {0x0d46, 0x0d48}, {0x0d4a, 0x0d4d}, {0x0d57, 0x0d57}, {0x0d82, 0x0d83}, + {0x0dca, 0x0dca}, {0x0dcf, 0x0dd4}, {0x0dd6, 0x0dd6}, {0x0dd8, 0x0ddf}, + {0x0df2, 0x0df3}, {0x0e31, 0x0e31}, {0x0e34, 0x0e3a}, {0x0e47, 0x0e4e}, + {0x0eb1, 0x0eb1}, {0x0eb4, 0x0eb9}, {0x0ebb, 0x0ebc}, {0x0ec8, 0x0ecd}, + {0x0f18, 0x0f19}, {0x0f35, 0x0f35}, {0x0f37, 0x0f37}, {0x0f39, 0x0f39}, + {0x0f3e, 0x0f3f}, {0x0f71, 0x0f84}, {0x0f86, 0x0f87}, {0x0f90, 0x0f97}, + {0x0f99, 0x0fbc}, {0x0fc6, 0x0fc6}, {0x102c, 0x1032}, {0x1036, 0x1039}, + {0x1056, 0x1059}, {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753}, + {0x1772, 0x1773}, {0x17b6, 0x17d3}, {0x17dd, 0x17dd}, {0x180b, 0x180d}, + {0x18a9, 0x18a9}, {0x1920, 0x192b}, {0x1930, 0x193b}, {0x20d0, 0x20ea}, + {0x302a, 0x302f}, {0x3099, 0x309a}, {0xfb1e, 0xfb1e}, {0xfe00, 0xfe0f}, + {0xfe20, 0xfe23}, + }; + + return lang_data.utf8 && intable(combining,ARRAY_SIZE(combining),(int)ch); +} diff --git a/src/mbwstring.h b/src/mbwstring.h new file mode 100644 index 0000000..336e7e3 --- /dev/null +++ b/src/mbwstring.h @@ -0,0 +1,13 @@ +extern const wchar_t *usw_convert2w(const char *, USTRINGW *); +extern const wchar_t *convert2w(const char *); +extern const char *us_convert2mb(const wchar_t *, USTRING *); +extern const char *convert2mb(const wchar_t *); +extern int utf_iscomposing(wchar_t); +extern int wc_cols(const wchar_t *, int, int); + +/* should not remain invisible: + * A0 = no-break space (shell does not understand it) + * AD = soft-hyphen + */ +#define ISWPRINT(CH) (!(lang_data.utf8 && ((CH) == L'\xa0' || (CH) == L'\xad')) && iswprint(CH)) +#define WCW(X) (ISWPRINT(X) ? wcwidth(X) : 1) diff --git a/src/mouse.c b/src/mouse.c new file mode 100644 index 0000000..bc862e4 --- /dev/null +++ b/src/mouse.c @@ -0,0 +1,92 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ +#include /* fputs() */ +#include "curses.h" /* NCURSES_MOUSE_VERSION */ + +#include "mouse.h" + +#include "cfg.h" /* cfg_num() */ +#include "control.h" /* control_loop() */ +#include "log.h" /* msgout() */ + +static FLAG enabled = 0; + +void +mouse_initialize(void) +{ + mouse_reconfig(); + mouse_set(); +} + +void +mouse_reconfig(void) +{ + enabled = cfg_num(CFG_MOUSE) > 0; + disp_data.mouse_swap = cfg_num(CFG_MOUSE) == 2; + if (enabled && !disp_data.mouse) { + msgout(MSG_NOTICE,"Cannot enable the mouse input (mouse interface not found)"); + enabled = 0; + } +#if NCURSES_MOUSE_VERSION >= 2 + if (enabled){ + mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, (mmask_t *)0); + mouseinterval(0); /* must implement double click by ourselves */ + } + else + mousemask(0, (mmask_t *)0); +#endif +} + +void +mouse_set(void) +{ +#if NCURSES_MOUSE_VERSION < 2 + if (enabled) { + fputs("\033[?1001s" "\033[?1002h",stdout); + fflush(stdout); + } +#endif +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +mouse_restore(void) +{ +#if NCURSES_MOUSE_VERSION < 2 + if (enabled) { + fputs("\033[?1002l" "\033[?1001r",stdout); + fflush(stdout); + } +#endif +} + +void +cx_common_mouse(void) +{ + if (MI_B(2)) { + msgout(MSG_i,"press the shift if you want to paste or copy text with the mouse"); + return; + } + + if (MI_AREA(BAR) && MI_DC(1)) { + if (minp.cursor == 0) + control_loop(MODE_HELP); + else if (minp.cursor == 1) + next_mode = MODE_SPECIAL_RETURN; + } +} diff --git a/src/mouse.h b/src/mouse.h new file mode 100644 index 0000000..0ab0504 --- /dev/null +++ b/src/mouse.h @@ -0,0 +1,5 @@ +extern void mouse_initialize(void); +extern void mouse_reconfig(void); +extern void mouse_restore(void); +extern void mouse_set(void); +extern void cx_common_mouse(void); diff --git a/src/notify.c b/src/notify.c new file mode 100644 index 0000000..39d3700 --- /dev/null +++ b/src/notify.c @@ -0,0 +1,71 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include "notify.h" + +#include "inout.h" /* win_panel_opt() */ +#include "opt.h" /* opt_changed() */ + +int +notif_prepare(void) +{ + panel_notif.pd->top = panel_notif.pd->curs = panel_notif.pd->min; + panel = panel_notif.pd; + textline = 0; + return 0; +} + +/* write options to a string */ +const char * +notif_saveopt(void) +{ + int i, j; + static char buff[NOTIF_TOTAL_ + 1]; + + for (i = j = 0; i < NOTIF_TOTAL_; i++) + if (NOPT(i)) + buff[j++] = 'A' + i; + buff[j] = '\0'; + + return buff; +} + +/* read options from a string */ +int +notif_restoreopt(const char *opt) +{ + int i; + unsigned char ch; + + for (i = 0; i < NOTIF_TOTAL_; i++) + NOPT(i) = 0; + + while ( (ch = *opt++) ) { + if (ch < 'A' || ch >= 'A' + NOTIF_TOTAL_) + return -1; + NOPT(ch - 'A') = 1; + } + + return 0; +} + +void +cx_notif(void) +{ + TOGGLE(NOPT(panel_notif.pd->curs)); + opt_changed(); + win_panel_opt(); +} diff --git a/src/notify.h b/src/notify.h new file mode 100644 index 0000000..c143df1 --- /dev/null +++ b/src/notify.h @@ -0,0 +1,4 @@ +extern int notif_prepare(void); +extern const char *notif_saveopt(void); +extern int notif_restoreopt(const char *); +extern void cx_notif(void); diff --git a/src/opt.c b/src/opt.c new file mode 100644 index 0000000..343d89d --- /dev/null +++ b/src/opt.c @@ -0,0 +1,132 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ +#include /* printf() */ +#include /* strerror() */ + +#include "opt.h" + +#include "cfg.h" /* cfg_num() */ +#include "cmp.h" /* cmp_saveopt() */ +#include "filerw.h" /* fr_open() */ +#include "filter.h" /* fopt_saveopt() */ +#include "log.h" /* msgout() */ +#include "notify.h" /* notif_saveopt() */ +#include "sort.h" /* sort_saveopt() */ + +/* limits to protect resources */ +#define OPT_FILESIZE_LIMIT 150 +#define OPT_LINES_LIMIT 15 + +static FLAG changed = 0; /* options have changed */ + +/* return value: 0 = ok, -1 = error */ +static int +opt_read(void) +{ + int i, tfd, split, code; + const char *line, *value; + FLAG corrupted; + + tfd = fr_open(user_data.file_opt,OPT_FILESIZE_LIMIT); + if (tfd == FR_NOFILE) + return 0; /* missing optional file is ok */ + if (tfd < 0) + return -1; + msgout(MSG_DEBUG,"OPTIONS: Processing options file \"%s\"",user_data.file_opt); + + split = fr_split(tfd,OPT_LINES_LIMIT); + if (split < 0 && split != FR_LINELIMIT) { + fr_close(tfd); + return -1; + } + + for (corrupted = 0, i = 0; (line = fr_line(tfd,i)); i++) { + /* split VARIABLE and VALUE */ + if ( (value = strchr(line,'=')) == 0) { + corrupted = 1; + continue; + } + value++; + if (strncmp(line,"COMPARE=",8) == 0) + code = cmp_restoreopt(value); + else if (strncmp(line,"FILTER=",7) == 0) + code = fopt_restoreopt(value); + else if (strncmp(line,"SORT=",5) == 0) + code = sort_restoreopt(value); + else if (strncmp(line,"NOTIFY=",7) == 0) + code = notif_restoreopt(value); + else + code = -1; + if (code < 0) + corrupted = 1; + } + fr_close(tfd); + + if (split < 0 || corrupted) { + msgout(MSG_NOTICE,"Invalid contents, the options file is outdated or corrupted"); + return -1; + } + return 0; +} + +void +opt_initialize(void) +{ + if (opt_read() == 0) + return; + + if (!user_data.nowrite) { + /* automatic recovery */ + msgout(MSG_NOTICE,"Attempting to overwrite the invalid options file"); + changed = 1; + opt_save(); + msgout(MSG_NOTICE,changed ? "Attempt failed" : "Attempt succeeded"); + } + msgout(MSG_W,"OPTIONS: An error occurred while reading data, details in log"); +} + +void +opt_changed(void) +{ + changed = 1; +} + +/* this is a cleanup function (see err_exit() in control.c) */ +/* errors are not reported to the user, because this action is not initiated by him/her */ +void +opt_save(void) +{ + FILE *fp; + + if (!changed || user_data.nowrite) + return; + + if ( (fp = fw_open(user_data.file_opt)) == 0) + return; + + fprintf(fp, + "#\n# CLEX options file\n#\n" + "COMPARE=%s\n" + "FILTER=%s\n" + "SORT=%s\n" + "NOTIFY=%s\n",cmp_saveopt(),fopt_saveopt(), + sort_saveopt(),notif_saveopt()); + + if (fw_close(fp) == 0) + changed = 0; +} diff --git a/src/opt.h b/src/opt.h new file mode 100644 index 0000000..558c077 --- /dev/null +++ b/src/opt.h @@ -0,0 +1,3 @@ +extern void opt_initialize(void); +extern void opt_changed(void); +extern void opt_save(void); diff --git a/src/panel.c b/src/panel.c new file mode 100644 index 0000000..128f270 --- /dev/null +++ b/src/panel.c @@ -0,0 +1,168 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include "panel.h" + +#include "cfg.h" /* cfg_num() */ +#include "filter.h" /* cx_filter() */ +#include "inout.h" /* win_panel_opt() */ + +void +pan_up_n(int n) +{ + if (panel->curs <= panel->min) + return; + + panel->curs -= n; + LIMIT_MIN(panel->curs,panel->min); + LIMIT_MAX(panel->top,panel->curs); + win_panel_opt(); +} + +void +cx_pan_up(void) +{ + pan_up_n(1); +} + +void +pan_down_n(int n) +{ + if (panel->curs >= panel->cnt - 1) + return; + + panel->curs += n; + LIMIT_MAX(panel->curs,panel->cnt - 1); + LIMIT_MIN(panel->top,panel->curs - disp_data.panlines + 1); + win_panel_opt(); +} + +void +cx_pan_down(void) +{ + pan_down_n(1); +} + +/* move to a screen line */ +void +pan_line(int n) +{ + int newcurs; + + if (n < 0 || n >= disp_data.panlines) + return; + newcurs = panel->top + n; + if (newcurs >= panel->cnt || newcurs == panel->curs) + return; + + panel->curs = newcurs; + win_panel_opt(); +} + +void +cx_pan_mouse(void) +{ + if (!MI_CLICK && !MI_WHEEL) + return; + + switch (minp.area) { + case AREA_PANEL: + if (MI_CLICK) + pan_line(minp.ypanel); + else + (MI_B(4) ? pan_up_n : pan_down_n)(cfg_num(CFG_MOUSE_SCROLL)); + break; + case AREA_TOPFRAME: + if (MI_CLICK) + cx_pan_pgup(); + break; + case AREA_BOTTOMFRAME: + if (MI_CLICK) + cx_pan_pgdown(); + break; + } +} + +void +cx_pan_home(void) +{ + panel->top = panel->curs = panel->min; + win_panel_opt(); +} + +void +cx_pan_end(void) +{ + panel->curs = panel->cnt - 1; + LIMIT_MIN(panel->top,panel->curs - disp_data.panlines + 1); + win_panel_opt(); +} + +void +cx_pan_pgup(void) +{ + if (panel->curs > panel->min) { + if (panel->curs != panel->top) + panel->curs = panel->top; + else { + panel->curs -= disp_data.panlines; + LIMIT_MIN(panel->curs,panel->min); + panel->top = panel->curs; + } + win_panel_opt(); + } +} + +void +cx_pan_pgdown(void) +{ + if (panel->curs < panel->cnt - 1) { + if (panel->curs != panel->top + disp_data.panlines - 1) + panel->curs = panel->top + disp_data.panlines - 1; + else + panel->curs += disp_data.panlines; + LIMIT_MAX(panel->curs,panel->cnt - 1); + LIMIT_MIN(panel->top,panel->curs - disp_data.panlines + 1); + win_panel_opt(); + } +} + +void +cx_pan_middle(void) +{ + panel->top = panel->curs - disp_data.panlines / 2; + LIMIT_MAX(panel->top,panel->cnt - disp_data.panlines); + LIMIT_MIN(panel->top,panel->min); + win_panel_opt(); +} + +void +pan_adjust(PANEL_DESC *p) +{ + /* always in bounds */ + LIMIT_MAX(p->top,p->cnt - 1); + LIMIT_MIN(p->top,p->min); + LIMIT_MAX(p->curs,p->cnt - 1); + LIMIT_MIN(p->curs,p->min); + + /* cursor must be visible */ + if (p->top > p->curs || p->top <= p->curs - disp_data.panlines) + p->top = p->curs - disp_data.panlines / 3; + /* bottom of the screen shouldn't be left blank ... */ + LIMIT_MAX(p->top,p->cnt - disp_data.panlines); + /* ... but that is not always possible */ + LIMIT_MIN(p->top,p->min); +} diff --git a/src/panel.h b/src/panel.h new file mode 100644 index 0000000..2805b01 --- /dev/null +++ b/src/panel.h @@ -0,0 +1,12 @@ +extern void cx_pan_down(void); +extern void pan_down_n(int); +extern void cx_pan_up(void); +extern void pan_up_n(int); +extern void pan_line(int); +extern void cx_pan_end(void); +extern void cx_pan_home(void); +extern void cx_pan_pgdown(void); +extern void cx_pan_pgup(void); +extern void cx_pan_middle(void); +extern void cx_pan_mouse(void); +extern void pan_adjust(PANEL_DESC *); diff --git a/src/preview.c b/src/preview.c new file mode 100644 index 0000000..709d96d --- /dev/null +++ b/src/preview.c @@ -0,0 +1,101 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include +#include + +#include "preview.h" + +#include "filerw.h" +#include "log.h" +#include "mbwstring.h" + +static const char * +expand_tabs(const char *str) +{ + char ch, *out; + int len, cnt; + static USTRING buff = UNULL; + + for (cnt = len = 0; (ch = str[len++]); ) + if (ch == '\t') + cnt++; + if (cnt == 0) + return str; + + us_setsize(&buff, len + 7 * cnt); + out = USTR(buff); + for (len = 0; (ch = *str++); ) { + if (ch == '\t') + do { + out[len++] = ' '; + } while (len % 8); + else + out[len++] = ch; + } + out[len] = '\0'; + + return out; +} + +int +preview_prepare(void) +{ + int i, tfd; + const char *line; + FILE_ENTRY *pfe; + + pfe = ppanel_file->files[ppanel_file->pd->curs]; + + if (!IS_FT_PLAIN(pfe->file_type)) { + msgout(MSG_i,"PREVIEW: not a regular file"); + return -1; + } + + tfd = fr_open_preview(SDSTR(pfe->file), PREVIEW_BYTES); + if (tfd < 0) { + msgout(MSG_i,"PREVIEW: unable to read the file, details in log"); + return -1; + } + if (!fr_is_text(tfd)) { + msgout(MSG_i, "PREVIEW: not a text file"); + fr_close(tfd); + return -1; + } + fr_split_preview(tfd,PREVIEW_LINES); + for (i = 0; (line = fr_line(tfd,i)); i++) + usw_convert2w(expand_tabs(line), &panel_preview.line[i]); + panel_preview.pd->cnt = panel_preview.realcnt = i; + if (fr_is_truncated(tfd)) + panel_preview.pd->cnt++; + fr_close(tfd); + + panel_preview.pd->top = panel_preview.pd->curs = 0; + panel_preview.title = SDSTR(pfe->filew); + + panel = panel_preview.pd; + textline = 0; + return 0; +} + +void +cx_preview_mouse(void) +{ + if (MI_AREA(PANEL) && MI_DC(1)) { + next_mode = MODE_SPECIAL_RETURN; + minp.area = AREA_NONE; + } +} diff --git a/src/preview.h b/src/preview.h new file mode 100644 index 0000000..0b74e9a --- /dev/null +++ b/src/preview.h @@ -0,0 +1,2 @@ +extern int preview_prepare(void); +extern void cx_preview_mouse(void); diff --git a/src/rename.c b/src/rename.c new file mode 100644 index 0000000..462173c --- /dev/null +++ b/src/rename.c @@ -0,0 +1,99 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* errno */ +#include /* log.h */ +#include /* rename() */ +#include /* strchr() */ +#include /* stat() */ +#include /* iswprint() */ + +#include "rename.h" + +#include "edit.h" /* edit_nu_putstr() */ +#include "inout.h" /* win_panel() */ +#include "list.h" /* list_directory() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2mb() */ + +static FILE_ENTRY *pfe; + +int +rename_prepare(void) +{ + wchar_t ch, *pch; + + /* inherited panel = ppanel_file->pd */ + if (panel->filtering == 1) + panel->filtering = 2; + + pfe = ppanel_file->files[ppanel_file->pd->curs]; + if (pfe->dotdir) { + msgout(MSG_w,"RENAME: refusing to rename the . and .. directories"); + return -1; + } + + edit_setprompt(&line_tmp,L"Rename the current file to: "); + textline = &line_tmp; + edit_nu_putstr(SDSTR(pfe->filew)); + for (pch = USTR(textline->line); (ch = *pch) != L'\0'; pch++) + if (!ISWPRINT(ch) || (lang_data.utf8 && ch == L'\xFFFD')) + *pch = L'_'; + + return 0; +} + +void +cx_rename(void) +{ + const char *oldname, *newname; + const wchar_t *newnamew; + struct stat st; + + if (line_tmp.size == 0) { + next_mode = MODE_SPECIAL_RETURN; + return; + } + + oldname = SDSTR(pfe->file); + newname = convert2mb(newnamew = USTR(textline->line)); + if (strcmp(newname,oldname) == 0) { + msgout(MSG_i,"file not renamed"); + next_mode = MODE_SPECIAL_RETURN; + return; + } + if (strchr(newname,'/')) { + msgout(MSG_i,"please enter the name without a directory part"); + return; + } + if (stat(newname,&st) == 0) { + msgout(MSG_i,"a file with this name exists already"); + return; + } + if (rename(oldname,newname) < 0) + msgout(MSG_w,"Renaming has failed: %s",strerror(errno)); + /* NFS: it does not mean the file is not renamed */ + else { + msgout(MSG_AUDIT,"Rename: \"%s\" --> \"%s\" in \"%s\"", + oldname,newname,USTR(ppanel_file->dir)); + sd_copy(&pfe->file,newname); + sdw_copy(&pfe->filew,newnamew); + } + list_directory(); + win_panel(); + next_mode = MODE_SPECIAL_RETURN; +} diff --git a/src/rename.h b/src/rename.h new file mode 100644 index 0000000..a59f882 --- /dev/null +++ b/src/rename.h @@ -0,0 +1,2 @@ +extern int rename_prepare(void); +extern void cx_rename(void); diff --git a/src/sdstring.c b/src/sdstring.c new file mode 100644 index 0000000..df1207e --- /dev/null +++ b/src/sdstring.c @@ -0,0 +1,162 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* free() */ +#include /* strlen() */ + +#include "util.h" /* estrdup() */ + +/* + * The SDSTRING structure (defined in sdstring.h) was designed for + * storing/retrieving of object names (e.g. file names or user + * names). + * + * Up to 95% of such names are short (less than 16 chars), these + * names are stored in static memory, long names are stored in + * dynamically allocated memory. + * + * - to initialize before first use: + * static SDSTRING sds = SDNULL("short_string"); + * static SDSTRINGW sds = SDNULL(L"short_string"); + * or + * SD_INIT(sdstring); + * - to re-initialize, e.g. before deallocating dynamic SDSTRING: + * sd_reset(); + * - to store a name (NULL ptr cannot be stored): + * sd_copy() + * or + * sd_copyn(); + * - to retrieve a name: + * SDSTR(sds) + * or + * PSDSTR(psds) + * + * WARNING: SD_INIT, SNULL, SDSTR, and PSDSTR are macros. + */ + +void +sd_reset(SDSTRING *psd) +{ + if (psd->SDname) { + free(psd->SDname); + psd->SDname = 0; + } + psd->SDmem[0] = '\0'; +} + +void +sd_copy(SDSTRING *psd, const char *src) +{ + if (psd->SDname) + free(psd->SDname); + + if (strlen(src) <= SDSTRING_LEN) { + psd->SDname = 0; + strcpy(psd->SDmem,src); + } + else { + psd->SDname = estrdup(src); + psd->SDmem[0] = '\0'; + } +} + +/* note: sd_copyn() adds terminating null byte */ +void +sd_copyn(SDSTRING *psd, const char *src, size_t len) +{ + char *dst; + + if (psd->SDname) + free(psd->SDname); + + if (len <= SDSTRING_LEN) { + psd->SDname = 0; + dst = psd->SDmem; + } + else { + dst = psd->SDname = emalloc(len + 1); + psd->SDmem[0] = '\0'; + } + + dst[len] = '\0'; + while (len-- > 0) + dst[len] = src[len]; +} + +void +sdw_reset(SDSTRINGW *psd) +{ + if (psd->SDname) { + free(psd->SDname); + psd->SDname = 0; + } + psd->SDmem[0] = L'\0'; +} + +#if 0 +void +sdw_xchg(SDSTRINGW *psd1, SDSTRINGW *psd2) +{ + wchar_t *name, mem[SDSTRING_LEN + 1]; + + name = psd2->SDname; + psd2->SDname = psd1->SDname; + psd1->SDname = name; + + wcscpy(mem,psd2->SDmem); + wcscpy(psd2->SDmem,psd1->SDmem); + wcscpy(psd1->SDmem,mem); +} +#endif + +void +sdw_copy(SDSTRINGW *psd, const wchar_t *src) +{ + if (psd->SDname) + free(psd->SDname); + + if (wcslen(src) <= SDSTRING_LEN) { + psd->SDname = 0; + wcscpy(psd->SDmem,src); + } + else { + psd->SDname = ewcsdup(src); + psd->SDmem[0] = L'\0'; + } +} + +/* note: sdw_copyn() adds terminating null byte */ +void +sdw_copyn(SDSTRINGW *psd, const wchar_t *src, size_t len) +{ + wchar_t *dst; + + if (psd->SDname) + free(psd->SDname); + + if (len <= SDSTRING_LEN) { + psd->SDname = 0; + dst = psd->SDmem; + } + else { + dst = psd->SDname = emalloc((len + 1) * sizeof(wchar_t)); + psd->SDmem[0] = L'\0'; + } + + dst[len] = L'\0'; + while (len-- > 0) + dst[len] = src[len]; +} diff --git a/src/sdstring.h b/src/sdstring.h new file mode 100644 index 0000000..e1666ed --- /dev/null +++ b/src/sdstring.h @@ -0,0 +1,23 @@ +#define SDSTRING_LEN 15 +typedef struct { + char *SDname; /* string if long, otherwise null ptr */ + char SDmem[SDSTRING_LEN + 1]; /* string if short, otherwise null string */ +} SDSTRING; + +typedef struct { + wchar_t *SDname; /* string if long, otherwise null ptr */ + wchar_t SDmem[SDSTRING_LEN + 1]; /* string if short, otherwise null string */ +} SDSTRINGW; + +#define SD_INIT(X) do { (X).SDname = 0; (X).SDmem[0] = '\0';} while (0) +#define PSDSTR(X) ((X)->SDname ? (X)->SDname : (X)->SDmem) +#define SDSTR(X) ((X).SDname ? (X).SDname : (X).SDmem) +#define SDNULL(STR) {0,STR} + +extern void sd_reset(SDSTRING *); +extern void sd_copy(SDSTRING *, const char *); +extern void sd_copyn(SDSTRING *, const char *, size_t); + +extern void sdw_reset(SDSTRINGW *); +extern void sdw_copy(SDSTRINGW *, const wchar_t *); +extern void sdw_copyn(SDSTRINGW *, const wchar_t *, size_t); diff --git a/src/select.c b/src/select.c new file mode 100644 index 0000000..477c155 --- /dev/null +++ b/src/select.c @@ -0,0 +1,119 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* stat() */ +#include /* open() */ +#include /* qsort() */ +#include /* strcmp() */ +#include /* close() */ + +#include "select.h" + +#include "control.h" /* get_current_mode() */ +#include "edit.h" /* edit_nu_putstr() */ +#include "inout.h" /* win_panel() */ +#include "list.h" /* list_directory_cond() */ +#include "match.h" /* match_pattern() */ + +static SDSTRINGW savepat[2] = { SDNULL(L"*"), SDNULL(L"*") }; +static FLAG mode_sel; /* 1 = select, 0 = deselect */ + +int +select_prepare(void) +{ + if (list_directory_cond(PANEL_EXPTIME) == 0) + win_panel(); + + mode_sel = get_current_mode() == MODE_SELECT; /* 0 or 1 */ + + panel = ppanel_file->pd; + if (panel->filtering == 1) + panel->filtering = 2; + + edit_setprompt(&line_tmp,mode_sel ? L"SELECT files: " : L"DESELECT files: "); + textline = &line_tmp; + edit_nu_putstr(SDSTR(savepat[mode_sel])); + + return 0; +} + +void +cx_select_toggle(void) +{ + ppanel_file->selected += + TOGGLE(ppanel_file->files[ppanel_file->pd->curs]->select) ? +1 : -1; + + /* cursor down */ + if (ppanel_file->pd->curs < ppanel_file->pd->cnt - 1) { + ppanel_file->pd->curs++; + LIMIT_MIN(ppanel_file->pd->top, + ppanel_file->pd->curs - disp_data.panlines + 1); + } + win_panel(); +} + +void +cx_select_range(void) +{ + int i; + FLAG mode; /* same meaning as mode_sel */ + + for (mode = !ppanel_file->files[i = ppanel_file->pd->curs]->select; + i >= 0 && ppanel_file->files[i]->select != mode; i--) + ppanel_file->files[i]->select = mode; + ppanel_file->selected += mode + ? ppanel_file->pd->curs - i : i - ppanel_file->pd->curs; + win_panel(); +} + +void +cx_select_invert(void) +{ + int i; + + for (i = 0; i < ppanel_file->pd->cnt; i++) + TOGGLE(ppanel_file->files[i]->select); + ppanel_file->selected = ppanel_file->pd->cnt - ppanel_file->selected; + win_panel(); +} + +void +cx_select_files(void) +{ + int i, cnt; + const wchar_t *sre; + FILE_ENTRY *pfe; + + next_mode = MODE_SPECIAL_RETURN; + if (line_tmp.size == 0) + return; + + sre = USTR(textline->line); + match_pattern_set(sre); + + /* save the pattern */ + sdw_copy(savepat + mode_sel,sre); + + for (i = cnt = 0; i < ppanel_file->pd->cnt; i++) { + pfe = ppanel_file->files[i]; + if (pfe->select != mode_sel && match_pattern(SDSTR(pfe->file))) { + pfe->select = mode_sel; + cnt++; + } + } + ppanel_file->selected += mode_sel ? +cnt : -cnt; + win_panel(); +} diff --git a/src/select.h b/src/select.h new file mode 100644 index 0000000..d1a69d3 --- /dev/null +++ b/src/select.h @@ -0,0 +1,5 @@ +extern int select_prepare(void); +extern void cx_select_toggle(void); +extern void cx_select_range(void); +extern void cx_select_invert(void); +extern void cx_select_files(void); diff --git a/src/signals.c b/src/signals.c new file mode 100644 index 0000000..ed3b7b1 --- /dev/null +++ b/src/signals.c @@ -0,0 +1,85 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* sigaction() */ +#include /* _POSIX_JOB_CONTROL */ + +#include "signals.h" + +#include "control.h" /* err_exit() */ +#include "inout.h" /* curses_cbreak() */ +#include "tty.h" /* tty_ctrlc() */ + +static RETSIGTYPE +int_handler(int sn) +{ + err_exit("Signal %s caught", sn == SIGTERM ? "SIGTERM" : "SIGHUP"); +} + +static RETSIGTYPE +ctrlc_handler(int unused) +{ + ctrlc_flag = 1; +} + +void +signal_initialize(void) +{ + struct sigaction act; + + /* ignore keyboard generated signals */ + act.sa_handler = SIG_IGN; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGINT,&act,0); + sigaction(SIGQUIT,&act,0); + + /* catch termination signals */ + act.sa_handler = int_handler; + sigaddset(&act.sa_mask,SIGTERM); + sigaddset(&act.sa_mask,SIGHUP); + sigaction(SIGTERM,&act,0); + sigaction(SIGHUP,&act,0); +} + +void +signal_ctrlc_on(void) +{ + struct sigaction act; + + curses_cbreak(); + tty_save(); + tty_ctrlc(); + + act.sa_handler = ctrlc_handler; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGINT,&act,0); +} + +void +signal_ctrlc_off(void) +{ + struct sigaction act; + + act.sa_handler = SIG_IGN; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGINT,&act,0); + + tty_restore(); + curses_raw(); +} diff --git a/src/signals.h b/src/signals.h new file mode 100644 index 0000000..487540a --- /dev/null +++ b/src/signals.h @@ -0,0 +1,3 @@ +extern void signal_initialize(void); +extern void signal_ctrlc_on(void); +extern void signal_ctrlc_off(void); diff --git a/src/sort.c b/src/sort.c new file mode 100644 index 0000000..68c820f --- /dev/null +++ b/src/sort.c @@ -0,0 +1,311 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* iswdigit() */ +#include /* qsort() */ +#include /* strlen() */ + +/* major() */ +#ifdef MAJOR_IN_MKDEV +# include +#endif +#ifdef MAJOR_IN_SYSMACROS +# include +#endif + +#include "sort.h" + +#include "directory.h" /* filepos_save() */ +#include "inout.h" /* win_panel() */ +#include "list.h" /* file_panel_data() */ +#include "opt.h" /* opt_changed() */ + +int +sort_prepare(void) +{ + panel_sort.pd->top = panel_sort.pd->min; + panel_sort.pd->curs = HIDE_TOTAL_ + GROUP_TOTAL_ + 2 + panel_sort.order; + /* place the cursor at the current sort order */ + panel_sort.newgroup = panel_sort.group; + panel_sort.neworder = panel_sort.order; + panel_sort.newhide = panel_sort.hide; + panel = panel_sort.pd; + textline = 0; + return 0; +} + +void +cx_sort_set(void) +{ + int sel; + + sel = panel_sort.pd->curs; + + if (sel < HIDE_TOTAL_) { + panel_sort.newhide = sel; + win_panel(); + return; + } + if (sel == HIDE_TOTAL_) + return; + sel -= HIDE_TOTAL_ + 1; + + if (sel < GROUP_TOTAL_) { + /* set grouping */ + panel_sort.newgroup = sel; + win_panel(); + return; + } + if (sel == GROUP_TOTAL_) + return; + sel -= GROUP_TOTAL_ + 1; + + if (sel < SORT_TOTAL_) { + /* set sort order */ + panel_sort.neworder = sel; + win_panel(); + return; + } + + /* activate settings */ + sel -= SORT_TOTAL_; + if (sel == 0) { + /* setting the default */ + if (panel_sort.order != panel_sort.neworder + || panel_sort.group != panel_sort.newgroup + || panel_sort.hide != panel_sort.newhide) { + panel_sort.group = panel_sort.newgroup; + panel_sort.order = panel_sort.neworder; + panel_sort.hide = panel_sort.newhide; + opt_changed(); + } + if (ppanel_file->other->order != panel_sort.neworder + || ppanel_file->other->group != panel_sort.newgroup + || ppanel_file->other->hide != panel_sort.newhide) { + ppanel_file->other->group = panel_sort.newgroup; + ppanel_file->other->order = panel_sort.neworder; + ppanel_file->other->hide = panel_sort.newhide; + ppanel_file->other->expired = 1; + } + } + if (ppanel_file->hide != panel_sort.newhide) { + ppanel_file->group = panel_sort.newgroup; + ppanel_file->order = panel_sort.neworder; + ppanel_file->hide = panel_sort.newhide; + list_directory(); + } + else if (ppanel_file->order != panel_sort.neworder + || ppanel_file->group != panel_sort.newgroup) { + ppanel_file->group = panel_sort.newgroup; + ppanel_file->order = panel_sort.neworder; + filepos_save(); + sort_files(); + filepos_set(); + } + next_mode = MODE_SPECIAL_RETURN; +} + +const char * +sort_saveopt(void) +{ + static char buff[4] = "???"; + + buff[0] = 'A' + panel_sort.order; + buff[1] = 'A' + panel_sort.group; + buff[2] = 'A' + panel_sort.hide; + + return buff; +} + +/* read options from a string */ +int +sort_restoreopt(const char *opt) +{ + unsigned char ch; + + ch = opt[0]; + if (ch < 'A' || ch >= 'A' + SORT_TOTAL_) + return -1; + panel_sort.order = ch - 'A'; + ch = opt[1]; + if (ch < 'A' || ch >= 'A' + GROUP_TOTAL_) + return -1; + panel_sort.group = ch - 'A'; + ch = opt[2]; + if (ch == '\0') + return 0; /* up to 4.6.5 */ + if (ch < 'A' || ch >= 'A' + HIDE_TOTAL_) + return -1; + panel_sort.hide = ch - 'A'; + return opt[3] == '\0' ? 0 : -1; +} + +/* compare reversed strings */ +static int +revstrcmp(const char *s1, const char *s2) +{ + size_t i1, i2; + int c1, c2; + + for (i1 = strlen(s1), i2 = strlen(s2); i1 > 0 && i2 > 0;) { + c1 = (unsigned char)s1[--i1]; + c2 = (unsigned char)s2[--i2]; + /* + * ignoring LOCALE, this sort order has nothing to do + * with a human language, it is intended for sendmail + * queue directories + */ + if (c1 != c2) + return c1 - c2; + } + return CMP(i1,i2); +} + +/* sort_group() return values in grouping order */ +enum FILETYPE_TYPE { + FILETYPE_DOTDIR, FILETYPE_DOTDOTDIR, FILETYPE_DIR, + FILETYPE_BDEV, FILETYPE_CDEV, FILETYPE_OTHER, FILETYPE_PLAIN +}; +static int +sort_group(int gr, FILE_ENTRY *pfe) +{ + int type; + + type = pfe->file_type; + if (IS_FT_PLAIN(type)) + return FILETYPE_PLAIN; + if (IS_FT_DIR(type)) { + if (pfe->dotdir == 1) + return FILETYPE_DOTDIR; + if (pfe->dotdir == 2) + return FILETYPE_DOTDOTDIR; + return FILETYPE_DIR; + } + if (gr == GROUP_DBCOP) { + if (type == FT_DEV_CHAR) + return FILETYPE_CDEV; + if (type == FT_DEV_BLOCK) + return FILETYPE_BDEV; + } + return FILETYPE_OTHER; +} + +/* compare function for numeric sort */ +int +num_wcscoll(const wchar_t *name1, const wchar_t *name2) +{ + wchar_t ch1, ch2; + int i, len1, len2, len; + + for (; /* until break */; ) { + while (*name1 && *name1 == *name2 && !iswdigit(*name1)) { + name1++; + name2++; + } + if (!iswdigit(*name1) || !iswdigit(*name2)) + break; + + /* compare two numbers (zero padded to the same length) */ + for (len1 = 1; iswdigit(name1[len1]); ) + len1++; + for (len2 = 1; iswdigit(name2[len2]); ) + len2++; + len = len1 > len2 ? len1 : len2; + for (i = 0; i < len; i++) { + ch1 = (i + len1 - len < 0) ? '0' : name1[i + len1 - len]; + ch2 = (i + len2 - len < 0) ? '0' : name2[i + len2 - len]; + if (ch1 != ch2) + return CMP(ch1,ch2); + } + if (len1 != len2) + return len2 - len1; + + name1 += len; + name2 += len; + } + return wcscoll(name1,name2); +} + +static int +qcmp(const void *e1, const void *e2) +{ + int cmp, gr, group1, group2; + FILE_ENTRY *pfe1, *pfe2; + + pfe1 = (*(FILE_ENTRY **)e1); + pfe2 = (*(FILE_ENTRY **)e2); + + /* I. file type grouping */ + gr = ppanel_file->group; + if (gr != GROUP_NONE) { + group1 = sort_group(gr,pfe1); + group2 = sort_group(gr,pfe2); + cmp = group1 - group2; + if (cmp) + return cmp; + + /* special sorting for devices */ + if (gr == GROUP_DBCOP && (group1 == FILETYPE_BDEV || group1 == FILETYPE_CDEV) ) { + cmp = major(pfe1->devnum) - major(pfe2->devnum); + if (cmp) + return cmp; + cmp = minor(pfe1->devnum) - minor(pfe2->devnum); + if (cmp) + return cmp; + } + } + + /* II. sort order */ + switch (ppanel_file->order) { + case SORT_NAME_NUM: + cmp = num_wcscoll(SDSTR(pfe1->filew),SDSTR(pfe2->filew)); + break; + case SORT_EXT: + cmp = strcoll(pfe1->extension,pfe2->extension); + break; + case SORT_SIZE: + cmp = CMP(pfe1->size,pfe2->size); + break; + case SORT_SIZE_REV: + cmp = CMP(pfe2->size,pfe1->size); + break; + case SORT_TIME: + cmp = CMP(pfe2->mtime,pfe1->mtime); + break; + case SORT_TIME_REV: + cmp = CMP(pfe1->mtime,pfe2->mtime); + break; + case SORT_EMAN: + return revstrcmp(SDSTR(pfe1->file),SDSTR(pfe2->file)); + default: + /* SORT_NAME */ + cmp = 0; + } + if (cmp) + return cmp; + + /* III. sort by file name */ + return strcoll(SDSTR(pfe1->file),SDSTR(pfe2->file)); +} + +void +sort_files(void) +{ + if (ppanel_file->all_cnt == 0) + return; + qsort(ppanel_file->all_files,ppanel_file->all_cnt,sizeof(FILE_ENTRY *),qcmp); + file_panel_data(); +} diff --git a/src/sort.h b/src/sort.h new file mode 100644 index 0000000..e612d4f --- /dev/null +++ b/src/sort.h @@ -0,0 +1,6 @@ +extern int sort_prepare(void); +extern const char *sort_saveopt(void); +extern int sort_restoreopt(const char *); +extern void sort_files(void); +extern void cx_sort_set(void); +extern int num_wcscoll(const wchar_t *, const wchar_t *); diff --git a/src/start.c b/src/start.c new file mode 100644 index 0000000..5235ab2 --- /dev/null +++ b/src/start.c @@ -0,0 +1,271 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* umask() */ +#include /* log.h */ +#include /* puts() */ +#include /* setenv() */ +#include /* strcmp() */ +#include /* getpid() */ +#include "curses.h" /* NCURSES_MOUSE_VERSION */ + +#include "bookmarks.h" /* bm_initialize() */ +#include "cfg.h" /* cfg_initialize() */ +#include "completion.h" /* compl_initialize() */ +#include "control.h" /* control_loop() */ +#include "directory.h" /* dir_initialize() */ +#include "exec.h" /* exec_initialize() */ +#include "filepanel.h" /* files_initialize() */ +#include "help.h" /* help_initialize() */ +#include "history.h" /* hist_initialize() */ +#include "inout.h" /* curses_initialize() */ +#include "inschar.h" /* inschar_initialize() */ +#include "lang.h" /* locale_initialize() */ +#include "list.h" /* list_initialize() */ +#include "mouse.h" /* mouse_initialize() */ +#include "opt.h" /* opt_initialize() */ +#include "log.h" /* logfile_open() */ +#include "signals.h" /* signal_initialize() */ +#include "tty.h" /* tty_initialize() */ +#include "userdata.h" /* userdata_initialize() */ +#include "util.h" /* base_name() */ +#include "xterm_title.h" /* xterm_title_initialize() */ + +/* + * NOTE: ANSI/ISO C requires that all global and static variables + * are initialized to zero. CLEX relies on it. + */ +static INPUTLINE log_filt = { L"",0,0,0 }, help_filt = { L"",0,0,0 }, shared_filt = { L"",0,0,0 }; + +static EXTRA_LINE el_exit[1] = { + { 0,0,MODE_SPECIAL_RETURN,0 } +}; +static EXTRA_LINE el_bm[2] = { + { 0,L"Changes will be saved",0,cx_bm_save }, + { L"Cancel",L"Changes will be discarded",0,cx_bm_revert } +}; +static EXTRA_LINE el_cfg[3] = { + { L"Cancel",L"Changes will be discarded",MODE_SPECIAL_RETURN,0 }, + { L"Apply",L"Use the new configuration in this session", + MODE_SPECIAL_RETURN,cx_cfg_apply }, + { L"Apply+Save",L"Save the configuration to disk", + MODE_SPECIAL_RETURN,cx_cfg_apply_save } +}; +static EXTRA_LINE el_dir[2] = { + { 0,0,MODE_SPECIAL_RETURN,cx_dir_enter }, + { L"Bookmarks",0,MODE_BM,cx_dir_enter }, +}; +static EXTRA_LINE el_dir_split[1] = { + { 0,0,MODE_SPECIAL_RETURN,cx_dir_enter } +}; +static EXTRA_LINE el_group[2] = { + { 0,0,MODE_SPECIAL_RETURN,0 }, + { L"Switch to user data (alt-U)",0,MODE_USER,0 } +}; +static EXTRA_LINE el_sort[1] = { + { L"Cancel",L"Changes will be discarded",MODE_SPECIAL_RETURN,0 } +}; +static EXTRA_LINE el_user[2] = { + { 0,0,MODE_SPECIAL_RETURN,0 }, + { L"Switch to group data (alt-G)",0,MODE_GROUP,0 } +}; +#define EL_EXIT (-1 * ARRAY_SIZE(el_exit)) + +/* + * PANEL_DESC initialization: + * cnt, top, curs, min, type, norev, extra, filter, drawfn + * implicit null: help, filtering + */ +static PANEL_DESC pd_bm = { 0,0,0, + -1 * ARRAY_SIZE(el_bm),PANEL_TYPE_BM,0,el_bm,&shared_filt,draw_line_bm }; +static PANEL_DESC pd_bm_edit = { 2,0,0, + EL_EXIT,PANEL_TYPE_BM,0,el_exit,0,draw_line_bm_edit }; +static PANEL_DESC pd_cfg = { CFG_TOTAL_,0,0, + -1 * ARRAY_SIZE(el_cfg),PANEL_TYPE_CFG,0,el_cfg,0,draw_line_cfg }; +static PANEL_DESC pd_cfg_menu = { 0,0,0, + 0,PANEL_TYPE_CFG_MENU,0,0,0,draw_line_cfg_menu }; +static PANEL_DESC pd_cmp = { 1 + CMP_TOTAL_,0,0, + EL_EXIT,PANEL_TYPE_CMP,0,el_exit,0,draw_line_cmp }; +static PANEL_DESC pd_cmp_sum = { 0,0,0, + EL_EXIT,PANEL_TYPE_CMP_SUM,0,el_exit,0,draw_line_cmp_sum }; +static PANEL_DESC pd_compl = { 0,0,0, + EL_EXIT,PANEL_TYPE_COMPL,0,el_exit,&shared_filt,draw_line_compl }; +static PANEL_DESC pd_dir = { 0,0,0, + -1 * ARRAY_SIZE(el_dir),PANEL_TYPE_DIR,0,el_dir,&shared_filt,draw_line_dir }; +static PANEL_DESC pd_dir_split = { 0,0,0, + -1 * ARRAY_SIZE(el_dir_split),PANEL_TYPE_DIR_SPLIT,0,el_dir_split,0,draw_line_dir_split }; +static PANEL_DESC pd_fopt = { FOPT_TOTAL_,0,0, + EL_EXIT,PANEL_TYPE_FOPT,0,el_exit,0,draw_line_fopt }; +static PANEL_DESC pd_grp = { 0,0,0, + -1 * ARRAY_SIZE(el_group),PANEL_TYPE_GROUP,0,el_group,&shared_filt,draw_line_group }; +static PANEL_DESC pd_help = { 0,0,0, + 0,PANEL_TYPE_HELP,1,0,&help_filt,draw_line_help }; +static PANEL_DESC pd_hist = { 0,0,0, + EL_EXIT,PANEL_TYPE_HIST,0,el_exit,&shared_filt,draw_line_hist }; +static PANEL_DESC pd_log = { 0,0,0, + EL_EXIT,PANEL_TYPE_LOG,0,el_exit,&log_filt,draw_line_log }; +static PANEL_DESC pd_mainmenu = /* 22 items in this menu */ { 22,EL_EXIT,EL_EXIT, + EL_EXIT,PANEL_TYPE_MAINMENU,0,el_exit,0,draw_line_mainmenu }; +static PANEL_DESC pd_notif = { NOTIF_TOTAL_,0,0, + EL_EXIT,PANEL_TYPE_NOTIF,0,el_exit,0,draw_line_notif }; +static PANEL_DESC pd_paste = /* 15 items in this menu */ { 15,EL_EXIT,EL_EXIT, + EL_EXIT,PANEL_TYPE_PASTE,0,el_exit,0,draw_line_paste }; +static PANEL_DESC pd_preview = /* 15 items in this menu */ { 0,0,0, + 0,PANEL_TYPE_PREVIEW,0,0,0,draw_line_preview }; +static PANEL_DESC pd_sort = /* 18 items in this menu */ { 18,0,0, + EL_EXIT,PANEL_TYPE_SORT,0,el_sort,0,draw_line_sort }; +static PANEL_DESC pd_usr = { 0,0,0, + -1 * ARRAY_SIZE(el_user),PANEL_TYPE_USER,0,el_user,&shared_filt,draw_line_user }; +PANEL_DESC *panel = 0; +PANEL_BM panel_bm = { &pd_bm }; +PANEL_BM_EDIT panel_bm_edit = { &pd_bm_edit }; +PANEL_COMPL panel_compl = { &pd_compl }; +PANEL_CFG panel_cfg = { &pd_cfg }; +PANEL_CFG_MENU panel_cfg_menu = { &pd_cfg_menu }; +PANEL_CMP panel_cmp = { &pd_cmp }; +PANEL_CMP_SUM panel_cmp_sum = { &pd_cmp_sum }; +PANEL_DIR panel_dir = { &pd_dir }; +PANEL_DIR_SPLIT panel_dir_split = { &pd_dir_split }; +PANEL_FOPT panel_fopt = { &pd_fopt }; +PANEL_GROUP panel_group = { &pd_grp }; +PANEL_HELP panel_help = { &pd_help }; +PANEL_HIST panel_hist = { &pd_hist }; +PANEL_LOG panel_log = { &pd_log }; +PANEL_MENU panel_mainmenu = { &pd_mainmenu }; +PANEL_NOTIF panel_notif = { &pd_notif }; +PANEL_PASTE panel_paste = { &pd_paste }; +PANEL_PREVIEW panel_preview = { &pd_preview }; +PANEL_SORT panel_sort = { &pd_sort, GROUP_DSP, SORT_NAME_NUM, HIDE_NEVER }; +PANEL_USER panel_user = { &pd_usr }; +PANEL_FILE *ppanel_file; + +DISP_DATA disp_data; +LANG_DATA lang_data; +USER_DATA user_data; +CLEX_DATA clex_data; + +MOUSE_INPUT minp; +KBD_INPUT kinp; + +TEXTLINE *textline = 0, line_cmd, line_tmp, line_dir, line_inschar; +CODE next_mode; +volatile FLAG ctrlc_flag; +const void *pcfg[CFG_TOTAL_]; + +int +main(int argc, char *argv[]) +{ + FLAG help = 0, version = 0; + int i; + const char *arg; + + locale_initialize(); /* before writing the first text message */ + + /* check command line arguments */ + for (i = 1; i < argc; i++) { + arg = argv[i]; + if (strcmp(arg,"--help") == 0) + help = 1; + else if (strcmp(arg,"--version") == 0) + version = 1; + else if (strcmp(arg,"--log") == 0) { + if (++i == argc) + err_exit("--log option requires an argument (filename)"); + logfile_open(argv[i]); + } + else { + msgout(MSG_W,"Unrecognized option '%s'",arg); + msgout(MSG_i,"Try '%s --help' for more information",base_name(argv[0])); + err_exit("Incorrect usage"); + } + } + if (version) + puts( + "\nCLEX File Manager " VERSION "\n" + " compiled with POSIX job control: " +#ifdef _POSIX_JOB_CONTROL + "yes\n" +#else + "no\n" +#endif + " mouse interface: " +#if NCURSES_MOUSE_VERSION >= 2 + "ncurses\n" +#else + "xterm\n" +#endif + "\nCopyright (C) 2001-2022 Vlado Potisk" + "\n\n" + "This is free software distributed without any warranty.\n" + "See the GNU General Public License for more details.\n" + "\n" + "Project homepage is https://github.com/xitop/clex"); + if (help) + printf( + "\nUsage: %s [OPTIONS]\n\n" + " --version display program version and exit\n" + " --help display this help and exit\n" + " --log logfile append log information to logfile\n", + base_name(argv[0])); + if (help || version) + exit(EXIT_SUCCESS); /* no cleanup necessary at this stage */ + + /* real start */ + puts("\n\n\nStarting CLEX " VERSION "\n"); + + /* initialize program data */ + clex_data.umask = umask(0777); + umask(clex_data.umask); + clex_data.pid = getpid(); + sprintf(clex_data.pidstr,"%d",(int)clex_data.pid); +#ifdef HAVE_SETENV + setenv("CLEX",clex_data.pidstr,1); /* let's have $CLEX (just so) */ +#endif + + /* low-level stuff except jc_initialize() */ + tty_initialize(); + signal_initialize(); + + /* read the configuration and options asap */ + userdata_initialize(); /* required by cfg_initialize */ + cfg_initialize(); + opt_initialize(); + bm_initialize(); + + /* initialize the rest, the order is not important (except when noted) */ + compl_initialize(); + dir_initialize(); + files_initialize(); + exec_initialize(); /* after files_initialize (if PROMPT contains $w) */ + help_initialize(); + hist_initialize(); + inschar_initialize(); + list_initialize(); + + /* user interface */ + curses_initialize(); + xterm_title_initialize(); /* after curses_initialize */ + mouse_initialize(); /* after curses_initialize */ + cx_version(); + + /* job control initialization is done last in order to provide enough time + for the parent process to finish its job control tasks */ + jc_initialize(); + control_loop(MODE_FILE); + + /* NOTREACHED */ + return 0; +} diff --git a/src/tty.c b/src/tty.c new file mode 100644 index 0000000..4fce42e --- /dev/null +++ b/src/tty.c @@ -0,0 +1,210 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* tolower() */ +#include /* errno */ +#include /* fcntl */ +#include /* fputs() */ +#include /* SIGTTIN */ +#include /* struct termios */ +#include /* STDIN_FILENO */ + +#include "tty.h" + +#include "control.h" /* err_exit() */ + +static struct termios *p_raw, *p_text = 0, *p_save = 0; + +#ifdef _POSIX_JOB_CONTROL +static pid_t save_pgid = 0; +#endif + +void +jc_initialize(void) +{ +#ifdef _POSIX_JOB_CONTROL + struct sigaction act; + + /* Wait until we are in the foreground */ + while (tcgetpgrp(STDIN_FILENO) != (save_pgid = getpgrp())) + kill(-save_pgid,SIGTTIN); + + /* ignore job control signals */ + act.sa_handler = SIG_IGN; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGTSTP,&act,0); + sigaction(SIGTTIN,&act,0); + sigaction(SIGTTOU,&act,0); + + /* put CLEX into its own process group */ + setpgid(clex_data.pid,clex_data.pid); + /* make it the foreground process group */ + tcsetpgrp(STDIN_FILENO,clex_data.pid); +#endif +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +jc_reset(void) +{ +#ifdef _POSIX_JOB_CONTROL + if (save_pgid) + tcsetpgrp(STDIN_FILENO,save_pgid); +#endif +} + +void +tty_initialize(void) +{ + static struct termios text, raw; + + if (!isatty(STDIN_FILENO)) + err_exit("This is an interactive program, but the standard input is not a terminal"); + + if (tcgetattr(STDIN_FILENO,&text) < 0) + err_exit("Cannot read the terminal parameters"); + + raw = text; /* struct copy */ + raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + p_text = &text; + p_raw = &raw; +} + +void +tty_save() +{ + static struct termios save; + + /* errors are silently ignored */ + p_save = tcgetattr(STDIN_FILENO,&save) == 0 ? &save : 0; +} + +void +tty_restore(void) +{ + if (p_save) + tcsetattr(STDIN_FILENO,TCSAFLUSH,p_save); +} + +/* + * make sure interrupt key is ctrl-C + * usage: tty_save(); tty_ctrlc(); + * install-SIGINT-handler + * do-stuff + * disable-SIGINT + * tty_restore(); + */ +void +tty_ctrlc() +{ + struct termios ctrlc; + + if (p_save) { + ctrlc = *p_save; + ctrlc.c_cc[VINTR] = CH_CTRL('C'); + tcsetattr(STDIN_FILENO,TCSAFLUSH,&ctrlc); + } +} + +/* noncanonical, no echo */ +void +tty_setraw(void) +{ + if (p_raw) + tcsetattr(STDIN_FILENO,TCSAFLUSH,p_raw); +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +tty_reset(void) +{ + if (p_text) + tcsetattr(STDIN_FILENO,TCSAFLUSH,p_text); +} + +static int +tty_getchar(void) +{ + int in, flags, loops = 0; + + while ((in = getchar()) == EOF) { + if (errno == EINTR) + continue; + if (errno == EAGAIN) { + flags = fcntl(STDIN_FILENO,F_GETFL); + if ((flags & O_NONBLOCK) == O_NONBLOCK) { + /* clear the non-blocking flag */ + fcntl(STDIN_FILENO,F_SETFL,flags & ~O_NONBLOCK); + continue; + } + } + if (++loops >= 3) + err_exit("Cannot read from standard input"); + } + return in; +} + +void +tty_press_enter(void) +{ + int in; + + if (disp_data.noenter) { + fputs("Returning to CLEX.",stdout); + disp_data.noenter = 0; + } + else { + fputs("Press to continue. ",stdout); + fflush(stdout); + tty_setraw(); + while ((in = tty_getchar()) != '\n' && in != '\r') + ; + tty_reset(); + } + + puts("\n----------------------------------------------"); + fflush(stdout); + disp_data.wait = 0; +} + +/* + * - if 'yeschar' is set, msg is a yes/no question where 'yeschar' (in lower or upper case) + * means confirmation ('yeschar' parameter itself should be entered in lower case) + */ +int +tty_dialog(int yeschar, const char *msg) +{ + int code; + + putchar('\n'); + fputs(msg,stdout); + if (yeschar) + fprintf(stdout," (%c = %s) ",yeschar,"yes"); + fflush(stdout); + tty_setraw(); + code = tolower(tty_getchar()); + tty_reset(); + if (yeschar) { + code = code == yeschar; + puts(code ? "yes" : "no"); + } + putchar('\n'); + fflush(stdout); + return code; +} diff --git a/src/tty.h b/src/tty.h new file mode 100644 index 0000000..1f1e102 --- /dev/null +++ b/src/tty.h @@ -0,0 +1,10 @@ +extern void tty_initialize(void); +extern void jc_initialize(void); +extern void tty_ctrlc(void); +extern void tty_setraw(void); +extern void tty_reset(void); +extern void tty_save(void); +extern void tty_restore(void); +extern void jc_reset(void); +extern void tty_press_enter(void); +extern int tty_dialog(int, const char *); diff --git a/src/undo.c b/src/undo.c new file mode 100644 index 0000000..73d5e47 --- /dev/null +++ b/src/undo.c @@ -0,0 +1,206 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* log.h */ + +#include "undo.h" + +#include "edit.h" /* edit_update() */ +#include "log.h" /* msgout() */ + +static TEXTLINE *current; +static USTRINGW undo_line = UNULL; +static int undo_size, undo_curs, undo_offset; +static FLAG disable = 1; + /* disable undo mechanism while performing undo operations */ +static EDIT_OP this_op; /* description of the current operation */ + +/* clear the undo history */ +void +undo_reset(void) +{ + if (textline == 0) + return; + + textline->last_op.code = OP_NONE; + textline->undo_levels = textline->redo_levels = 0; + disable = 1; +} + +/* + * check if 'longstr' is the same as 'shortstr' would be + * with 'len' chars inserted at position 'pos' + */ +static int +cmp_strings(const wchar_t *shortstr, const wchar_t *longstr, int pos, int len) +{ + return pos >= 0 && len >=0 + && (pos == 0 || wcsncmp(shortstr,longstr,pos) == 0) + && wcscmp(shortstr + pos,longstr + pos + len) == 0; +} + +/* which edit operation was this one ? */ +static void +tell_edit_op(void) +{ + int pos, diff; + const wchar_t *before, *after; + + before = USTR(undo_line); + after = USTR(textline->line); + diff = textline->size - undo_size; + if (diff > 0) { + if (cmp_strings(before,after,pos = textline->curs - diff,diff)) { + this_op.code = OP_INS; + this_op.pos = pos; + this_op.len = diff; + return; + } + } + else if (diff == 0) { + if (wcscmp(before,after) == 0) { + this_op.code = OP_NONE; + return; + } + } + else { + if (cmp_strings(after,before,pos = textline->curs,diff = -diff)) { + this_op.code = OP_DEL; + this_op.pos = pos; + this_op.len = diff; + return; + } + } + + this_op.code = OP_CHANGE; + this_op.pos = this_op.len = 0; +} + +/* make a copy of 'textline' before an edit operation ... */ +void +undo_before(void) +{ + if (textline == 0) + return; + + disable = 0; + current = textline; + usw_copy(&undo_line,USTR(textline->line)); + undo_size = textline->size; + undo_curs = textline->curs; + undo_offset = textline->offset; +} + +/* ... and now see what happened with it after the operation */ +#define MERGE_MAX 30 +void +undo_after(void) +{ + int idx, total, delta; + + if (TSET(disable) || textline == 0 || textline != current) + return; + + tell_edit_op(); + if (this_op.code == OP_NONE) + return; + + textline->redo_levels = 0; + + /* + * two operations of the same type at the same position + * can be merged into a single operation + */ + total = this_op.len + textline->last_op.len; + delta = this_op.pos - textline->last_op.pos; + if (( + this_op.code == OP_INS && textline->last_op.code == OP_INS + && delta == textline->last_op.len && total < MERGE_MAX + ) || ( + this_op.code == OP_DEL && textline->last_op.code == OP_DEL + && (delta == 0 || (delta == -1 && this_op.len == 1)) && total < MERGE_MAX + )) { + /* merge */ + if (this_op.code == OP_DEL) + textline->last_op.pos = this_op.pos; + textline->last_op.len = total; + return; + } + + textline->last_op = this_op; /* struct copy */ + + idx = (textline->undo_base + textline->undo_levels) % UNDO_LEVELS; + if (textline->undo_levels < UNDO_LEVELS) + textline->undo_levels++; + else + textline->undo_base = (textline->undo_base + 1) % UNDO_LEVELS; + + usw_xchg(&textline->undo[idx].save_line,&undo_line); + textline->undo[idx].save_size = undo_size; + textline->undo[idx].save_curs = undo_curs; + textline->undo[idx].save_offset = undo_offset; +} + +/* op: nonzero = undo, 0 = redo */ +static void +undo_redo(int op) +{ + int idx; + + if (textline == 0) + return; + if (op) { + if (textline->undo_levels == 0) { + msgout(MSG_i,"undo not possible"); + return; + } + idx = --textline->undo_levels; + textline->redo_levels++; + } + else { + if (textline->redo_levels == 0) { + msgout(MSG_i,"redo not possible"); + return; + } + idx = textline->undo_levels++; + textline->redo_levels--; + } + idx = (textline->undo_base + idx) % UNDO_LEVELS; + + usw_xchg(&textline->line,&textline->undo[idx].save_line); + textline->size = textline->undo[idx].save_size; + textline->curs = textline->undo[idx].save_curs; + textline->offset = textline->undo[idx].save_offset; + textline->undo[idx].save_size = undo_size; + textline->undo[idx].save_curs = undo_curs; + textline->undo[idx].save_offset = undo_offset; + + edit_update(); + textline->last_op.code = OP_CHANGE; + disable = 1; +} + +void +cx_undo(void) +{ + undo_redo(1); +} + +void +cx_redo(void) +{ + undo_redo(0); +} diff --git a/src/undo.h b/src/undo.h new file mode 100644 index 0000000..6e5fa9b --- /dev/null +++ b/src/undo.h @@ -0,0 +1,5 @@ +extern void undo_reset(void); +extern void undo_before(void); +extern void undo_after(void); +extern void cx_undo(void); +extern void cx_redo(void); diff --git a/src/userdata.c b/src/userdata.c new file mode 100644 index 0000000..582d5d0 --- /dev/null +++ b/src/userdata.c @@ -0,0 +1,761 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* + * Routines in userdata.c mostly provide access to data stored + * in /etc/passwd and /etc/group. All required data is read + * into memory to speed up lookups. The data returned by + * lookup_xxx() is valid only until next data refresh, the + * caller must copy the returned string value if necessary. + */ + +#include "clexheaders.h" + +#include /* stat() */ +#ifdef HAVE_UNAME +# include /* uname() */ +#endif +#include /* time() */ +#include /* getgrent() */ +#include /* getpwent() */ +#include /* log.h */ +#include /* sprintf() */ +#include /* qsort() */ +#include /* strlen() */ +#include /* stat() */ + +#include "userdata.h" + +#include "edit.h" /* edit_insertchar() */ +#include "filter.h" /* cx_filter() */ +#include "log.h" /* msgout() */ +#include "match.h" /* match_substr() */ +#include "mbwstring.h" /* convert2w() */ +#include "util.h" /* emalloc() */ + +/* + * cached user(group) records are re-read when: + * - /etc/passwd (/etc/group) file changes, or + * - the cache expires (EXPIRATION in seconds) to allow + * changes e.g. in NIS to get detected, or + * - explicitly requested + */ +#define EXPIRATION 300 /* 5 minutes */ + +typedef struct pwdata { + struct pwdata *next; /* tmp pointer */ + SDSTRINGW login; + SDSTRINGW homedir; + SDSTRINGW gecos; + uid_t uid; +} PWDATA; + +typedef struct grdata { + struct grdata *next; /* tmp pointer */ + SDSTRINGW group; + gid_t gid; +} GRDATA; + +typedef struct { + time_t timestamp; /* when the data was obtained, or 0 */ + dev_t device; /* device/inode for /etc/passwd */ + ino_t inode; + int cnt; /* # of entries */ + PWDATA **by_name; /* sorted by name (for binary search, ignoring locale) */ + PWDATA **by_uid; /* sorted by uid */ + PWDATA *ll; /* linked list, unsorted */ +} USERDATA; + +typedef struct { + time_t timestamp; /* when the data was obtained, or 0 */ + dev_t device; /* device/inode for /etc/group */ + ino_t inode; + int cnt; /* # of entries */ + GRDATA **by_name; /* sorted by name (for binary search, ignoring locale) */ + GRDATA **by_gid; /* sorted by gid */ + GRDATA *ll; /* linked list, unsorted */ +} GROUPDATA; + +static USERDATA utable; +static GROUPDATA gtable; + +static time_t now; + +static struct { + const wchar_t *str; + size_t len; + int index; +} ufind, gfind; /* used by user- and groupname_find() */ + +static int +qcmp_name(const void *e1, const void *e2) +{ + return wcscmp( /* not wcscoll() */ + SDSTR((*(PWDATA **)e1)->login), + SDSTR((*(PWDATA **)e2)->login)); +} + +static int +qcmp_uid(const void *e1, const void *e2) +{ + return CMP((*(PWDATA **)e1)->uid,(*(PWDATA **)e2)->uid); +} + +static void +read_utable(void) +{ + int i, cnt; + PWDATA *ud = 0 /* prevent compiler warning */, *old; + struct passwd *pw; + static FLAG err = 0; + + utable.timestamp = now; + setpwent(); + for (old = utable.ll, cnt = 0; (pw = getpwent()); cnt++) { + if (old) { + /* use the old PWDATA struct */ + ud = old; + old = ud->next; + } + else { + /* create a new one */ + ud = emalloc(sizeof(PWDATA)); + ud->next = utable.ll; + utable.ll = ud; + SD_INIT(ud->login); + SD_INIT(ud->homedir); + SD_INIT(ud->gecos); + } + ud->uid = pw->pw_uid; + sdw_copy(&ud->login,convert2w(pw->pw_name)); + sdw_copy(&ud->homedir,convert2w(pw->pw_dir)); + sdw_copy(&ud->gecos,convert2w(pw->pw_gecos)); + } + endpwent(); + + /* free unused PWDATA structs */ + if (cnt > 0) + for (; old; old = ud->next) { + ud->next = old->next; + sdw_reset(&old->login); + sdw_reset(&old->homedir); + sdw_reset(&old->gecos); + free(old); + } + + if (utable.cnt != cnt && utable.by_name) { + free(utable.by_name); + free(utable.by_uid); + utable.by_name = utable.by_uid = 0; + } + + /* I was told using errno for error detection with getpwent() is not portable */ + if ((utable.cnt = cnt) == 0) { + utable.timestamp = 0; + if (!TSET(err)) + msgout(MSG_W,"USER ACCOUNTS: Cannot obtain user account data"); + return; + } + if (TCLR(err)) + msgout(MSG_W,"USER ACCOUNTS: User account data is now available"); + + /* linked list -> two sorted arrays */ + if (utable.by_name == 0) { + utable.by_name = emalloc(cnt * sizeof(PWDATA *)); + utable.by_uid = emalloc(cnt * sizeof(PWDATA *)); + for (ud = utable.ll, i = 0; i < cnt; i++, ud = ud->next) + utable.by_name[i] = utable.by_uid[i] = ud; + } + qsort(utable.by_name,cnt,sizeof(PWDATA *),qcmp_name); + qsort(utable.by_uid ,cnt,sizeof(PWDATA *),qcmp_uid); +} + +static int +qcmp_gname(const void *e1, const void *e2) +{ + return wcscmp( /* not wcscoll() */ + SDSTR((*(GRDATA **)e1)->group), + SDSTR((*(GRDATA **)e2)->group)); +} + +static int +qcmp_gid(const void *e1, const void *e2) +{ + return CMP((*(GRDATA **)e1)->gid,(*(GRDATA **)e2)->gid); +} + +static void +read_gtable(void) +{ + int i, cnt; + GRDATA *gd = 0 /* prevent compiler warning */, *old; + struct group *gr; + static FLAG err = 0; + + gtable.timestamp = now; + setgrent(); + for (old = gtable.ll, cnt = 0; (gr = getgrent()); cnt++) { + if (old) { + /* use the old GRDATA struct */ + gd = old; + old = gd->next; + } + else { + /* create a new one */ + gd = emalloc(sizeof(GRDATA)); + gd->next = gtable.ll; + gtable.ll = gd; + SD_INIT(gd->group); + } + gd->gid = gr->gr_gid; + sdw_copy(&gd->group,convert2w(gr->gr_name)); + } + endgrent(); + + /* free unused GRDATA structs */ + if (cnt > 0) + for (; old; old = gd->next) { + gd->next = old->next; + sdw_reset(&old->group); + free(old); + } + + if (gtable.cnt != cnt && gtable.by_name) { + free(gtable.by_name); + free(gtable.by_gid); + gtable.by_name = gtable.by_gid = 0; + } + + if ((gtable.cnt = cnt) == 0) { + gtable.timestamp = 0; + if (!TSET(err)) + msgout(MSG_W,"USER ACCOUNTS: Cannot obtain user group data"); + return; + } + if (TCLR(err)) + msgout(MSG_W,"USER ACCOUNTS: User group data is now available"); + + /* linked list -> sorted array */ + if (gtable.by_name == 0) { + gtable.by_name = emalloc(cnt * sizeof(GRDATA *)); + gtable.by_gid = emalloc(cnt * sizeof(GRDATA *)); + for (gd = gtable.ll, i = 0; i < cnt; i++, gd = gd->next) + gtable.by_name[i] = gtable.by_gid[i] = gd; + } + qsort(gtable.by_name,cnt,sizeof(GRDATA *),qcmp_gname); + qsort(gtable.by_gid ,cnt,sizeof(GRDATA *),qcmp_gid); +} + +static int +shelltype(const char *shell) +{ + size_t len; + + len = strlen(shell); + if (len >= 2 && shell[len - 2] == 's' && shell[len - 1] == 'h') + return (len >= 3 && shell[len - 3] == 'c') ? SHELL_CSH : SHELL_SH; + return SHELL_OTHER; +} + +void +userdata_initialize(void) +{ + static char uidstr[24]; + static SDSTRING host = SDNULL("localhost"); + struct passwd *pw; + const char *name, *xdg; + uid_t myuid; + +#ifdef HAVE_UNAME + char ch, *pch, *pdot; + FLAG ip; + struct utsname ut; + + uname(&ut); + sd_copy(&host,ut.nodename); + + /* strip the domain part */ + for (ip = 1, pdot = 0, pch = SDSTR(host); (ch = *pch); pch++) { + if (ch == '.') { + if (pdot == 0) + pdot = pch; + } + else if (ch < '0' || ch > '9') + ip = 0; /* this is a name and not an IP address */ + if (!ip && pdot != 0) { + *pdot = '\0'; + break; + } + } +#endif + user_data.host = SDSTR(host); + user_data.hostw = ewcsdup(convert2w(user_data.host)); + + user_data.nowrite = 0; + + msgout(MSG_AUDIT,"CLEX version: \""VERSION "\""); + msgout(MSG_HEADING,"Examining data of your account"); + + myuid = getuid(); + if ((pw = getpwuid(myuid)) == 0) { + sprintf(uidstr,"%d",(int)myuid); + msgout(MSG_W,"Cannot find your account (UID=%s)" + " in the user database",uidstr); + sprintf(uidstr,"UID_%d",(int)myuid); + user_data.login = uidstr; + user_data.nowrite = 1; + } + else + user_data.login = estrdup(pw->pw_name); + user_data.loginw = ewcsdup(convert2w(user_data.login)); + + if (checkabs(name = getenv("SHELL"))) + user_data.shell = name; + else if (pw && checkabs(pw->pw_shell)) + user_data.shell = estrdup(pw->pw_shell); + else { + msgout(MSG_W,"Cannot obtain the name of your shell program; using \"/bin/sh\""); + user_data.shell = "/bin/sh"; + } + name = base_name(user_data.shell); + user_data.shellw = ewcsdup(convert2w(name)); + user_data.shelltype = shelltype(name); + msgout(MSG_AUDIT,"Command interpreter: \"%s\"",user_data.shell); + + if (checkabs(name = getenv("HOME"))) { + user_data.homedir = name; + if (strcmp(name,"/") == 0) { + if (pw && *pw->pw_dir && strcmp(pw->pw_dir,"/") != 0) { + msgout(MSG_W,"Your home directory is the root directory, " + "but according to the password file it should be \"%s\"",pw->pw_dir); + user_data.nowrite = 1; + } + } + } + else if (pw && checkabs(pw->pw_dir)) + user_data.homedir = estrdup(pw->pw_dir); + else { + msgout(MSG_W,"Cannot obtain the name of your home directory; using \"/\""); + user_data.homedir = "/"; + user_data.nowrite = 1; + } + + if (!user_data.nowrite && strcmp(user_data.homedir,"/") == 0 && myuid != 0) { + msgout(MSG_W,"Your home directory is the root directory, but you are not root"); + user_data.nowrite = 1; + } + + user_data.homedirw = ewcsdup(convert2w(user_data.homedir)); + msgout(MSG_DEBUG,"Home directory: \"%s\"",user_data.homedir); + + if (user_data.nowrite) + msgout(MSG_W,"Due to the problem reported above CLEX will not save any " + "data to disk. This includes configuration, options and bookmarks"); + + user_data.isroot = geteuid() == 0; /* 0 or 1 */; + + xdg = getenv("XDG_CONFIG_HOME"); + if (xdg && *xdg) { + pathname_set_directory(xdg); + user_data.subdir = estrdup(pathname_join("clex")); + } + else { + pathname_set_directory(user_data.homedir); + user_data.subdir = estrdup(pathname_join(".config/clex")); + } + msgout(MSG_DEBUG,"Configuration directory: \"%s\"",user_data.subdir); + pathname_set_directory(user_data.subdir); + user_data.file_cfg = estrdup(pathname_join("config")); + user_data.file_opt = estrdup(pathname_join("options")); + user_data.file_bm = estrdup(pathname_join("bookmarks")); + + msgout(MSG_HEADING,0); +} + +void +userdata_expire(void) +{ + utable.timestamp = gtable.timestamp = 0; +} + +/* returns 1 if data was re-read, 0 if unchanged */ +int +userdata_refresh(void) +{ + FLAG stat_ok; + int reloaded; + struct stat st; + + reloaded = 0; + + now = time(0); + stat_ok = stat("/etc/passwd",&st) == 0; + if (!stat_ok || st.st_mtime >= utable.timestamp + || st.st_dev != utable.device || st.st_ino != utable.inode + || now > utable.timestamp + EXPIRATION ) { + read_utable(); + utable.device = stat_ok ? st.st_dev : 0; + utable.inode = stat_ok ? st.st_ino : 0; + reloaded = 1; + } + + stat_ok = stat("/etc/group",&st) == 0; + if (!stat_ok || st.st_mtime >= gtable.timestamp + || st.st_dev != gtable.device || st.st_ino != gtable.inode + || now > gtable.timestamp + EXPIRATION) { + read_gtable(); + gtable.device = stat_ok ? st.st_dev : 0; + gtable.inode = stat_ok ? st.st_ino : 0; + reloaded = 1; + } + + return reloaded; +} + +/* simple binary search algorithm */ +#define BIN_SEARCH(COUNT,CMPFUNC,RETVAL) \ + { \ + int min, med, max, cmp; \ + for (min = 0, max = COUNT - 1; min <= max; ) { \ + med = (min + max) / 2; \ + cmp = CMPFUNC; \ + if (cmp == 0) \ + return RETVAL; \ + if (cmp < 0) \ + max = med - 1; \ + else \ + min = med + 1; \ + } \ + return 0; \ + } +/* end of BIN_SEARCH() macro */ + +/* numeric uid -> login name */ +const wchar_t * +lookup_login(uid_t uid) +{ + BIN_SEARCH(utable.cnt, + CMP(uid,utable.by_uid[med]->uid), + SDSTR(utable.by_uid[med]->login)) +} + +/* numeric gid -> group name */ +const wchar_t * +lookup_group(gid_t gid) +{ + BIN_SEARCH(gtable.cnt, + CMP(gid,gtable.by_gid[med]->gid), + SDSTR(gtable.by_gid[med]->group)) +} + +static const wchar_t * +lookup_homedir(const wchar_t *user, size_t len) +{ + static SDSTRINGW username = SDNULL(L""); + + if (len == 0) + return user_data.homedirw; + + sdw_copyn(&username,user,len); + BIN_SEARCH(utable.cnt, + wcscmp(SDSTR(username),SDSTR(utable.by_name[med]->login)), + SDSTR(utable.by_name[med]->homedir)) +} + +/* + * check if 'dir' is of the form ~username/dir with a valid username + * typical usage: + * tilde = is_dir_tilde(dir); + * ... dequote dir ... + * if (tilde) dir = dir_tilde(dir); + * note: without dequoting is this sufficient: + * tilde = *dir == '~'; + */ +int +is_dir_tilde(const wchar_t *dir) +{ + size_t i; + + if (*dir != L'~') + return 0; + + for (i = 1; dir[i] != L'\0' && dir[i] != L'/'; i++) + ; + return lookup_homedir(dir + 1,i - 1) != 0; +} + +/* + * dir_tilde() function performs tilde substitution. It understands + * ~user/dir notation and transforms it to proper directory name. + * The result of the substitution (if performed) is stored in + * a static buffer that might get overwritten by successive calls. + */ +const wchar_t * +dir_tilde(const wchar_t *dir) +{ + size_t i; + const wchar_t *home; + static USTRINGW buff = UNULL; + + if (*dir != L'~') + return dir; + + for (i = 1; dir[i] != L'\0' && dir[i] != L'/'; i++) + ; + home = lookup_homedir(dir + 1,i - 1); + if (home == 0) + return dir; /* no such user */ + + usw_cat(&buff,home,dir + i,(wchar_t *)0); + return USTR(buff); +} + +/* + * Following two functions implement username completion. First + * username_find_init() is called to initialize the search, thereafter + * each call to username_find() returns one matching entry. + */ +void +username_find_init(const wchar_t *str, size_t len) +{ + int min, med, max, cmp; + + ufind.str = str; + ufind.len = len; + + if (len == 0) { + ufind.index = 0; + return; + } + + for (min = 0, max = utable.cnt - 1; min <= max; ) { + med = (min + max) / 2; + cmp = wcsncmp(str,SDSTR(utable.by_name[med]->login),len); + if (cmp == 0) { + /* + * the binary search algorithm is slightly altered here, + * multiple matches are possible, we need the first one + */ + if (min == max) { + ufind.index = med; + return; + } + max = med; + } + else if (cmp < 0) + max = med - 1; + else + min = med + 1; + } + + ufind.index = utable.cnt; +} + +const wchar_t * +username_find(const wchar_t **pgecos) +{ + const wchar_t *login, *gecos; + + if (ufind.index >= utable.cnt) + return 0; + login = SDSTR(utable.by_name[ufind.index]->login); + if (ufind.len && wcsncmp(ufind.str,login,ufind.len)) + return 0; + if (pgecos) { + gecos = SDSTR(utable.by_name[ufind.index]->gecos); + *pgecos = *gecos == L'\0' ? 0 : gecos; + } + ufind.index++; + return login; +} + +/* the same find functions() for groups */ +void +groupname_find_init(const wchar_t *str, size_t len) +{ + int min, med, max, cmp; + + gfind.str = str; + gfind.len = len; + + if (len == 0) { + gfind.index = 0; + return; + } + + for (min = 0, max = gtable.cnt - 1; min <= max; ) { + med = (min + max) / 2; + cmp = wcsncmp(str,SDSTR(gtable.by_name[med]->group),len); + if (cmp == 0) { + /* + * the binary search algorithm is slightly altered here, + * multiple matches are possible, we need the first one + */ + if (min == max) { + gfind.index = med; + return; + } + max = med; + } + else if (cmp < 0) + max = med - 1; + else + min = med + 1; + } + + gfind.index = gtable.cnt; +} + +const wchar_t * +groupname_find(void) +{ + const wchar_t *group; + + if (gfind.index >= gtable.cnt) + return 0; + group = SDSTR(gtable.by_name[gfind.index]->group); + if (gfind.len && wcsncmp(gfind.str,group,gfind.len)) + return 0; + gfind.index++; + return group; +} + +void +user_panel_data(void) +{ + int i, j; + size_t len; + const wchar_t *login, *gecos; + uid_t curs; + + curs = VALID_CURSOR(panel_user.pd) ? panel_user.users[panel_user.pd->curs].uid : 0; + if (panel_user.pd->filtering) + match_substr_set(panel_user.pd->filter->line); + + panel_user.maxlen = 0; + for (i = j = 0; i < utable.cnt; i++) { + if (curs == utable.by_uid[i]->uid) + panel_user.pd->curs = j; + login = SDSTR(utable.by_uid[i]->login); + gecos = SDSTR(utable.by_uid[i]->gecos); + if (panel_user.pd->filtering && !match_substr(login) && !match_substr(gecos)) + continue; + panel_user.users[j].uid = utable.by_uid[i]->uid; + panel_user.users[j].login = login; + len = wcslen(login); + if (len > panel_user.maxlen) + panel_user.maxlen = len; + panel_user.users[j++].gecos = gecos; + } + + panel_user.pd->cnt = j; +} + +int +user_prepare(void) +{ + if (utable.cnt > panel_user.usr_alloc) { + efree(panel_user.users); + panel_user.usr_alloc = utable.cnt; + panel_user.users = emalloc(panel_user.usr_alloc * sizeof(USER_ENTRY)); + } + + panel_user.pd->filtering = 0; + panel_user.pd->curs = -1; + user_panel_data(); + panel_user.pd->top = panel_user.pd->min; + panel_user.pd->curs = 0; + + panel = panel_user.pd; + textline = &line_cmd; + + return 0; +} + + +void +cx_user_paste(void) +{ + edit_nu_insertstr(panel_user.users[panel_user.pd->curs].login,QUOT_NORMAL); + edit_insertchar(' '); + if (panel->filtering == 1) + cx_filter(); +} + +void +cx_user_mouse(void) +{ + if (MI_PASTE) + cx_user_paste(); +} + +void +group_panel_data(void) +{ + int i, j; + const wchar_t *group; + gid_t curs; + + curs = VALID_CURSOR(panel_group.pd) ? panel_group.groups[panel_group.pd->curs].gid : 0; + if (panel_group.pd->filtering) + match_substr_set(panel_group.pd->filter->line); + + for (i = j = 0; i < gtable.cnt; i++) { + if (curs == gtable.by_gid[i]->gid) + panel_group.pd->curs = j; + group = SDSTR(gtable.by_gid[i]->group); + if (panel_group.pd->filtering && !match_substr(group)) + continue; + panel_group.groups[j].gid = gtable.by_gid[i]->gid; + panel_group.groups[j++].group = group; + } + + panel_group.pd->cnt = j; +} + +int +group_prepare(void) +{ + if (gtable.cnt > panel_group.grp_alloc) { + efree(panel_group.groups); + panel_group.grp_alloc = gtable.cnt; + panel_group.groups = emalloc(panel_group.grp_alloc * sizeof(GROUP_ENTRY)); + } + + panel_group.pd->filtering = 0; + panel_group.pd->curs = -1; + group_panel_data(); + panel_group.pd->top = panel_group.pd->min; + panel_group.pd->curs = 0; + panel = panel_group.pd; + textline = &line_cmd; + + return 0; +} + +void +cx_group_paste(void) +{ + edit_nu_insertstr(panel_group.groups[panel_group.pd->curs].group,QUOT_NORMAL); + edit_insertchar(' '); + if (panel->filtering == 1) + cx_filter(); +} + +void +cx_group_mouse(void) +{ + if (MI_PASTE) + cx_group_paste(); +} diff --git a/src/userdata.h b/src/userdata.h new file mode 100644 index 0000000..a573aa6 --- /dev/null +++ b/src/userdata.h @@ -0,0 +1,19 @@ +extern void userdata_initialize(void); +extern void userdata_expire(void); +extern int userdata_refresh(void); +extern const wchar_t *lookup_login(uid_t); +extern const wchar_t *lookup_group(gid_t); +extern void username_find_init(const wchar_t *, size_t); +extern const wchar_t *username_find(const wchar_t **); +extern void groupname_find_init(const wchar_t *, size_t); +extern const wchar_t *groupname_find(void); +extern int is_dir_tilde(const wchar_t *); +extern const wchar_t *dir_tilde(const wchar_t *); +extern int user_prepare(void); +extern void user_panel_data(void); +extern int group_prepare(void); +extern void group_panel_data(void); +extern void cx_user_paste(void); +extern void cx_user_mouse(void); +extern void cx_group_paste(void); +extern void cx_group_mouse(void); diff --git a/src/ustring.c b/src/ustring.c new file mode 100644 index 0000000..5121949 --- /dev/null +++ b/src/ustring.c @@ -0,0 +1,271 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* va_list */ +#include /* free() */ +#include /* strlen() */ + +#include "util.h" /* emalloc() */ + +/* + * The USTRING structure (defined in ustring.h) can store a string + * of unlimited length. The memory is allocated dynamically. + * + * - to initialize (to NULL ptr value) before first use: + * - static and global variables are initialized by default, + * but if you prefer explicit initialization: + * static USTRING us = UNULL; + * which is equivalent to + * static USTRING us = { 0, 0 }; + * - dynamic variables are initialized this way: + * US_INIT(ustring); + * - to re-initialize, e.g. before deallocating dynamic USTRING: + * us_reset(); + * - to store a string: (us_copy accepts also a null ptr) + * us_copy(); + * or + * us_copyn(); + * - to retrieve a string: + * USTR(us) + * or + * PUSTR(pus) + * - to edit stored name: + * a) allocate enough memory with us_setsize() or us_resize() + * b) edit the string starting at USTR() location + * + * WARNING: US_INIT, USTR, and PUSTR are macros + */ + +/* these are tunable parameters */ +/* ALLOC_UNIT in ustring.h */ +#define MINIMUM_FREE (4 * ALLOC_UNIT) + +/* + * SHOULD_CHANGE_ALLOC() is true if + * 1) we need more memory, or + * 2) we can free considerable amount of memory (MINIMUM_FREE) + */ +#define SHOULD_CHANGE_ALLOC(RQ) \ + (pustr->USalloc < RQ || pustr->USalloc >= RQ + MINIMUM_FREE) + +/* memory is allocated in chunks to prevent excessive resizing */ +#define ROUND_ALLOC(RQ) \ + ((1 + (RQ - 1) / ALLOC_UNIT) * ALLOC_UNIT) +/* note that ROUND_ALLOC(0) is ALLOC_UNIT and not 0 */ + +/* clear the data and free the memory */ +void +us_reset(USTRING *pustr) +{ + if (pustr->USalloc) { + free(pustr->USstr); + pustr->USalloc = 0; + } + pustr->USstr = 0; +} + +/* wchar version */ +void +usw_reset(USTRINGW *pustr) +{ + if (pustr->USalloc) { + free(pustr->USstr); + pustr->USalloc = 0; + } + pustr->USstr = 0; +} + +/* us_setsize() makes room for at least 'req' characters */ +size_t +us_setsize(USTRING *pustr, size_t req) +{ + if (SHOULD_CHANGE_ALLOC(req)) { + if (pustr->USalloc) + free(pustr->USstr); + pustr->USalloc = ROUND_ALLOC(req); + pustr->USstr = emalloc(pustr->USalloc); + } + + return pustr->USalloc; /* real buffer size is returned */ +} + +/* wchar version; note that 'req' is in characters, not bytes */ +size_t +usw_setsize(USTRINGW *pustr, size_t req) +{ + if (SHOULD_CHANGE_ALLOC(req)) { + if (pustr->USalloc) + free(pustr->USstr); + pustr->USalloc = ROUND_ALLOC(req); + pustr->USstr = emalloc(sizeof(wchar_t) * pustr->USalloc); + } + + return pustr->USalloc; +} + +/* like us_setsize(), but preserving contents */ +size_t +us_resize(USTRING *pustr, size_t req) +{ + if (SHOULD_CHANGE_ALLOC(req)) { + pustr->USalloc = ROUND_ALLOC(req); + pustr->USstr = erealloc(pustr->USstr,pustr->USalloc); + } + + return pustr->USalloc; +} + + +/* wchar version */ +size_t +usw_resize(USTRINGW *pustr, size_t req) +{ + if (SHOULD_CHANGE_ALLOC(req)) { + pustr->USalloc = ROUND_ALLOC(req); + pustr->USstr = erealloc(pustr->USstr,sizeof(wchar_t) * pustr->USalloc); + } + + return pustr->USalloc; +} + +/* quick alternative to copy */ +void +us_xchg(USTRING *s1, USTRING *s2) +{ + char *xstr; + size_t xalloc; + + xstr = s1->USstr; + s1->USstr = s2->USstr; + s2->USstr = xstr; + + xalloc = s1->USalloc; + s1->USalloc = s2->USalloc; + s2->USalloc = xalloc; +} + +/* wchar version */ +void +usw_xchg(USTRINGW *s1, USTRINGW *s2) +{ + wchar_t *xstr; + size_t xalloc; + + xstr = s1->USstr; + s1->USstr = s2->USstr; + s2->USstr = xstr; + + xalloc = s1->USalloc; + s1->USalloc = s2->USalloc; + s2->USalloc = xalloc; +} + +char * +us_copy(USTRING *pustr, const char *src) +{ + if (src == 0) { + us_reset(pustr); + return 0; + } + us_setsize(pustr,strlen(src) + 1); + strcpy(pustr->USstr,src); + return pustr->USstr; +} + +/* wchar version */ +wchar_t * +usw_copy(USTRINGW *pustr, const wchar_t *src) +{ + if (src == 0) { + usw_reset(pustr); + return 0; + } + usw_setsize(pustr,wcslen(src) + 1); + wcscpy(pustr->USstr,src); + return pustr->USstr; +} + +/* note: us_copyn() adds terminating null byte */ +char * +us_copyn(USTRING *pustr, const char *src, size_t len) +{ + char *dst; + + us_setsize(pustr,len + 1); + dst = pustr->USstr; + dst[len] = '\0'; + while (len-- > 0) + dst[len] = src[len]; + return dst; +} + +/* wchar version */ +wchar_t * +usw_copyn(USTRINGW *pustr, const wchar_t *src, size_t len) +{ + wchar_t *dst; + + usw_setsize(pustr,len + 1); + dst = pustr->USstr; + dst[len] = L'\0'; + while (len-- > 0) + dst[len] = src[len]; + return dst; +} + +/* concatenation: us_cat(&ustring, str1, str2, ..., strN, (char *)0); */ +void +us_cat(USTRING *pustr, ...) +{ + size_t len; + char *str; + va_list argptr; + + va_start(argptr,pustr); + for (len = 1; (str = va_arg(argptr, char *)); ) + len += strlen(str); + va_end(argptr); + us_setsize(pustr,len); + + va_start(argptr,pustr); + for (len = 0; (str = va_arg(argptr, char *)); ) { + strcpy(pustr->USstr + len,str); + len += strlen(str); + } + va_end(argptr); +} + +/* wchar version */ +void +usw_cat(USTRINGW *pustr, ...) +{ + size_t len; + wchar_t *str; + va_list argptr; + + va_start(argptr,pustr); + for (len = 1; (str = va_arg(argptr, wchar_t *)); ) + len += wcslen(str); + va_end(argptr); + usw_setsize(pustr,len); + + va_start(argptr,pustr); + for (len = 0; (str = va_arg(argptr, wchar_t *)); ) { + wcscpy(pustr->USstr + len,str); + len += wcslen(str); + } + va_end(argptr); +} diff --git a/src/ustring.h b/src/ustring.h new file mode 100644 index 0000000..0b82c89 --- /dev/null +++ b/src/ustring.h @@ -0,0 +1,31 @@ +typedef struct { + char *USstr; /* space to hold some character string */ + size_t USalloc; /* size of the allocated memory */ +} USTRING; + +typedef struct { + wchar_t *USstr; /* space to hold some wide character string */ + size_t USalloc; /* size of the allocated memory */ +} USTRINGW; + +#define ALLOC_UNIT 24 /* in bytes, do not change without a good reason */ +#define US_INIT(X) do { (X).USstr = 0; (X).USalloc = 0; } while (0) +#define PUSTR(X) ((X)->USstr) +#define USTR(X) ((X).USstr) +#define UNULL {0,0} + +extern void us_reset(USTRING *); +extern size_t us_setsize(USTRING *, size_t); +extern size_t us_resize(USTRING *, size_t); +extern void us_xchg(USTRING *, USTRING *); +extern char *us_copy(USTRING *, const char *); +extern char *us_copyn(USTRING *, const char *, size_t); +extern void us_cat(USTRING *, ...); + +extern void usw_reset(USTRINGW *); +extern size_t usw_setsize(USTRINGW *, size_t); +extern size_t usw_resize(USTRINGW *, size_t); +extern void usw_xchg(USTRINGW *, USTRINGW *); +extern wchar_t *usw_copy(USTRINGW *, const wchar_t *); +extern wchar_t *usw_copyn(USTRINGW *, const wchar_t *, size_t); +extern void usw_cat(USTRINGW *, ...); diff --git a/src/ustringutil.c b/src/ustringutil.c new file mode 100644 index 0000000..74d1fa5 --- /dev/null +++ b/src/ustringutil.c @@ -0,0 +1,104 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* USTRING enhanced versions of fixed buffer functions */ + +#include "clexheaders.h" + +#include /* errno */ +#include /* va_list */ +#include /* vsnprintf() */ +#include /* strlen() */ +#include /* readlink() */ + +#include "ustringutil.h" + +#ifndef va_copy +# ifdef __va_copy +# define va_copy __va_copy +# else +# define va_copy(dst,src) memcpy(&dst,&src,sizeof(va_list)) +# endif +#endif + +/* USTRING version of getcwd() */ +int +us_getcwd(USTRING *pustr) +{ + us_setsize(pustr,ALLOC_UNIT); + for (;/* until return*/;) { + if (getcwd(pustr->USstr,pustr->USalloc)) + return 0; + if (errno != ERANGE) + return -1; + /* increase buffer */ + us_setsize(pustr,pustr->USalloc + ALLOC_UNIT); + } +} + +/* USTRING version of readlink() */ +int +us_readlink(USTRING *pustr, const char *path) +{ + int len; + + us_setsize(pustr,ALLOC_UNIT); + for (;/* until return*/;) { + len = readlink(path,pustr->USstr,pustr->USalloc); + if (len == -1) + return -1; + if (len < pustr->USalloc) { + pustr->USstr[len] = '\0'; + return 0; + } + /* increase buffer */ + us_setsize(pustr,pustr->USalloc + ALLOC_UNIT); + } +} + +#define VPRINTF_MAX 512 /* max output length if vsnpritnf() does not behave correctly */ +/* USTRING version of vprintf() */ +void +us_vprintf(USTRING *pustr, const char *format, va_list argptr) +{ + int i, len, cnt; + va_list ap; + static FLAG conformsC99 = 0; + + /* roughly estimate the output size */ + len = strlen(format); + for (cnt = i = 0; i < len - 1; i++) + if (format[i] == '%') + cnt++; + us_setsize(pustr,len + cnt * ALLOC_UNIT); + + for (;/* until return */;) { + va_copy(ap,argptr); + len = vsnprintf(pustr->USstr,pustr->USalloc,format,ap); + va_end(ap); + if (len == -1) { + if (conformsC99 || pustr->USalloc > VPRINTF_MAX) { + us_cat(pustr,"INTERNAL ERROR: failed format string: \"",format,"\"",(char *)0); + return; + } + us_setsize(pustr,pustr->USalloc + ALLOC_UNIT); + } + else if (len >= pustr->USalloc) { + conformsC99 = 1; + us_setsize(pustr,len + 1); + } + else + return; + } +} diff --git a/src/ustringutil.h b/src/ustringutil.h new file mode 100644 index 0000000..d4b68d9 --- /dev/null +++ b/src/ustringutil.h @@ -0,0 +1,3 @@ +extern int us_getcwd(USTRING *); +extern int us_readlink(USTRING *, const char *); +extern void us_vprintf(USTRING *, const char *, va_list); diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..7437452 --- /dev/null +++ b/src/util.c @@ -0,0 +1,195 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +/* miscellaneous utilities */ + +#include "clexheaders.h" + +#include /* SSIZE_MAX */ +#include /* malloc() */ +#include /* strlen() */ +#include /* read() */ + +#include "util.h" + +#include "control.h" /* err_exit() */ + +/* variables used in pathname_xxx() functions */ +static USTRING path_buff = UNULL; +static size_t path_dirlen; + +/* get rid of directory part in 'pathname' */ +const char * +base_name(const char *pathname) +{ + const char *base, *pch; + char ch; + + for (pch = base = pathname; (ch = *pch++) != '\0'; ) + if (ch == '/') + base = pch; + return base; +} + +/* primitive check for an absolute pathname */ +int +checkabs(const char *path) +{ + return path != 0 && *path == '/'; +} + +static void +alloc_fail(size_t size) +{ + err_exit("Memory allocation failed, could not allocate %lu bytes", + (unsigned long)size); + /* NOTREACHED */ +} + +/* malloc with error checking */ +void * +emalloc(size_t size) +{ + void *mem; + + if (size > SSIZE_MAX) + /* + * possible problems with signed/unsigned int ! + * + * It is not normal to request such a huge memory + * block anyway (16-bit systems are not supported) + */ + alloc_fail(size); + if ((mem = malloc(size)) == 0) + alloc_fail(size); + return mem; +} + +/* realloc with error checking */ +void * +erealloc(void *ptr, size_t size) +{ + void *mem; + + /* not sure if really all realloc()s can handle this case */ + if (ptr == 0) + return emalloc(size); + + if (size > SSIZE_MAX) + /* see emalloc() above */ + alloc_fail(size); + + if ((mem = realloc(ptr,size)) == 0) + alloc_fail(size); + return mem; +} + +/* strdup with error checking */ +char * +estrdup(const char *str) +{ + char *dup; + + if (str == 0) + return 0; + dup = emalloc(strlen(str) + 1); + strcpy(dup,str); + return dup; +} + +/* wcsdup with error checking */ +wchar_t * +ewcsdup(const wchar_t *str) +{ + wchar_t *dup; + + if (str == 0) + return 0; + dup = emalloc((wcslen(str) + 1) * sizeof(wchar_t)); + wcscpy(dup,str); + return dup; +} + +void +efree(void *ptr) +{ + if (ptr) + free(ptr); +} + +/* set the directory name for pathname_join() */ +void +pathname_set_directory(const char *dir) +{ + char *str; + + path_dirlen = strlen(dir); + us_resize(&path_buff,path_dirlen + ALLOC_UNIT); + /* extra bytes for slash and initial space for the filename */ + str = USTR(path_buff); + strcpy(str,dir); + if (str[path_dirlen - 1] != '/') + str[path_dirlen++] = '/'; + /* the string is now not null terminated, that's ok */ +} + +/* + * join the filename 'file' with the directory set by + * pathname_set_directory() above + * + * returned data is overwritten by subsequent calls + */ +char * +pathname_join(const char *file) +{ + us_resize(&path_buff,path_dirlen + strlen(file) + 1); + strcpy(USTR(path_buff) + path_dirlen,file); + return USTR(path_buff); +} + +/* + * under certain condition can read() return fewer bytes than requested, + * this wrapper function handles it + * + * remember: error check should be (read() == -1) and not (read() < 0) + */ +ssize_t +read_fd(int fd, char *buff, size_t bytes) +{ + size_t total; + ssize_t rd; + + for (total = 0; bytes > 0; total += rd, bytes -= rd) { + rd = read(fd,buff + total,bytes); + if (rd == -1) /* error */ + return -1; + if (rd == 0) /* EOF */ + break; + } + return total; +} + +/* this is a hash function written by Justin Sobel */ +unsigned int +jshash(const wchar_t *str) +{ + unsigned int len, i = 0, hash = 1315423911; + + len = wcslen(str); + for (i = 0; i < len; i++) + hash ^= ((hash << 5) + str[i] + (hash >> 2)); + + return hash; +} + diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..860a657 --- /dev/null +++ b/src/util.h @@ -0,0 +1,11 @@ +extern const char *base_name(const char *); +extern int checkabs(const char *); +extern void *emalloc(size_t); +extern void *erealloc(void *, size_t); +extern char *estrdup(const char *); +extern wchar_t *ewcsdup(const wchar_t *); +extern void efree(void *); +extern void pathname_set_directory(const char *); +extern char *pathname_join(const char *); +extern ssize_t read_fd(int, char *, size_t); +extern unsigned int jshash(const wchar_t *); diff --git a/src/xterm_title.c b/src/xterm_title.c new file mode 100644 index 0000000..2ded0b7 --- /dev/null +++ b/src/xterm_title.c @@ -0,0 +1,206 @@ +/* + * + * CLEX File Manager + * + * Copyright (C) 2001-2022 Vlado Potisk + * + * CLEX is free software without warranty of any kind; see the + * GNU General Public License as set out in the "COPYING" document + * which accompanies the CLEX File Manager package. + * + * CLEX can be downloaded from https://github.com/xitop/clex + * + */ + +#include "clexheaders.h" + +#include /* waitpid() */ +#include /* open() */ +#include /* isprint() */ +#include /* open() */ +#include /* sigaction() */ +#include /* va_list */ +#include /* fputs() */ +#include /* getenv() */ +#include /* strchr() */ +#include /* fork() */ +#include /* iswprint() */ + +#include "xterm_title.h" + +#include "cfg.h" /* cfg_num() */ +#include "log.h" /* msgout() */ +#include "mbwstring.h" /* convert2mb() */ +#include "util.h" /* estrdup() */ + +static FLAG enabled = 0; +static const char *old_title = 0, default_title[] = "terminal"; + +#define XPROP_TIMEOUT 4 /* timeout for the xprop command in seconds */ +#define CMD_STR 64 /* buffer size for the executed command's name in the title */ + +void +xterm_title_initialize(void) +{ + xterm_title_reconfig(); + xterm_title_set(0,0,0); +} + +/* + * run the command: xprop -id $WINDOWID WM_NAME 2>/dev/null + * to get the current xterm title + */ +static char * +get_title(void) +{ + int fd[2], efd; + ssize_t rd; + char *p1, *p2, title[192]; + const char *wid; + pid_t pid; + struct sigaction act; + + if ( (wid = getenv("WINDOWID")) == 0) + return 0; + + if (pipe(fd) < 0 || (pid = fork()) < 0) + return 0; + + if (pid == 0) { + /* this is the child process */ + logfile_close(); + + close(fd[0]); /* close read end */ + if (fd[1] != STDOUT_FILENO) { + if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) + _exit(126); + close(fd[1]); + } + + efd = open("/dev/null",O_RDONLY); + if (efd != STDERR_FILENO) { + if (dup2(efd, STDERR_FILENO) != STDERR_FILENO) + _exit(126); + close(efd); + } + + act.sa_handler = SIG_DFL; + act.sa_flags = 0; + sigemptyset(&act.sa_mask); + sigaction(SIGALRM,&act,0); + alarm(XPROP_TIMEOUT); + + execlp("xprop", "xprop", "-id", wid, "WM_NAME", (char *)0); + _exit(127); + } + + /* parent continues here */ + /* do not return before waitpid() - otherwise you'll create a zombie */ + close(fd[1]); /* close write end */ + rd = read_fd(fd[0],title,sizeof(title) - 1); + close(fd[0]); + + if (waitpid(pid, 0, 0) < 0) + return 0; + + if (rd == -1) + return 0; + + title[rd] = '\0'; + /* get the window title in quotation marks */ + p1 = strchr(title,'\"'); + if (p1 == 0) + return 0; + p2 = strchr(++p1,'\"'); + if (p2 == 0) + return 0; + *p2 = '\0'; + + return estrdup(p1); +} + +void +xterm_title_reconfig(void) +{ + enabled = cfg_num(CFG_XTERM_TITLE); + if (!enabled || (old_title != 0 && old_title != default_title)) + return; + + if (disp_data.noxterm || (!disp_data.xterm && !disp_data.xwin)) { + msgout(MSG_NOTICE,"Disabling the terminal title change feature, because required support is missing."); + enabled = 0; + return; + } + + if ((old_title = get_title()) == 0) { + msgout(MSG_NOTICE,"Could not get the current terminal window title" + " because the command \"xprop -id $WINDOWID WM_NAME\" has failed." + " CLEX will not be able to restore the original title when it terminates"); + old_title = default_title; + } +} + +static void +set_xtitle(const char *str1, ...) +{ + va_list argptr; + const char *strn; + + fputs("\033]0;",stdout); + + fputs(str1,stdout); + va_start(argptr,str1); + while ( (strn = va_arg(argptr, char *)) ) + if (*strn) + fputs(strn,stdout); + va_end(argptr); + + /* Xterm FAQ claims \007 is incorrect, but everybody uses it */ + fputs("\007",stdout); + fflush(stdout); +} + +void +xterm_title_set(int busy, const char *cmd, const wchar_t *cmdw) +{ + wchar_t wch, title_cmdw[CMD_STR]; + static USTRING local = UNULL; + const char *title_cmd; + FLAG islong, isnonprint; + int i; + + if (!enabled) + return; + + if (cmd == 0) { + /* CLEX is idle */ + set_xtitle("clex: ", user_data.login,"@",user_data.host, (char *)0); + return; + } + + /* CLEX is executing (busy is true) or was executing (busy is false) 'cmd' */ + for (islong = isnonprint = 0, i = 0; (wch = cmdw[i]); i++) { + if (i == CMD_STR - 1) { + islong = 1; + break; + } + if (!iswprint(wch)) { + isnonprint = 1; + wch = L'?'; + } + title_cmdw[i] = wch; + } + title_cmdw[i] = '\0'; + + title_cmd = (islong || isnonprint) ? us_convert2mb(title_cmdw,&local) : cmd; + set_xtitle(busy ? "" : "[", "clex: ", title_cmd, islong ? "..." : "", + busy ? (char *)0 : "]", (char *)0); +} + +/* this is a cleanup function (see err_exit() in control.c) */ +void +xterm_title_restore(void) +{ + if (enabled) + set_xtitle(old_title,(char *)0); +} diff --git a/src/xterm_title.h b/src/xterm_title.h new file mode 100644 index 0000000..6f601d9 --- /dev/null +++ b/src/xterm_title.h @@ -0,0 +1,4 @@ +extern void xterm_title_initialize(void); +extern void xterm_title_reconfig(void); +extern void xterm_title_set(int, const char *, const wchar_t *); +extern void xterm_title_restore(void);