├── README.md ├── LICENSE └── shellect /README.md: -------------------------------------------------------------------------------- 1 | # shellect 2 | 3 | shellect is a selection system written in POSIX shell. 4 | 5 | ## Table of content 6 | 7 | 8 | 9 | 10 | * [Preview](#preview) 11 | * [Dependency](#dependency) 12 | * [Introduction](#introduction) 13 | * [Projects that is using `shellect`](#projects-that-is-using-shellect) 14 | * [Implementation Details](#implementation-details) 15 | * [Explanation for TUI manipulation](#explanation-for-tui-manipulation) 16 | * [Overcome the limitation of POSIX shell](#overcome-the-limitation-of-posix-shell) 17 | 18 | 19 | 20 | ## Preview 21 | 22 | [![shellect](https://asciinema.org/a/jLJay0bFv0mqSfcnWbAWYiVwu.png)](https://asciinema.org/a/jLJay0bFv0mqSfcnWbAWYiVwu) 23 | 24 | ## Dependency 25 | 26 | 1. POSIX-compliant shell: printf, set, unset, shift, test, while, continue, break, return, case, trap, getopts 27 | 2. stty 28 | 3. dd 29 | 4. cat 30 | 31 | ## Introduction 32 | 33 | shellect will either accept standard input or assign the display content by `-c` option, i.e., to display all the non-hidden files and directories in your `$HOME` directory, 34 | 35 | ```sh 36 | printf '%s\n' $HOME/* | shellect # standard input 37 | shellect -c "$HOME/*" # -c option 38 | 39 | ``` 40 | 41 | The keybindings are: 42 | 43 | ``` 44 | k/↑/Ctrl-p - up 45 | j/↓/Ctrl-n - down 46 | l/→ - right 47 | h/← - left 48 | Ctrl-f/PageDown - PageDown 49 | Ctrl-u/PageUp - PageUp 50 | g/Home/Ctrl-a - go to top 51 | G/End/Ctrl-e - go to bottom 52 | / - search 53 | ? - show keybinds 54 | q - quit 55 | ``` 56 | 57 | Command-line option: 58 | 59 | ``` 60 | Usage: 61 | 62 | shellect [OPTIONS] ([ARGS]) 63 | 64 | -h, Show help options 65 | -i, Set case-insensitive search 66 | -l, Set live-search 67 | -n=[num], Set numbers of line per entry 68 | -d=[delim], Set delimiter (IFS, internal field separator) 69 | -c=[content], Set content to display 70 | -f=[format], Set the format to print out content 71 | -t=[msg], Set top status bar message 72 | -b=[msg], Set bottom status bar message 73 | 74 | format detail: 75 | nldel delete last nl, equiv to "\${1%\$nl}" 76 | basename only print basename, equiv to "\${1##*/}" ;; 77 | 78 | if unset or empty, then equiv to "\$1" 79 | 80 | live-search detail: 81 | Enter confirm 82 | Backspace delete previous character 83 | Tab Tab-completion forward 84 | Shift-Tab Tab-completion backward 85 | control char ignore 86 | others print out 87 | ``` 88 | 89 | ## Projects that is using `shellect` 90 | 91 | - [bib.awk](https://github.com/huijunchen9260/bib.awk) 92 | - [dmenufm](https://github.com/huijunchen9260/dmenufm) 93 | 94 | ## Implementation Details 95 | 96 | shellect born from my experience in developing my bibliography manager, [`shbib`](https://github.com/huijunchen9260/shbib), and I built [`shbib`](https://github.com/huijunchen9260/shbib) on the basis provided by [`shfm`](https://github.com/dylanaraps/shfm). I realized that if I do not obey the Unix philosophy and keep adding functions to [`shbib`](https://github.com/huijunchen9260/shbib), [`shbib`](https://github.com/huijunchen9260/shbib) would grow exponentially and eventually become a pain to maintain. Therefore, I isolate out shellect as an individual selection system that just written in POSIX shell. 97 | 98 | ### Explanation for TUI manipulation 99 | 100 | Basically, printing out the raw escape sequence to manipulate the terminal output works in most terminal, and its function is way richer than `tput`. 101 | However, printing out these escape sequence can be daunting, and it is often time-consuming to remember the function of each arbitrary sequence. 102 | Thus, The following `esc` function is steal from [`shfm`](https://github.com/dylanaraps/shfm), with some of my own comment and modification to facilitate the understanding. 103 | All of the resource can be found in the following three resources: 104 | 105 | - Reference for color: https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences 106 | - Reference for vt100: https://vt100.net/docs/vt510-rm/contents.html 107 | - Reference for escape sequence: https://github.com/dylanaraps/pure-sh-bible#escape-sequences 108 | 109 | ```sh 110 | esc() { 111 | case $1 in 112 | # vt100 (IL is vt102) (DECTCEM is vt520) 113 | CUP) printf '%s[%s;%sH' "$esc_c" "$2" "$3" ;; 114 | # cursor to LINES($2), COLUMNS($3) 115 | CUU) printf '%s[%sA' "$esc_c" "$2" ;; 116 | # cursor up 117 | CUD) printf '%s[%sB' "$esc_c" "$2" ;; 118 | # cursor down 119 | CUR) printf '%s[%sC' "$esc_c" "$2" ;; 120 | # cursor right 121 | CUL) printf '%s[%sD' "$esc_c" "$2" ;; 122 | # cursor left 123 | DECAWM) printf '%s[?7%s' "$esc_c" "$2" ;; 124 | # (h: set; l: unset) line wrap 125 | DECRC) printf '%s8' "$esc_c" ;; 126 | # cursor restore 127 | DECSC) printf '%s7' "$esc_c" ;; 128 | # cursor save 129 | DECSTBM) printf '%s[%s;%sr' "$esc_c" "$2" "$3" ;; 130 | # scroll region ($2: top; $3: bottom) 131 | DECSLRM) printf '%s[%s;%ss' "$esc_c" "$2" "$3" ;; 132 | # Set left and right margin 133 | DECTCEM) printf '%s[?25%s' "$esc_c" "$2" ;; 134 | # (h: show; l: hide) cursor visible 135 | ED[0-2]) printf '%s[%sJ' "$esc_c" "${1#ED}" ;; 136 | # Erase Display: 137 | # 0: From the cursor through the end of the display 138 | # 1: From the beginning of the display through the cursor 139 | # 2: The complete display 140 | EL[0-2]) printf '%s[%sK' "$esc_c" "${1#EL}" ;; 141 | # Erase Line: 142 | # 0: from cursor to end of the line 143 | # 1: from beginning of the line to cursor 144 | # 2: entire line 145 | IL) printf '%s[%sL' "$esc_c" "$2" ;; 146 | # insert blank line 147 | SGR) printf '%s[%s;%sm' "$esc_c" "$2" "$3" ;; 148 | # colors ($2); attribute ($3) 149 | 150 | # Color list: 151 | # FG BG 152 | # Black 30 40 153 | # Red 31 41 154 | # Green 32 42 155 | # Yellow 33 43 156 | # Blue 34 44 157 | # Magenta 35 45 158 | # Cyan 36 46 159 | # White 37 47 160 | # Bright Black 90 100 161 | # Bright Red 91 101 162 | # Bright Green 92 102 163 | # Bright Yellow 93 103 164 | # Bright Blue 94 104 165 | # Bright Magenta 95 105 166 | # Bright Cyan 96 106 167 | # Bright White 97 107 168 | 169 | # Attribute list: 170 | # Reset 0/'' 171 | # Bold 1 172 | # Faint 2 173 | # Italic 3 174 | # Underline 4 175 | # Slow blink 5 176 | # Swap foreground and background colors. 7 177 | # Hidden 8 178 | # Strike-through 9 179 | 180 | # xterm (since 1988, supported widely) 181 | screen_alt) printf '%s[?1049%s' "$esc_c" "$2" ;; # (h: to; l: back from) alternate buffer 182 | esac 183 | } 184 | ``` 185 | 186 | ### Overcome the limitation of POSIX shell 187 | 188 | POSIX shell is very limited, and quite inefficient compared to compiling language. 189 | Previously, I experimented the efficiency of POSIX shell in terms of passing through all the argument array elements into the key detecting part: 190 | 191 | > The efficiency of shellect is highly constraint by the total number of entries and the content that you want to display. 192 | > With `bash`, as I tested, probably only numbers of 5000 is large enough to create significant lag. The command I run is `tree /directory/have/5000/subitems | shellect` or `echo $(seq 1 5000) | shellect`. 193 | > With `dash`, the efficiency is highly depends on both directions. At the number 20000, shellect runs fair efficiency. The command is `tree /directory/have/20000/subitems | shellect`. With the number of 30000, the pointer will not stop if I relieve my key press. However, changing the command to `echo $(seq 1 30000) | shellect`, in my computer, shellect runs with fair efficiency. 194 | > Comparing with `dmenu` and `fzf`, shellect is probably extremely inefficient in terms of large numbers of entry. This is probably the limitation of an interpreting language compared to compiling language. 195 | 196 | As an interpreting language, I found a way to avoid such inefficiency. 197 | 198 | First, I'll define some terminologies that I'll use through the explanation: 199 | 200 | 1. argument array: POSIX shell has no array type. However, there's actually one, and only one array in POSIX shell, i.e., the positional parameters, `$1`, `$2`, etc. 201 | To see more information, go to "Working with arrays" section in [Rich’s sh (POSIX shell) tricks](http://www.etalabs.net/sh_tricks.html). 202 | 2. selection: the item in argument array that is defined in `$cur`. 203 | 3. Length of array: access by `$#`. The length of the total content is `$last`, and the length of a list is `$len`. `$len` is set to 500 if the length of the total content, `$last`, is larger than 500. 204 | 205 | 206 | ```sh 207 | while key=$(dd ibs=1 count=1 2>/dev/null); do 208 | ... 209 | done 210 | ``` 211 | 212 | This `while` loop is the part to detect key press. 213 | This `dd` command has nothing to interact with the argument array. 214 | However, `dd`'s efficiency will be highly affected by argument array that just pass through it. 215 | If the total number of argument array is too high, then `dd` will become laggy when reading the key press, eventually causing the cursor movement is laggy. 216 | 217 | To resolve this limitation. I developed a technique to only feed part of the total content to the above while loop: 218 | 219 | ```sh 220 | key() { 221 | input_assign ... # Generate a list which is part of the total content 222 | 223 | set -- $list # let the list to be argument array 224 | 225 | while key=$(dd ibs=1 count=1 2>/dev/null); do 226 | ... 227 | othercommand 228 | return 0 # Go back to main function and stay in the while loop in main function 229 | ... 230 | selection key pressed 231 | return 1 # Go back to main and leave the while loop in main function 232 | done 233 | } 234 | 235 | main() { 236 | ... 237 | while [ $? -eq 0 ]; do # If return 0, stay in while loop; others, leave the while loop 238 | set -- $content # total content 239 | key "$@" 240 | done 241 | } 242 | ``` 243 | 244 | The following steps are to actively switch between `main` function and `key` function. 245 | Within the `$list`, the selection stay in the `while key` loop. If the current selection ever go out of the `$list`, then go back to `main` function, reload the `$content` and generate new `$list`, back to `while key` loop, and process again. 246 | That is to say, user will experience an one-time inaction when reach the boundary of `$list`. 247 | This inaction is to renew `$list` to match current position in the whole `$content`. 248 | Press again, and the selection will move to the next item. 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, see . 308 | 309 | Also add information on how to contact you by electronic and paper mail. 310 | 311 | If the program is interactive, make it output a short notice like this 312 | when it starts in an interactive mode: 313 | 314 | Gnomovision version 69, Copyright (C) year name of author 315 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 316 | This is free software, and you are welcome to redistribute it 317 | under certain conditions; type `show c' for details. 318 | 319 | The hypothetical commands `show w' and `show c' should show the appropriate 320 | parts of the General Public License. Of course, the commands you use may 321 | be called something other than `show w' and `show c'; they could even be 322 | mouse-clicks or menu items--whatever suits your program. 323 | 324 | You should also get your employer (if you work as a programmer) or your 325 | school, if any, to sign a "copyright disclaimer" for the program, if 326 | necessary. Here is a sample; alter the names: 327 | 328 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 329 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 330 | 331 | , 1 April 1989 332 | Moe Ghoul, President of Vice 333 | 334 | This General Public License does not permit incorporating your program into 335 | proprietary programs. If your program is a subroutine library, you may 336 | consider it more useful to permit linking proprietary applications with the 337 | library. If this is what you want to do, use the GNU Lesser General 338 | Public License instead of this License. 339 | -------------------------------------------------------------------------------- /shellect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # shellect - an interactive select utility for POSIX shells 4 | # Copyright (C) 2025 Hui-Jun Chen 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, see . 18 | 19 | # Variable explain: 20 | # $y: the current argument in the whole content 21 | # $yy: the current argument in the list 22 | # $pos: the current position on argument y 23 | # $y2: the current position on current screen position 24 | # $num: numbers of lines per argument 25 | # $fin: bottom for printable area, depends on num 26 | # $token: indicator for actions 27 | # Dimension is adjustable by $top & $bottom 28 | 29 | # Copied from shfm under MIT License: 30 | # Copyright (c) 2020 Dylan Araps 31 | # https://github.com/dylanaraps/shfm 32 | esc() { 33 | case $1 in 34 | # vt100 (IL is vt102) (DECTCEM is vt520) 35 | CUP) printf '%s[%s;%sH' "$esc_c" "$2" "$3" ;; 36 | # cursor to LINES($2), COLUMNS($3) 37 | CUU) printf '%s[%sA' "$esc_c" "$2" ;; 38 | # cursor up 39 | CUD) printf '%s[%sB' "$esc_c" "$2" ;; 40 | # cursor down 41 | CUR) printf '%s[%sC' "$esc_c" "$2" ;; 42 | # cursor right 43 | CUL) printf '%s[%sD' "$esc_c" "$2" ;; 44 | # cursor left 45 | DECAWM) printf '%s[?7%s' "$esc_c" "$2" ;; 46 | # (h: set; l: unset) line wrap 47 | DECRC) printf '%s8' "$esc_c" ;; 48 | # cursor restore 49 | DECSC) printf '%s7' "$esc_c" ;; 50 | # cursor save 51 | DECSTBM) printf '%s[%s;%sr' "$esc_c" "$2" "$3" ;; 52 | # scroll region 53 | DECTCEM) printf '%s[?25%s' "$esc_c" "$2" ;; 54 | # (h: show; l: hide) cursor visible 55 | ED[0-2]) printf '%s[%sJ' "$esc_c" "${1#ED}" ;; 56 | # clear screen 57 | EL[0-2]) printf '%s[%sK' "$esc_c" "${1#EL}" ;; 58 | # clear line 59 | IL) printf '%s[%sL' "$esc_c" "$2" ;; 60 | # insert line 61 | SGR) printf '%s[%s;%sm' "$esc_c" "$2" "$3" ;; 62 | # colors ($2); attribute ($3) 63 | 64 | # xterm (since 1988, supported widely) 65 | screen_alt) printf '%s[?1049%s' "$esc_c" "$2" ;; 66 | # (h: to; l: back from) alternate buffer 67 | esac 68 | } 69 | 70 | term_setup() { 71 | bottom=$((LINES - 2)) # space for bottom status_line 72 | top=3 # space for top status_line 73 | fin=$(( bottom - (bottom - (top - 1)) % num )) # bottom for content printed 74 | 75 | stty=$(stty -g) 76 | stty -icanon -echo 77 | esc screen_alt h 78 | esc DECAWM l 79 | esc DECTCEM l 80 | esc ED2 81 | esc DECSTBM "$top" "$fin" 82 | } 83 | 84 | term_getsize() { 85 | # false-positive, behavior intentional, globbing is disabled. 86 | # shellcheck disable=2046 87 | { 88 | set -f -- $(stty size) 89 | set +f 90 | } 91 | 92 | LINES=$1 COLUMNS=$2 93 | } 94 | 95 | term_reset() { 96 | esc DECAWM h >&2 97 | esc DECTCEM h >&2 98 | esc ED2 >&2 99 | esc DECSTBM >&2 100 | esc screen_alt l >&2 101 | stty "$stty" 102 | } 103 | 104 | term_init() { 105 | term_setup 106 | stty_init="$stty" 107 | } 108 | 109 | term_exit() { 110 | stty="$stty_init" 111 | term_reset 112 | } 113 | 114 | old_save() { 115 | old_num=$num 116 | old_y=$y 117 | old_yy=$yy 118 | old_y2=$y2 119 | old_pos=$pos 120 | old_format=$format 121 | old_msg=$msg 122 | old_ltype=$ltype 123 | old_last=$last 124 | old_len=$len 125 | } 126 | 127 | old_reset() { 128 | num=$old_num 129 | y=$old_y 130 | yy=$old_yy 131 | y2=$old_y2 132 | pos=$old_pos 133 | format=$old_format 134 | msg=$old_msg 135 | ltype=$old_ltype 136 | last=$old_last 137 | len=$old_len 138 | } 139 | 140 | input_assign() { 141 | offset=$1 142 | length=$2 143 | 144 | shift "$(( offset > 0 ? offset : 0 ))"; shift 2; 145 | 146 | i=1 147 | while [ "$i" -le "$length" ]; do 148 | save_list="$save_list${ifs:=$nl}$1" 149 | shift $(( $# > 0 ? 1 : 0 )) 150 | i=$(( i + 1 )) 151 | done 152 | 153 | save_list="${save_list#${ifs:=$nl}}" 154 | list=$save_list 155 | 156 | unset i offset length save_list 157 | } 158 | 159 | 160 | redraw() { 161 | 162 | pos=$(( num * y - num + top )) 163 | end=$(( fin + 1 )) 164 | width=$(( (end - top) / num )) 165 | 166 | input_assign "$(( y >= width ? y - width : 0 ))" "$width" "$@" 167 | 168 | IFS=${ifs:=$nl} 169 | # shellcheck disable=2086 170 | set -- $list 171 | unset IFS 172 | 173 | esc ED2 174 | 175 | status_line "$last" 176 | list_print "$@" 177 | 178 | unset list 179 | } 180 | 181 | 182 | list_print() { 183 | esc CUP "$top" 184 | 185 | unset cur 186 | 187 | i=1 188 | 189 | for file do 190 | 191 | case $(( num * i - num + top )) in 192 | "$y2") cur=$file; save_cur=$file ;; 193 | *) cur= ;; 194 | esac 195 | 196 | case $(( num * i - num + top - end )) in 197 | -*) 198 | arg_format "$file" 199 | esc CUD 200 | i=$((i + 1)) 201 | ;; 202 | 0|*) break ;; 203 | esac 204 | 205 | done 206 | 207 | esc CUP "$(( pos > y2 ? y2 : pos ))" 208 | 209 | cur=$save_cur 210 | unset save_cur 211 | } 212 | 213 | arg_print() { 214 | shift "$1" 215 | arg_format "$1" 216 | } 217 | 218 | 219 | arg_format() { 220 | esc EL0 221 | 222 | # overall display rule 223 | case "$cur" in 224 | '') esc CUR 5; esc SGR '' 2 ;; 225 | *) esc CUR 2; esc SGR 35 1; printf '>'; esc SGR 33 1; esc CUR 2 ;; 226 | esac 227 | 228 | # content display format 229 | case "$format" in 230 | basename) printf '%s\r' "${1##*/}" ;; 231 | nldel) printf '%s\r' "${1%$nl}" ;; 232 | '') printf '%s\r' "$1" ;; 233 | esac 234 | 235 | esc SGR 236 | } 237 | 238 | 239 | term_move_up() { 240 | 241 | y=$(( y - move )) yy=$(( yy - move )) 242 | pos=$(( num * y - num + top )) 243 | 244 | # Generate list for partial input 245 | input_assign "$(( yy - 1 ))" "$(( move + 1 ))" "$@" 246 | IFS=${ifs:=$nl} 247 | # shellcheck disable=2086 248 | set -- $list 249 | unset IFS list cur 250 | 251 | i=1 252 | while [ "$i" -le "$move" ] ; do 253 | arg_print "$(( move - i + 2 ))" "$@" # print the latter item first 254 | 255 | case "$y2" in # adjust for upper bound 256 | "$top") [ "$num" -gt 1 ] && esc CUU $(( num - 1 )); esc IL "$num" ;; 257 | *) esc CUU "$(( num * 2 - 1 ))"; y2=$(( y2 > top ? y2 - num : top )) ;; 258 | esac 259 | 260 | i=$(( i + 1 )) 261 | done 262 | 263 | offset=$y 264 | cur=$1 265 | arg_format "$1" 266 | 267 | # Restore cursor to match y2 268 | [ "$num" -gt 1 ] && esc CUU "$(( num - 1 ))" 269 | 270 | status_line "$last" 271 | 272 | unset i offset 273 | } 274 | 275 | term_move_down() { 276 | 277 | y=$(( y + move )) yy=$(( yy + move )) 278 | pos=$(( num * y - num + top )) 279 | y2=$(( y2 + num * move < fin - (num - 1) ? y2 + num * move : fin - (num - 1) )) 280 | 281 | # Generate list for partial input 282 | input_assign "$(( yy - move - 1 ))" "$(( move + 1 ))" "$@" 283 | IFS=${ifs:=$nl} 284 | # shellcheck disable=2086 285 | set -- $list 286 | unset IFS cur 287 | 288 | i=1 289 | while [ "$i" -le "$move" ] ; do 290 | arg_format "$1" 291 | shift 1 292 | printf '\n' 293 | 294 | i=$(( i + 1 )) 295 | done 296 | 297 | offset=$y 298 | cur=$1 299 | arg_format "$1" 300 | 301 | [ "$num" -gt 1 ] && esc CUU "$(( num - 1 ))" 302 | 303 | status_line "$last" 304 | 305 | unset i offset 306 | } 307 | 308 | status_line () { 309 | esc DECSC 310 | esc CUP 1; esc EL2; printf '%s ' "($y/$1)"; printf "$msg" 311 | [ -n "$ltype" ] && { esc CUP "$LINES"; esc EL2; printf '%s' "$ltype"; } 312 | esc DECRC 313 | } 314 | 315 | prompt() { 316 | esc DECSC 317 | esc CUP "$LINES" 318 | printf %s "$1" 319 | esc DECTCEM h 320 | esc EL0 321 | 322 | case $2 in 323 | r) 324 | stty -cread icanon echo 1>/dev/null 2>&1 325 | read -r ans ||: 326 | stty -icanon -echo 327 | ;; 328 | l) press=$(dd ibs=1 count=1 2>/dev/null) ;; 329 | esac 330 | 331 | esc DECRC 332 | esc DECTCEM l 333 | } 334 | 335 | tab_complete() { 336 | shift "$(( count - 1 > 0 ? count - 1 : 0 ))" 337 | ans=$1 338 | } 339 | 340 | search() { 341 | search_list="$1" 342 | IFS="$2" 343 | str="$3" 344 | 345 | case $case_insense in 346 | 1) # case-insensitive filter 347 | 348 | # lowercase both search_list and str 349 | lower_search_list=$(printf '%s' "$search_list" | dd conv=lcase 2>/dev/null) 350 | lower_str=$(printf '%s' "$str" | dd conv=lcase 2>/dev/null) 351 | 352 | # First run: match lowercase and record the number of positional parameter 353 | i=1 s=1 354 | # shellcheck disable=2086 355 | set -- $lower_search_list 356 | for line do 357 | case $line in 358 | *$lower_str*) 359 | case $token in 360 | l) # live search: only search until bound 361 | case $((s - bound)) in 362 | -*|0) posnum="$posnum $i" ;; 363 | *) break ;; 364 | esac 365 | s=$(( s + 1 )) 366 | ;; 367 | /) posnum="$posnum $i" ;; 368 | esac 369 | esac 370 | i=$(( i + 1 )) 371 | done 372 | posnum=${posnum#* } # delete first space 373 | 374 | # Second run: match the item based on posnum above 375 | # shellcheck disable=2086 376 | set -- $search_list 377 | 378 | j=1 379 | for line do 380 | [ -z "$posnum" ] && break 381 | n=${posnum%% *} # first line number 382 | case $j in 383 | "$n") 384 | filter="$filter$IFS$line" 385 | 386 | case $posnum in # delete first number 387 | *[[:space:]]*) posnum=${posnum#* } ;; 388 | *) posnum= ;; 389 | esac 390 | ;; 391 | esac 392 | j=$(( j + 1 )) 393 | done 394 | 395 | ;; 396 | *) # case-sensitive filter 397 | 398 | s=1 399 | 400 | # shellcheck disable=2086 401 | set -- $search_list 402 | for line do 403 | case $line in 404 | *$str*) 405 | case $token in 406 | l) # live search: only search until bound 407 | case $((s - bound)) in 408 | -*|0) filter="$filter$IFS$line" ;; 409 | *) break ;; 410 | esac 411 | s=$(( s + 1 )) 412 | ;; 413 | /) filter="$filter$IFS$line" ;; 414 | esac 415 | esac 416 | done 417 | 418 | ;; 419 | esac 420 | 421 | filt_out=${filter#*$IFS} 422 | 423 | unset search_list IFS str filter posnum lower_list lower_str i j n s 424 | 425 | } 426 | 427 | key() { 428 | 429 | loop=$(( loop + 1 )) 430 | 431 | # Generate new list 432 | case $(( y - move )) in 433 | 1) y=1; yy=1; input_assign "0" "$len" "$@" ;; 434 | $(( last - move - 1 ))) y=$last; input_assign "$(( y - len ))" "$len" "$@" ;; 435 | *) 436 | case $(( y - len )) in 437 | -*) input_assign "$(( yy > 1 ? y - yy : y - 1 ))" "$len" "$@" ;; 438 | *) input_assign "$(( yy > 1 ? y - len : y - 1 ))" "$len" "$@" ;; 439 | esac 440 | esac 441 | 442 | IFS=${ifs:=$nl} 443 | # shellcheck disable=2086 444 | set -- $list 445 | unset IFS 446 | 447 | while key=$(dd ibs=1 count=1 2>/dev/null); do 448 | 449 | case $key${esc:=0} in 450 | "$ctrl_u"?|~5) # Ctrl-u / PageUp 451 | move=$(( width / 4 > 2 ? width / 4 : 2 )) 452 | # Terminal condition 453 | case $y in 454 | -*|0|1) continue ;; 455 | esac 456 | # Adjust move step 457 | case $(( y - move - 1 )) in 458 | -*|0) move=$(( y - 1 )) ;; 459 | esac 460 | # Move or update list 461 | case $(( yy - move )) in 462 | -*|0) yy=$(( y > len ? len : y )); return 0 ;; 463 | *) term_move_up "$@" ;; 464 | esac 465 | ;; 466 | 467 | "$ctrl_f"?|~6) # Ctrl-f / PageDown 468 | move=$(( width / 4 > 2 ? width / 4 : 2 )) 469 | # Terminal condition 470 | case $y in 471 | "$last") continue ;; 472 | esac 473 | # Adjust move step 474 | case $(( y - last + move )) in 475 | [1-9]*) move=$(( last - y )) ;; 476 | esac 477 | # Move or update list 478 | case $(( yy - len + move )) in 479 | -*|0) term_move_down "$@" ;; 480 | *) yy=1; return 0 ;; 481 | esac 482 | ;; 483 | k?|A2|"$ctrl_p"?) # k / Arrow Up / Ctrl-p 484 | move=1 485 | # Terminal condition 486 | case $y in 487 | -*|0|1) continue ;; 488 | esac 489 | # Move or update list 490 | case $(( yy - move )) in 491 | -*|0) yy=$len; return 0 ;; 492 | *) term_move_up "$@" ;; 493 | esac 494 | ;; 495 | j?|B2|"$ctrl_n"?) # j / Arrow Down / Ctrl-n 496 | move=1 497 | # Terminal condition 498 | case $y in 499 | "$last") continue ;; 500 | esac 501 | # Move or update list 502 | case $(( yy - len + move )) in 503 | -*|0) term_move_down "$@" ;; 504 | *) yy=1; return 0 ;; 505 | esac 506 | ;; 507 | 508 | g?|H2|"$ctrl_a"?) # g / Home / Ctrl-a 509 | # Terminal condition 510 | case $y in 511 | 1) continue ;; 512 | esac 513 | # Normal mode v.s. Search mode 514 | case $token in 515 | '/') 516 | IFS=${ifs:=$nl} 517 | # shellcheck disable=2086 518 | set -- $filt_out 519 | unset IFS 520 | ;; 521 | '') 522 | IFS=${ifs:=$nl} 523 | # shellcheck disable=2086 524 | set -- $content 525 | unset IFS 526 | esac 527 | y=1 yy=1 y2=$top pos=$top 528 | redraw "$@" 529 | return 0 530 | ;; 531 | G?|F2|"$ctrl_e"?) # G / End / Ctrl-e 532 | # Terminal condition 533 | case $y in 534 | "$last") continue ;; 535 | esac 536 | # Normal mode v.s. Search mode 537 | case $token in 538 | '/') 539 | IFS=${ifs:=$nl} 540 | # shellcheck disable=2086 541 | set -- $filt_out 542 | unset IFS 543 | ;; 544 | '') 545 | IFS=${ifs:=$nl} 546 | # shellcheck disable=2086 547 | set -- $content 548 | unset IFS 549 | esac 550 | y=$last yy=$len pos=$(( num * y - num + top )) 551 | y2=$(( pos < fin - (num - 1) ? pos : fin - (num - 1) )) 552 | redraw "$@" 553 | return 0 554 | ;; 555 | l?|C2|"$esc") # l / Arrow Right / Esc 556 | case $token in 557 | '?') continue ;; 558 | *) 559 | term_reset 560 | return 1 561 | ;; 562 | esac 563 | ;; 564 | 565 | h?|D2|"$bs_char"?) # h / Arrow Left / Backspace 566 | case $token in 567 | '?'|'/') 568 | unset token filt_out ans 569 | old_reset 570 | IFS=${ifs:=$nl} 571 | # shellcheck disable=2086 572 | set -- $content 573 | unset IFS 574 | y=1 yy=1 y2=$top 575 | redraw "$@" 576 | return 0 577 | ;; 578 | esac 579 | ;; 580 | 581 | /?) # / 582 | count=0 bound=$width 583 | old_save 584 | while :; do 585 | case $live_search in 586 | '') 587 | token='/' 588 | prompt / r 589 | ;; 590 | 1) 591 | token='l' 592 | prompt "/$ans" l 593 | case $press${esc:=0} in 594 | "$esc") # Enter 595 | case $cur in 596 | 'no result') continue ;; 597 | *) 598 | ltype="Searching..." 599 | status_line "$last" 600 | ltype="Search mode" 601 | token='/' 602 | case $# in 603 | 1) # only one result, directly output it 604 | term_reset 605 | return 1 606 | ;; 607 | esac 608 | ;; 609 | esac 610 | ;; 611 | "$bs_char"?) # Backspace 612 | case $ans in 613 | '') continue ;; # Inactive when no ans 614 | *) 615 | ans=${ans%%?} 616 | count=0 617 | unset complist 618 | ;; 619 | esac 620 | ;; 621 | "$tab"?) # Tab 622 | case $count in 623 | 0) # First run, record complist & $# 624 | complist=$filt_out 625 | IFS=${ifs:=$nl} 626 | # shellcheck disable=2086 627 | set -- $complist 628 | unset IFS 629 | tot=$# 630 | count=$(( count + 1 <= $# ? count + 1 : $# )) 631 | tab_complete "$@" 632 | ;; 633 | *) # Other run, compare count and $# 634 | case $(( count - tot )) in 635 | -*) 636 | IFS=${ifs:=$nl} 637 | # shellcheck disable=2086 638 | set -- $complist 639 | unset IFS 640 | count=$(( count + 1 <= $# ? count + 1 : $# )) 641 | tab_complete "$@" 642 | ;; 643 | *) continue ;; 644 | esac 645 | ;; 646 | esac 647 | ;; 648 | Z2) # Shift-Tab 649 | case $count in 650 | 0) # First run, record complist 651 | complist=$filt_out 652 | IFS=${ifs:=$nl} 653 | # shellcheck disable=2086 654 | set -- $complist 655 | unset IFS 656 | count=$(( count - 1 >= 1 ? count - 1 : 1 )) 657 | tab_complete "$@" 658 | esc=0 659 | ;; 660 | *) # Other run, compare count and 1 661 | case $(( count - 1 )) in 662 | -*|0) esc=0; continue ;; 663 | *) 664 | IFS=${ifs:=$nl} 665 | # shellcheck disable=2086 666 | set -- $complist 667 | unset IFS 668 | count=$(( count - 1 >= 1 ? count - 1 : 1 )) 669 | tab_complete "$@" 670 | esc=0 671 | ;; 672 | esac 673 | ;; 674 | esac 675 | ;; 676 | "$esc_c"*) esc=1; continue ;; 677 | '[1') esc=2; continue ;; 678 | [[:cntrl:]]?) esc=0; continue ;; # Do not accept other control char 679 | *?) # Others 680 | esc=0 681 | ans="$ans$press" 682 | count=0 683 | unset complist 684 | ;; 685 | esac 686 | ;; 687 | esac 688 | search "$content" "$ifs" "$ans" 689 | # redraw if different & not Enter 690 | [ "$filt_out" = "$last_filt_out" ] && [ -n "$press" ] && continue 691 | IFS=${ifs:=$nl} 692 | # shellcheck disable=2086 693 | set -- $filt_out 694 | unset IFS 695 | msg="Search by *${ans%%$nl*}*"; 696 | case $# in 697 | 0) num=1; set -- 'no result' ;; 698 | esac 699 | case $token in 700 | l) 701 | last='??' 702 | last_filt_out="$filt_out" 703 | redraw "$@" 704 | ;; 705 | /) 706 | last=$# 707 | len=$(( $# > 500 ? 500 : $# )) 708 | # num=$old_num 709 | y=1 yy=1 y2=$top pos=$top 710 | term_getsize 711 | term_setup 712 | redraw "$@" 713 | break 714 | ;; 715 | esac 716 | done 717 | unset count complist bound last_filt_out 718 | return 0 719 | ;; 720 | 721 | \??) # ? 722 | set -- 'k/↑/Ctrl-p - up' \ 723 | 'j/↓/Ctrl-n - down' \ 724 | 'l/→ - right' \ 725 | 'h/← - left' \ 726 | 'Ctrl-f/PageDown - PageDown' \ 727 | 'Ctrl-u/PageUp - PageUp' \ 728 | 'g/Home/Ctrl-a - go to top' \ 729 | 'G/End/Ctrl-e - go to bottom' \ 730 | '/ - search' \ 731 | ' live-search detail:' \ 732 | ' Enter - confirm' \ 733 | ' Backspace - delete previous character' \ 734 | ' Tab - Tab-completion forward' \ 735 | ' Shift-Tab - Tab-completion backward' \ 736 | ' control char ignore' \ 737 | ' others print out' \ 738 | '? - show keybinds' \ 739 | 'q - quit' 740 | 741 | unset format 742 | old_save 743 | num=1; y=1; yy=1; y2=$top; last=$#; len=$#; ltype=""; msg=keybinds; token='?'; 744 | redraw "$@" 745 | ;; 746 | 747 | q?) # q 748 | term_exit 749 | exit 0 750 | ;; 751 | "$esc_c"*) esc=1 ;; 752 | '[1') esc=2 ;; 753 | 5?) esc=5 ;; # PageUp 754 | 6?) esc=6 ;; # PageDown 755 | *) esc=0 ;; 756 | esac 757 | done 758 | } 759 | 760 | main() { 761 | 762 | set -e 763 | 764 | trap 'term_exit; exit 0' INT QUIT 765 | trap 'term_getsize; term_setup; y=1; yy=1; y2=$top; pos=$top; redraw "$@"' WINCH 766 | 767 | term_getsize 768 | term_init 769 | y=1 yy=1 y2=$top pos=$top 770 | 771 | IFS=${ifs:=$nl} 772 | # shellcheck disable=2086 773 | set -- $content 774 | unset IFS 775 | last=$# 776 | len=$(( $# > 500 ? 500 : $# )) 777 | 778 | input_assign "0" "$len" "$@" 779 | 780 | IFS=${ifs:=$nl} 781 | # shellcheck disable=2086 782 | set -- $list 783 | unset IFS 784 | 785 | redraw "$@" 786 | 787 | # shellcheck disable=2181 788 | while key "$@"; do 789 | case $token in 790 | '/') # Search mode 791 | IFS=${ifs:=$nl} 792 | # shellcheck disable=2086 793 | set -- $filt_out 794 | unset IFS 795 | ;; 796 | '') # Normal 797 | IFS=${ifs:=$nl} 798 | # shellcheck disable=2086 799 | set -- $content 800 | unset IFS 801 | esac 802 | done 803 | } 804 | 805 | usage () { 806 | cat << EOF 807 | Usage: 808 | 809 | shellect [OPTIONS] ([ARGS]) 810 | 811 | -h, Show help options 812 | -i, Set case-insensitive search 813 | -l, Set live-search 814 | -n=[num], Set numbers of line per entry 815 | -d=[delim], Set delimiter (IFS, internal field separator) 816 | -c=[content], Set content to display 817 | -f=[format], Set the format to print out content 818 | -t=[msg], Set top status bar message 819 | -b=[msg], Set bottom status bar message 820 | 821 | format detail: 822 | nldel delete last nl, equiv to "\${1%\$nl}" 823 | basename only print basename, equiv to "\${1##*/}" ;; 824 | 825 | if unset or empty, then equiv to "\$1" 826 | 827 | live-search detail: 828 | Enter confirm 829 | Backspace delete previous character 830 | Tab Tab-completion forward 831 | Shift-Tab Tab-completion backward 832 | control char ignore 833 | others print out 834 | EOF 835 | } 836 | 837 | 838 | nl=' 839 | ' 840 | tab=' ' 841 | 842 | # special key setting 843 | esc_c=$(printf '\033') 844 | bs_char=$(printf '\177') 845 | ctrl_f=$(printf '\006') 846 | ctrl_u=$(printf '\025') 847 | ctrl_n=$(printf '\016') 848 | ctrl_p=$(printf '\020') 849 | ctrl_a=$(printf '\001') 850 | ctrl_e=$(printf '\005') 851 | 852 | while getopts "t:b:n:d:c:f:hil" result; do 853 | case "${result}" in 854 | n) num=${OPTARG} ;; 855 | d) ifs=${OPTARG} ;; 856 | c) content=${OPTARG} ;; 857 | f) format=${OPTARG} ;; 858 | i) case_insense=1 ;; 859 | l) live_search=1 ;; 860 | t) msg="${OPTARG}" ;; 861 | b) ltype="${OPTARG}" ;; 862 | h) usage && exit 0 ;; 863 | *) printf 'Invalid argument' && exit 0 ;; 864 | esac 865 | done 866 | 867 | case "$content" in 868 | '') content=$(cat -u -) ;; # Accept pipe stdin 869 | esac 870 | 871 | num=${num:=1} # num=1 if unset 872 | main <&2 >/dev/tty # why it works, I don't know. 873 | printf '%s' "$cur" >&1 874 | --------------------------------------------------------------------------------