├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── cache.go ├── cache_test.go ├── codes.go ├── cursor.go ├── editor.go ├── editor_js.go ├── editor_plan9.go ├── editor_unix.go ├── editor_windows.go ├── errors.go ├── events.go ├── examples └── 01-getting-started │ ├── LICENSE │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── find.go ├── go.mod ├── go.sum ├── godoc.go ├── hint.go ├── history.go ├── hotkey_functions.go ├── hotkey_recall.go ├── instance.go ├── preview.go ├── preview_test.go ├── raw └── LICENSE ├── raw_bsd.go ├── raw_js.go ├── raw_linux.go ├── raw_plan9.go ├── raw_solaris.go ├── raw_unix.go ├── raw_windows.go ├── read.go ├── read_js.go ├── read_nojs.go ├── read_test.go ├── readline.go ├── runecache.go ├── signal_fallback.go ├── signal_unix.go ├── suggestions.go ├── syntax.go ├── tab.go ├── tabfind.go ├── tabgrid.go ├── tabgrid_test.go ├── tabmap.go ├── term.go ├── test_notty ├── go.mod ├── go.sum └── main.go ├── timer.go ├── tokenise.go ├── undo.go ├── unicode.go ├── unicode_test.go ├── update.go ├── vim.go ├── vim_command.go ├── vim_delete.go ├── vim_delete_test.go ├── write.go ├── write_js.go ├── write_test.go └── write_tty.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled ELFs / Win PEs 2 | main 3 | main.exe 4 | 5 | # Misc 6 | .directory 7 | /vender 8 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # lmorg/readline 2 | 3 | v4.0.0 marks a breaking change to the tab completion function. 4 | 5 | Earlier versions expected multiple parameters to be returned however from 6 | v4.0.0 onwards, a pointer to a structure is instead expected: 7 | ``` 8 | type TabCompleterReturnT struct { 9 | Prefix string 10 | Suggestions []string 11 | Descriptions map[string]string 12 | DisplayType TabDisplayType 13 | HintCache HintCacheFuncT 14 | Preview PreviewFuncT 15 | } 16 | ``` 17 | This allows for more configurability and without the cost of copying multiple 18 | different pieces of data nor future breaking changes whenever additional new 19 | features are added. 20 | 21 | ## Changes 22 | 23 | ### 4.1.0 24 | 25 | * Murex has switched back to calling this package for `readline`, meaning this 26 | package will now see more regular updates and bug fixes 27 | 28 | * bugfix: cursor wouldn't step backwards in VIM mode when cursor at end of line 29 | 30 | * experimental support added for integrating `readline` into GUI applications 31 | 32 | ### 4.0.0 33 | 34 | * support for wide and zero width unicode characters 35 | ([inherited from Murex](https://murex.rocks/changelog/v4.0.html)) 36 | 37 | * preview modes 38 | ([inherited from Murex](https://murex.rocks/user-guide/interactive-shell.html#preview)) 39 | 40 | * API improvements 41 | 42 | * rewritten event system 43 | ([discussion](https://github.com/lmorg/murex/discussions/799)) 44 | 45 | * vastly improved buffered rendering -- this leads to few rendering glitches 46 | and particularly on slower machines and/or terminals 47 | 48 | * added missing vim and emacs keybindings 49 | ([full list of keybindings](listhttps://murex.rocks/user-guide/terminal-keys.html)) 50 | 51 | * additional tests 52 | 53 | * fixed glitches on Windows terminals 54 | ([discussion](https://github.com/lmorg/murex/issues/630)) 55 | 56 | * readline command mode 57 | ([discussion](https://github.com/lmorg/murex/discussions/905)) 58 | 59 | ### 3.0.1 60 | 61 | This is a bug fix release: 62 | 63 | * Nil map panic fixed when using dtx.AppendSuggestions() 64 | 65 | * Hint text line proper blanked (this is a fix to a regression bug introduced 66 | in version 3.0.0) 67 | 68 | * Example 01 updated to reflect API changes in 3.0.0 69 | 70 | ### 3.0.0 71 | 72 | This release brings a considerable number of new features and bug fixes 73 | inherited from readline's use in murex (https://github.com/lmorg/murex) 74 | 75 | * Wrapped lines finally working (where the input line is longer than the 76 | terminal width) 77 | 78 | * Delayed tab completion - allows asynchronous updates to the tab completion so 79 | slower suggestions do not halt the user experience 80 | 81 | * Delayed syntax timer - allows syntax highlighting to run asynchronously for 82 | slower parsers (eg spell checkers) 83 | 84 | * Support for GetCursorPos ANSI escape sequence (though I don't have a terminal 85 | which supports this to test the code on) 86 | 87 | * Better support for wrapped hint text lines 88 | 89 | * Fixed bug with $EDITOR error handling in Windows and Plan 9 90 | 91 | * Code clean up - fewer writes to the terminal 92 | 93 | If you just use the exported API end points then your code should still work 94 | verbatim. However if you are working against a fork or custom patch set then 95 | considerable more work may be required to merge the changes. 96 | 97 | ### 2.1.0 98 | 99 | Error returns from `readline` have been created as error a variable, which is 100 | more idiomatic to Go than the err constants that existed previously. Currently 101 | both are still available to use however I will be deprecating the the constants 102 | in a latter release. 103 | 104 | **Deprecated constants:** 105 | ```go 106 | const ( 107 | // ErrCtrlC is returned when ctrl+c is pressed 108 | ErrCtrlC = "Ctrl+C" 109 | 110 | // ErrEOF is returned when ctrl+d is pressed 111 | ErrEOF = "EOF" 112 | ) 113 | ``` 114 | 115 | **New error variables:** 116 | ```go 117 | var ( 118 | // CtrlC is returned when ctrl+c is pressed 119 | CtrlC = errors.New("Ctrl+C") 120 | 121 | // EOF is returned when ctrl+d is pressed 122 | // (this is actually the same value as io.EOF) 123 | EOF = errors.New("EOF") 124 | ) 125 | ``` 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lmorg/readline 2 | 3 | ## Import Path 4 | 5 | From CLI: 6 | ``` 7 | go get github.com/lmorg/readline/v4 8 | ``` 9 | 10 | From Go code: 11 | ``` 12 | import "github.com/lmorg/readline/v4" 13 | ``` 14 | 15 | ## Preface 16 | 17 | This package provides the interactive capabilities for various command line 18 | interfaces such as the shell [Murex](https://murex.rocks). 19 | 20 | It also has experimental support for handling text input operations in GUI 21 | applications. However its primary focus is as a "batteries included" 22 | CLI input library. 23 | 24 | ## `readline` In Action 25 | 26 | [![asciicast](https://asciinema.org/a/232714.svg)](https://asciinema.org/a/232714) 27 | 28 | This is a very rough and ready recording but it does demonstrate a few of the 29 | features available in `readline`. These features include: 30 | * hint text (the blue status text below the prompt - however the colour is 31 | configurable) 32 | * syntax highlighting (albeit there isn't much syntax to highlight in the 33 | example) 34 | * tab-completion in gridded mode (seen when typing `cd`) 35 | * tab-completion in list view (seen when selecting an process name to `kill` 36 | and the process ID was substituted when selected) 37 | * regex searching through the tab-completion suggestions (seen in both `cd` and 38 | `kill` - enabled by pressing `[CTRL+f]`) 39 | * line editing using `$EDITOR` (`vi` in the example - enabled by pressing 40 | `[ESC]` followed by `[v]`) 41 | * `readline`'s warning before pasting multiple lines of data into the buffer 42 | * the preview option that's available as part of the aforementioned warning 43 | * and VIM keys (enabled by pressing `[ESC]`) 44 | 45 | ## Example Code 46 | 47 | Using `readline` is as simple as: 48 | 49 | ```go 50 | func main() { 51 | rl := readline.NewInstance() 52 | 53 | for { 54 | line, err := rl.Readline() 55 | 56 | if err != nil { 57 | fmt.Println("Error:", err) 58 | return 59 | } 60 | 61 | fmt.Printf("You just typed: '%s'\n", line) 62 | } 63 | } 64 | ``` 65 | 66 | However I suggest you read through the examples in `/examples` for help using 67 | some of the more advanced features in `readline`. 68 | 69 | The source for `readline` will also be documented in godoc: https://godoc.org/github.com/lmorg/readline/v4 70 | 71 | ## Version Information 72 | 73 | Because the last thing a developer wants is to do is fix breaking changes after 74 | updating modules, I will make the following guarantees: 75 | 76 | * The version string will be based on Semantic Versioning. ie version numbers 77 | will be formatted `(major).(minor).(patch)` - for example `2.0.1` 78 | 79 | * `major` releases _will_ have breaking changes. Be sure to read CHANGES.md for 80 | upgrade instructions 81 | 82 | * `minor` releases will contain new APIs or introduce new user facing features 83 | which may affect useability from an end user perspective. However `minor` 84 | releases will not break backwards compatibility at the source code level and 85 | nor will it break existing expected user-facing behavior. These changes will 86 | be documented in CHANGES.md too 87 | 88 | * `patch` releases will be bug fixes and such like. Where the code has changed 89 | but both API endpoints and user experience will remain the same (except where 90 | expected user experience was broken due to a bug, then that would be bumped 91 | to either a `minor` or `major` depending on the significance of the bug and 92 | the significance of the change to the user experience) 93 | 94 | * Any updates to documentation, comments within code or the example code will 95 | not result in a version bump because they will not affect the output of the 96 | go compiler. However if this concerns you then I recommend pinning your 97 | project to the git commit hash rather than a `patch` release 98 | 99 | My recommendation is to pin to either the `minor` or `patch` release and I will 100 | endeavour to keep breaking changes to an absolute minimum. 101 | 102 | ## Hotkeys 103 | 104 | `readline` hotkeys can be found in [Murex's documentation](https://murex.rocks/user-guide/terminal-keys.html). 105 | While some of that document will be specific to Murex, the vast majority of 106 | Murex's interactive capabilities is leveraged via this `readline` package. 107 | 108 | ## Change Log 109 | 110 | v4.0.0 marks a breaking change to the tab completion function. 111 | 112 | Earlier versions expected multiple parameters to be returned however from 113 | v4.0.0 onwards, a pointer to a structure is instead expected: 114 | ``` 115 | type TabCompleterReturnT struct { 116 | Prefix string 117 | Suggestions []string 118 | Descriptions map[string]string 119 | DisplayType TabDisplayType 120 | HintCache HintCacheFuncT 121 | Preview PreviewFuncT 122 | } 123 | ``` 124 | This allows for more configurability and without the cost of copying multiple 125 | different pieces of data nor future breaking changes whenever additional new 126 | features are added. 127 | 128 | The full changelog can be viewed at [CHANGES.md](CHANGES.md) 129 | 130 | ## License Information 131 | 132 | `readline` is licensed Apache 2.0. All the example code and documentation in 133 | `/examples` is public domain. 134 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "sync" 4 | 5 | //acSuggestions map[string][]string 6 | //acDescriptions map[string]map[string]string 7 | 8 | // string 9 | 10 | type cacheString struct { 11 | mutex sync.Mutex 12 | indexes []string 13 | size int 14 | values map[string]string 15 | } 16 | 17 | func (c *cacheString) Init(rl *Instance) { 18 | c.mutex.Lock() 19 | 20 | c.indexes = make([]string, rl.MaxCacheSize) 21 | c.size = 0 22 | c.values = make(map[string]string) 23 | 24 | c.mutex.Unlock() 25 | } 26 | 27 | func (c *cacheString) Append(line []rune, v string) { 28 | sLine := string(line) 29 | c.mutex.Lock() 30 | 31 | if c.size == len(c.indexes)-1 { 32 | delete(c.values, c.indexes[0]) 33 | c.indexes = append(c.indexes[1:], "") 34 | } else { 35 | c.size++ 36 | } 37 | 38 | c.indexes[c.size] = sLine 39 | c.values[sLine] = v 40 | 41 | c.mutex.Unlock() 42 | } 43 | 44 | func (c *cacheString) Get(line []rune) string { 45 | sLine := string(line) 46 | c.mutex.Lock() 47 | 48 | v := c.values[sLine] 49 | 50 | c.mutex.Unlock() 51 | return v 52 | } 53 | 54 | // []rune 55 | 56 | type cacheSliceRune struct { 57 | mutex sync.Mutex 58 | indexes []string 59 | size int 60 | values map[string][]rune 61 | } 62 | 63 | func (c *cacheSliceRune) Init(rl *Instance) { 64 | c.mutex.Lock() 65 | 66 | c.indexes = make([]string, rl.MaxCacheSize) 67 | c.size = 0 68 | c.values = make(map[string][]rune) 69 | 70 | c.mutex.Unlock() 71 | } 72 | 73 | func (c *cacheSliceRune) Append(line, v []rune) { 74 | sLine := string(line) 75 | c.mutex.Lock() 76 | 77 | if c.size == len(c.indexes)-1 { 78 | delete(c.values, c.indexes[0]) 79 | c.indexes = append(c.indexes[1:], "") 80 | } else { 81 | c.size++ 82 | } 83 | 84 | c.indexes[c.size] = sLine 85 | c.values[sLine] = v 86 | 87 | c.mutex.Unlock() 88 | } 89 | 90 | func (c *cacheSliceRune) Get(line []rune) []rune { 91 | sLine := string(line) 92 | c.mutex.Lock() 93 | 94 | v := c.values[sLine] 95 | 96 | c.mutex.Unlock() 97 | return v 98 | } 99 | 100 | // Slice 101 | 102 | // Map 103 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // string 9 | 10 | func TestCacheStringMethods(t *testing.T) { 11 | rl := NewInstance() 12 | rl.MaxCacheSize = 10 13 | c := new(cacheString) 14 | c.Init(rl) 15 | 16 | for i := 0; i < 20; i++ { 17 | sLine := fmt.Sprintf("line %d", i) 18 | rLine := []rune(sLine) 19 | sValue := fmt.Sprintf("value %d", i) 20 | 21 | r := c.Get(rLine) 22 | if string(r) == sValue { 23 | t.Fatalf(`Pre-Append error: c.Get(%s) == "%s"`, sLine, string(r)) 24 | } 25 | 26 | c.Append(rLine, sValue) 27 | 28 | r = c.Get(rLine) 29 | if string(r) != sValue { 30 | t.Fatalf(`Post-Append error: c.Get(%s) == "%s"`, sLine, string(r)) 31 | } 32 | } 33 | 34 | c.Init(rl) 35 | if c.size != 0 || len(c.values["line 1"]) > 0 { 36 | t.Error("cacheString failed to reinitialize correctly") 37 | t.Logf(" size: %d", c.size) 38 | t.Logf(` c.values["line 1"]: "%s"`, string(c.values["line 1"])) 39 | } 40 | } 41 | 42 | func TestCacheStringOutOfBounds(t *testing.T) { 43 | rl := NewInstance() 44 | rl.MaxCacheSize = 10 45 | 46 | c := new(cacheString) 47 | c.Init(rl) 48 | 49 | for i := 0; i < 20; i++ { 50 | line := []rune(fmt.Sprintf("line %d", i)) 51 | value := fmt.Sprintf("value %d", i) 52 | c.Append(line, value) 53 | } 54 | } 55 | 56 | // []rune 57 | 58 | func TestCacheSliceRuneMethods(t *testing.T) { 59 | rl := NewInstance() 60 | rl.MaxCacheSize = 10 61 | c := new(cacheSliceRune) 62 | c.Init(rl) 63 | 64 | for i := 0; i < 20; i++ { 65 | sLine := fmt.Sprintf("line %d", i) 66 | rLine := []rune(sLine) 67 | sValue := fmt.Sprintf("value %d", i) 68 | rValue := []rune(sValue) 69 | 70 | r := c.Get(rLine) 71 | if string(r) == sValue { 72 | t.Fatalf(`Pre-Append error: c.Get(%s) == "%s"`, sLine, string(r)) 73 | } 74 | 75 | c.Append(rLine, rValue) 76 | 77 | r = c.Get(rLine) 78 | if string(r) != sValue { 79 | t.Fatalf(`Post-Append error: c.Get(%s) == "%s"`, sLine, string(r)) 80 | } 81 | } 82 | 83 | c.Init(rl) 84 | if c.size != 0 || len(c.values["line 1"]) > 0 { 85 | t.Error("cacheString failed to reinitialize correctly") 86 | t.Logf(" size: %d", c.size) 87 | t.Logf(` c.values["line 1"]: "%s"`, string(c.values["line 1"])) 88 | } 89 | } 90 | 91 | func TestCacheSlineRuneOutOfBounds(t *testing.T) { 92 | rl := NewInstance() 93 | rl.MaxCacheSize = 10 94 | 95 | c := new(cacheSliceRune) 96 | c.Init(rl) 97 | 98 | for i := 0; i < 20; i++ { 99 | line := []rune(fmt.Sprintf("line %d", i)) 100 | value := []rune(fmt.Sprintf("value %d", i)) 101 | c.Append(line, value) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /codes.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | // Character codes 4 | const ( 5 | charCtrlA = iota + 1 6 | charCtrlB 7 | charCtrlC 8 | charEOF 9 | charCtrlE 10 | charCtrlF 11 | charCtrlG 12 | charBackspace // ISO 646 13 | charTab 14 | charCtrlJ 15 | charCtrlK 16 | charCtrlL 17 | charCtrlM 18 | charCtrlN 19 | charCtrlO 20 | charCtrlP 21 | charCtrlQ 22 | charCtrlR 23 | charCtrlS 24 | charCtrlT 25 | charCtrlU 26 | charCtrlV 27 | charCtrlW 28 | charCtrlX 29 | charCtrlY 30 | charCtrlZ 31 | charEscape 32 | charCtrlSlash // ^\ 33 | charCtrlCloseSquare // ^] 34 | charCtrlHat // ^^ 35 | charCtrlUnderscore // ^_ 36 | charBackspace2 = 127 // ASCII 1963 37 | ) 38 | 39 | // Escape sequences 40 | var ( 41 | seqUp = string([]byte{27, 91, 65}) 42 | seqDown = string([]byte{27, 91, 66}) 43 | seqForwards = string([]byte{27, 91, 67}) 44 | seqBackwards = string([]byte{27, 91, 68}) 45 | seqHome = string([]byte{27, 91, 72}) 46 | seqHomeSc = string([]byte{27, 91, 49, 126}) 47 | seqEnd = string([]byte{27, 91, 70}) 48 | seqEndSc = string([]byte{27, 91, 52, 126}) 49 | seqDelete = string([]byte{27, 91, 51, 126}) 50 | seqShiftTab = string([]byte{27, 91, 90}) 51 | seqPageUp = string([]byte{27, 91, 53, 126}) 52 | seqPageDown = string([]byte{27, 91, 54, 126}) 53 | seqOptUp = string([]byte{27, 27, 91, 65}) 54 | seqOptDown = string([]byte{27, 27, 91, 66}) 55 | seqOptLeft = string([]byte{27, 27, 91, 68}) 56 | seqOptRight = string([]byte{27, 27, 91, 67}) 57 | seqCtrlUp = string([]byte{27, 91, 49, 59, 53, 65}) 58 | seqCtrlDown = string([]byte{27, 91, 49, 59, 53, 66}) 59 | seqCtrlLeft = string([]byte{27, 91, 49, 59, 53, 68}) 60 | seqCtrlRight = string([]byte{27, 91, 49, 59, 53, 67}) 61 | 62 | seqF1VT100 = string([]byte{27, 79, 80}) 63 | seqF2VT100 = string([]byte{27, 79, 81}) 64 | seqF3VT100 = string([]byte{27, 79, 82}) 65 | seqF4VT100 = string([]byte{27, 79, 83}) 66 | seqF1 = string([]byte{27, 91, 49, 49, 126}) 67 | seqF2 = string([]byte{27, 91, 49, 50, 126}) 68 | seqF3 = string([]byte{27, 91, 49, 51, 126}) 69 | seqF4 = string([]byte{27, 91, 49, 52, 126}) 70 | seqF5 = string([]byte{27, 91, 49, 53, 126}) 71 | seqF6 = string([]byte{27, 91, 49, 55, 126}) 72 | seqF7 = string([]byte{27, 91, 49, 56, 126}) 73 | seqF8 = string([]byte{27, 91, 49, 57, 126}) 74 | seqF9 = string([]byte{27, 91, 50, 48, 126}) 75 | seqF10 = string([]byte{27, 91, 50, 49, 126}) 76 | seqF11 = string([]byte{27, 91, 50, 51, 126}) 77 | seqF12 = string([]byte{27, 91, 50, 52, 126}) 78 | 79 | seqShiftF1 = string([]byte{27, 91, 49, 59, 50, 80}) 80 | seqShiftF2 = string([]byte{27, 91, 49, 59, 50, 81}) 81 | seqShiftF3 = string([]byte{27, 91, 49, 59, 50, 82}) 82 | seqShiftF4 = string([]byte{27, 91, 49, 59, 50, 83}) 83 | seqShiftF5 = string([]byte{27, 91, 49, 53, 59, 50, 126}) 84 | seqShiftF6 = string([]byte{27, 91, 49, 55, 59, 50, 126}) 85 | seqShiftF7 = string([]byte{27, 91, 49, 56, 59, 50, 126}) 86 | seqShiftF8 = string([]byte{27, 91, 49, 57, 59, 50, 126}) 87 | seqShiftF9 = string([]byte{27, 91, 50, 48, 59, 50, 126}) 88 | seqShiftF10 = string([]byte{27, 91, 50, 49, 59, 50, 126}) 89 | seqShiftF11 = string([]byte{27, 91, 50, 51, 59, 50, 126}) 90 | seqShiftF12 = string([]byte{27, 91, 50, 52, 59, 50, 126}) 91 | ) 92 | 93 | const ( 94 | //seqPosSave = "\x1b[s" 95 | //seqPosRestore = "\x1b[u" 96 | 97 | seqClearLineAfter = "\x1b[0K" 98 | seqClearLineBefore = "\x1b[1K" 99 | seqClearLine = "\x1b[2K" 100 | seqClearScreenBelow = "\x1b[J" 101 | seqClearScreen = "\x1b[2J" 102 | 103 | seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R" 104 | 105 | seqSetCursorPosTopLeft = "\x1b[1;1H" 106 | seqSaveBuffer = "\x1b[?47h" 107 | seqRestoreBuffer = "\x1b[?47l" //+ curPosSave 108 | ) 109 | 110 | // Text effects 111 | const ( 112 | seqReset = "\x1b[0m" 113 | seqBold = "\x1b[1m" 114 | seqUnderscore = "\x1b[4m" 115 | seqBlink = "\x1b[5m" 116 | ) 117 | 118 | // Text colours 119 | const ( 120 | seqFgBlack = "\x1b[30m" 121 | seqFgRed = "\x1b[31m" 122 | seqFgGreen = "\x1b[32m" 123 | seqFgYellow = "\x1b[33m" 124 | seqFgBlue = "\x1b[34m" 125 | seqFgMagenta = "\x1b[35m" 126 | seqFgCyan = "\x1b[36m" 127 | seqFgWhite = "\x1b[37m" 128 | 129 | /*seqFgBlackBright = "\x1b[1;30m" 130 | seqFgRedBright = "\x1b[1;31m" 131 | seqFgGreenBright = "\x1b[1;32m" 132 | seqFgYellowBright = "\x1b[1;33m" 133 | seqFgBlueBright = "\x1b[1;34m" 134 | seqFgMagentaBright = "\x1b[1;35m" 135 | seqFgCyanBright = "\x1b[1;36m" 136 | seqFgWhiteBright = "\x1b[1;37m"*/ 137 | ) 138 | 139 | // Background colours 140 | const ( 141 | seqBgBlack = "\x1b[40m" 142 | seqBgRed = "\x1b[41m" 143 | seqBgGreen = "\x1b[42m" 144 | seqBgYellow = "\x1b[43m" 145 | seqBgBlue = "\x1b[44m" 146 | seqBgMagenta = "\x1b[45m" 147 | seqBgCyan = "\x1b[46m" 148 | seqBgWhite = "\x1b[47m" 149 | 150 | /*seqBgBlackBright = "\x1b[1;40m" 151 | seqBgRedBright = "\x1b[1;41m" 152 | seqBgGreenBright = "\x1b[1;42m" 153 | seqBgYellowBright = "\x1b[1;43m" 154 | seqBgBlueBright = "\x1b[1;44m" 155 | seqBgMagentaBright = "\x1b[1;45m" 156 | seqBgCyanBright = "\x1b[1;46m" 157 | seqBgWhiteBright = "\x1b[1;47m"*/ 158 | ) 159 | 160 | const ( 161 | seqEscape = "\x1b" 162 | 163 | // generated using: 164 | // a [a..z] -> foreach c { -> tr [:lower:] [:upper:] -> set C; out (seqAlt$C = "\x1b$c") } 165 | seqAltA = "\x1ba" 166 | seqAltB = "\x1bb" 167 | seqAltC = "\x1bc" 168 | seqAltD = "\x1bd" 169 | seqAltE = "\x1be" 170 | seqAltF = "\x1bf" 171 | seqAltG = "\x1bg" 172 | seqAltH = "\x1bh" 173 | seqAltI = "\x1bi" 174 | seqAltJ = "\x1bj" 175 | seqAltK = "\x1bk" 176 | seqAltL = "\x1bl" 177 | seqAltM = "\x1bm" 178 | seqAltN = "\x1bn" 179 | seqAltO = "\x1bo" 180 | seqAltP = "\x1bp" 181 | seqAltQ = "\x1bq" 182 | seqAltR = "\x1br" 183 | seqAltS = "\x1bs" 184 | seqAltT = "\x1bt" 185 | seqAltU = "\x1bu" 186 | seqAltV = "\x1bv" 187 | seqAltW = "\x1bw" 188 | seqAltX = "\x1bx" 189 | seqAltY = "\x1by" 190 | seqAltZ = "\x1bz" 191 | 192 | /*seqAltShiftA = "\x1bA" 193 | seqAltShiftB = "\x1bB" 194 | seqAltShiftC = "\x1bC" 195 | seqAltShiftD = "\x1bD" 196 | seqAltShiftE = "\x1bE" 197 | seqAltShiftF = "\x1bF" 198 | seqAltShiftG = "\x1bG" 199 | seqAltShiftH = "\x1bH" 200 | seqAltShiftI = "\x1bI" 201 | seqAltShiftJ = "\x1bJ" 202 | seqAltShiftK = "\x1bK" 203 | seqAltShiftL = "\x1bL" 204 | seqAltShiftM = "\x1bM" 205 | seqAltShiftN = "\x1bN" 206 | seqAltShiftO = "\x1bO" 207 | seqAltShiftP = "\x1bP" 208 | seqAltShiftQ = "\x1bQ" 209 | seqAltShiftR = "\x1bR" 210 | seqAltShiftS = "\x1bS" 211 | seqAltShiftT = "\x1bT" 212 | seqAltShiftU = "\x1bU" 213 | seqAltShiftV = "\x1bV" 214 | seqAltShiftW = "\x1bW" 215 | seqAltShiftX = "\x1bX" 216 | seqAltShiftY = "\x1bY" 217 | seqAltShiftZ = "\x1bZ"*/ 218 | ) 219 | -------------------------------------------------------------------------------- /cursor.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | func (rl *Instance) leftMost() []byte { 11 | if rl.isNoTty { 12 | return []byte{} 13 | } 14 | 15 | fd := int(os.Stdout.Fd()) 16 | w, _, err := GetSize(fd) 17 | if err != nil { 18 | return []byte{'\r', '\n'} 19 | } 20 | 21 | b := make([]byte, w+1) 22 | for i := 0; i < w; i++ { 23 | b[i] = ' ' 24 | } 25 | b[w] = '\r' 26 | 27 | return b 28 | } 29 | 30 | var rxRcvCursorPos = regexp.MustCompile("^\x1b([0-9]+);([0-9]+)R$") 31 | 32 | func (rl *Instance) getCursorPos() (x int, y int) { 33 | if rl.isNoTty { 34 | return 0, 0 35 | } 36 | 37 | if !ForceCrLf { 38 | return 0, 0 39 | } 40 | 41 | if !rl.EnableGetCursorPos { 42 | return -1, -1 43 | } 44 | 45 | disable := func() (int, int) { 46 | rl.printErr("\r\ngetCursorPos() not supported by terminal emulator, disabling....\r\n") 47 | rl.EnableGetCursorPos = false 48 | return -1, -1 49 | } 50 | 51 | rl.print(seqGetCursorPos) 52 | b := make([]byte, 64) 53 | i, err := rl.read(b) 54 | if err != nil { 55 | return disable() 56 | } 57 | 58 | if !rxRcvCursorPos.Match(b[:i]) { 59 | return disable() 60 | } 61 | 62 | match := rxRcvCursorPos.FindAllStringSubmatch(string(b[:i]), 1) 63 | y, err = strconv.Atoi(match[0][1]) 64 | if err != nil { 65 | return disable() 66 | } 67 | 68 | x, err = strconv.Atoi(match[0][2]) 69 | if err != nil { 70 | return disable() 71 | } 72 | 73 | return x, y 74 | } 75 | 76 | const ( 77 | cursorUpf = "\x1b[%dA" 78 | cursorDownf = "\x1b[%dB" 79 | cursorForwf = "\x1b[%dC" 80 | cursorBackf = "\x1b[%dD" 81 | ) 82 | 83 | func moveCursorUpStr(i int) string { 84 | if i < 1 { 85 | return "" 86 | } 87 | 88 | return fmt.Sprintf(cursorUpf, i) 89 | } 90 | 91 | func moveCursorDownStr(i int) string { 92 | if i < 1 { 93 | return "" 94 | } 95 | 96 | return fmt.Sprintf(cursorDownf, i) 97 | } 98 | 99 | func moveCursorForwardsStr(i int) string { 100 | if i < 1 { 101 | return "" 102 | } 103 | 104 | return fmt.Sprintf(cursorForwf, i) 105 | } 106 | 107 | func moveCursorBackwardsStr(i int) string { 108 | if i < 1 { 109 | return "" 110 | } 111 | 112 | return fmt.Sprintf(cursorBackf, i) 113 | } 114 | 115 | func (rl *Instance) moveCursorToStartStr() string { 116 | posX, posY := rl.lineWrapCellPos() 117 | return moveCursorBackwardsStr(posX-rl.promptLen) + moveCursorUpStr(posY) 118 | } 119 | 120 | func (rl *Instance) moveCursorFromStartToLinePosStr() string { 121 | posX, posY := rl.lineWrapCellPos() 122 | output := moveCursorForwardsStr(posX) 123 | output += moveCursorDownStr(posY) 124 | return output 125 | } 126 | 127 | func (rl *Instance) moveCursorFromEndToLinePosStr() string { 128 | lineX, lineY := rl.lineWrapCellLen() 129 | posX, posY := rl.lineWrapCellPos() 130 | output := moveCursorBackwardsStr(lineX - posX) 131 | output += moveCursorUpStr(lineY - posY) 132 | return output 133 | } 134 | 135 | func (rl *Instance) moveCursorByRuneAdjustStr(rAdjust int) string { 136 | oldX, oldY := rl.lineWrapCellPos() 137 | 138 | rl.line.SetRunePos(rl.line.RunePos() + rAdjust) 139 | 140 | newX, newY := rl.lineWrapCellPos() 141 | 142 | y := newY - oldY 143 | 144 | var output string 145 | 146 | switch { 147 | case y < 0: 148 | output += moveCursorUpStr(-y) 149 | case y > 0: 150 | output += moveCursorDownStr(y) 151 | } 152 | 153 | x := newX - oldX 154 | switch { 155 | case x < 0: 156 | output += moveCursorBackwardsStr(-x) 157 | case x > 0: 158 | output += moveCursorForwardsStr(x) 159 | } 160 | 161 | return output 162 | } 163 | -------------------------------------------------------------------------------- /editor.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "io" 7 | "os" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | func (rl *Instance) launchEditor(multiline []rune) ([]rune, error) { 13 | if rl.isNoTty { 14 | return multiline, nil //errors.New("not supported with no TTY call") 15 | } 16 | 17 | return rl._launchEditor(multiline) 18 | } 19 | 20 | func (rl *Instance) writeTempFile(content []byte) (string, error) { 21 | fileID := strconv.Itoa(time.Now().Nanosecond()) + ":" + rl.line.String() 22 | 23 | h := md5.New() 24 | _, err := h.Write([]byte(fileID)) 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | name := rl.TempDirectory + "readline-" + hex.EncodeToString(h.Sum(nil)) + "-" + strconv.Itoa(os.Getpid()) 30 | 31 | file, err := os.Create(name) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | defer file.Close() 37 | 38 | _, err = file.Write(content) 39 | return name, err 40 | } 41 | 42 | func readTempFile(name string) ([]byte, error) { 43 | file, err := os.Open(name) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | b, err := io.ReadAll(file) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | if len(b) > 0 && b[len(b)-1] == '\n' { 54 | b = b[:len(b)-1] 55 | } 56 | 57 | if len(b) > 0 && b[len(b)-1] == '\r' { 58 | b = b[:len(b)-1] 59 | } 60 | 61 | if len(b) > 0 && b[len(b)-1] == '\n' { 62 | b = b[:len(b)-1] 63 | } 64 | 65 | if len(b) > 0 && b[len(b)-1] == '\r' { 66 | b = b[:len(b)-1] 67 | } 68 | 69 | err = os.Remove(name) 70 | return b, err 71 | } 72 | -------------------------------------------------------------------------------- /editor_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package readline 5 | 6 | import "errors" 7 | 8 | func (rl *Instance) _launchEditor(multiline []rune) ([]rune, error) { 9 | return rl.line.Runes(), errors.New("Not currently supported in WebAssembly") 10 | } 11 | -------------------------------------------------------------------------------- /editor_plan9.go: -------------------------------------------------------------------------------- 1 | //go:build plan9 2 | // +build plan9 3 | 4 | package readline 5 | 6 | import "errors" 7 | 8 | func (rl *Instance) _launchEditor(multiline []rune) ([]rune, error) { 9 | return rl.line.Runes(), errors.New("Not currently supported on Plan 9") 10 | } 11 | -------------------------------------------------------------------------------- /editor_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 && !js 2 | // +build !windows,!plan9,!js 3 | 4 | package readline 5 | 6 | import ( 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | const defaultEditor = "vi" 12 | 13 | func (rl *Instance) _launchEditor(multiline []rune) ([]rune, error) { 14 | name, err := rl.writeTempFile([]byte(string(multiline))) 15 | if err != nil { 16 | return multiline, err 17 | } 18 | 19 | editor := os.Getenv("EDITOR") 20 | // default editor if $EDITOR not set 21 | if editor == "" { 22 | editor = defaultEditor 23 | } 24 | 25 | cmd := exec.Command(editor, name) 26 | 27 | cmd.Stdin = os.Stdin 28 | cmd.Stdout = os.Stdout 29 | cmd.Stderr = os.Stderr 30 | 31 | if err := cmd.Start(); err != nil { 32 | return multiline, err 33 | } 34 | 35 | if err := cmd.Wait(); err != nil { 36 | return multiline, err 37 | } 38 | 39 | b, err := readTempFile(name) 40 | return []rune(string(b)), err 41 | } 42 | -------------------------------------------------------------------------------- /editor_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package readline 5 | 6 | import "errors" 7 | 8 | func (rl *Instance) _launchEditor(multiline []rune) ([]rune, error) { 9 | return rl.line.Runes(), errors.New("Not currently supported on Windows") 10 | } 11 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | const ( 8 | _CtrlC = "Ctrl+C" 9 | _EOF = "EOF" 10 | ) 11 | 12 | var ( 13 | // CtrlC is returned when ctrl+c is pressed 14 | ErrCtrlC = errors.New(_CtrlC) 15 | 16 | // EOF is returned when ctrl+d is pressed. 17 | // (this is actually the same value as io.EOF) 18 | ErrEOF = errors.New(_EOF) 19 | ) 20 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | // EventState presents a simplified view of the current readline state 4 | type EventState struct { 5 | Line string 6 | CursorPos int 7 | KeyPress string 8 | IsMasked bool 9 | InputMode string 10 | PreviewMode string 11 | } 12 | 13 | // EventReturn is a structure returned by the callback event function. 14 | // This is used by readline to determine what state the API should 15 | // return to after the readline event. 16 | type EventReturn struct { 17 | Actions []func(rl *Instance) 18 | HintText []rune 19 | SetLine []rune 20 | SetPos int 21 | Continue bool 22 | MoreEvents bool 23 | } 24 | 25 | // keyPressEventCallbackT: keyPress, eventId, line, pos 26 | type keyPressEventCallbackT func(int, *EventState) *EventReturn 27 | 28 | // AddEvent registers a new keypress handler 29 | func (rl *Instance) AddEvent(keyPress string, callback keyPressEventCallbackT) { 30 | rl.evtKeyPress[keyPress] = callback 31 | } 32 | 33 | // DelEvent deregisters an existing keypress handler 34 | func (rl *Instance) DelEvent(keyPress string) { 35 | delete(rl.evtKeyPress, keyPress) 36 | } 37 | 38 | func (rl *Instance) newEventState(keyPress string) *EventState { 39 | return &EventState{ 40 | Line: rl.line.String(), 41 | CursorPos: rl.line.RunePos(), 42 | KeyPress: keyPress, 43 | IsMasked: rl.PasswordMask > 0, 44 | InputMode: rl._getInputMode(), 45 | PreviewMode: rl._getPreviewMode(), 46 | } 47 | } 48 | 49 | const ( 50 | EventModeInputDefault = "Normal" 51 | EventModeInputVimKeys = "VimKeys" 52 | EventModeInputVimReplaceOnce = "VimReplaceOnce" 53 | EventModeInputVimReplaceMany = "VimReplaceMany" 54 | EventModeInputVimDelete = "VimDelete" 55 | EventModeInputVimCommand = "VimCommand" 56 | EventModeInputAutocomplete = "Autocomplete" 57 | EventModeInputFuzzyFind = "FuzzyFind" 58 | ) 59 | 60 | // _getInputMode is used purely for event reporting 61 | func (rl *Instance) _getInputMode() string { 62 | switch { 63 | case rl.modeViMode == vimKeys: 64 | return EventModeInputVimKeys 65 | case rl.modeViMode == vimReplaceOnce: 66 | return EventModeInputVimReplaceOnce 67 | case rl.modeViMode == vimReplaceMany: 68 | return EventModeInputVimReplaceMany 69 | case rl.modeViMode == vimDelete: 70 | return EventModeInputVimDelete 71 | case rl.modeViMode == vimCommand: 72 | return EventModeInputVimCommand 73 | case rl.modeTabFind: 74 | return EventModeInputFuzzyFind 75 | case rl.modeTabCompletion: 76 | return EventModeInputAutocomplete 77 | default: 78 | return EventModeInputDefault 79 | } 80 | } 81 | 82 | const ( 83 | EventModePreviewOff = "Disabled" 84 | EventModePreviewItem = "Autocomplete" 85 | EventModePreviewLine = "CmdLine" 86 | EventModePreviewUnknown = "Unknown" 87 | ) 88 | 89 | // _getPreviewMode is used purely for event reporting 90 | func (rl *Instance) _getPreviewMode() string { 91 | switch { 92 | case rl.previewMode == previewModeClosed: 93 | return EventModePreviewOff 94 | case rl.previewRef == previewRefLine: 95 | return EventModePreviewLine 96 | case rl.previewRef == previewRefDefault: 97 | return EventModePreviewItem 98 | default: 99 | return EventModePreviewUnknown 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/01-getting-started/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /examples/01-getting-started/README.md: -------------------------------------------------------------------------------- 1 | # lmorg/readline 2 | 3 | ## Examples: 01 - Getting Started 4 | 5 | This example shows how to create a console application which uses `readline` 6 | for input and tab-completion at it's most basic level. 7 | 8 | To use these examples you can just `go build ./main.go` (while inside this 9 | directory) or create your own project using the code demonstrated here. 10 | 11 | ### Create a list of tab-completion suggestions 12 | 13 | First we need to create a slice of strings which `readline` will use as part of 14 | it's tab-complete suggestions: 15 | 16 | ```go 17 | var items = []string{ 18 | "hello", 19 | "world", 20 | } 21 | ``` 22 | 23 | Then we can create a handler function for `readline` to call which will return 24 | this slice we've just created: 25 | 26 | ```go 27 | func Tab(line []rune, pos int, dtx readline.DelayedTabContext) (string, []string, map[string]string, readline.TabDisplayType) { 28 | var suggestions []string 29 | 30 | for i := range items { 31 | // Since in this example we don't want all items to be suggested, only 32 | // those that we have already started typing, lets build a new slice 33 | // from `items` with the matched suggestions 34 | if strings.HasPrefix(items[i], string(line)) { 35 | // The append that happens here should be mostly self explanatory 36 | // however there is one surprise in that we are also cropping the 37 | // string. Basically readline will output the completion suggestions 38 | // verbatim. This means if your user types "foo" and your suggestion 39 | // is "foobar" then the result returned will be "foofoobar". So you 40 | // need to crop the partial string from the suggestions. 41 | suggestions = append(suggestions, items[i][pos:]) 42 | } 43 | } 44 | 45 | // `line[:pos]` is a string to prefix the tab-completion suggestions. For 46 | // most use cases this will be the string you're literally just cropped out 47 | // in the `items[i][pos:]` part of the `append` above. While this does seem 48 | // unduly complicated and pointless, there may be some instances where this 49 | // proves a useful feature (for example where the tab-completion suggestion 50 | // needs to differ from what value it returns when selected). It is also 51 | // worth noting that any value you enter here will not be entered on to the 52 | // interactive line you're typing when the suggestion is selected. ie this 53 | // string is a prefix purely for display purposes. 54 | // 55 | // `suggestions` is the tab-completion suggestions slice we created above. 56 | // 57 | // I agree having a `nil` in a return is ugly. The rational is you can have 58 | // one single tab handler that can return either a slice of suggestions or 59 | // a map (eg when you want a description with the suggestion) and can do so 60 | // with compile type checking intact (ie had I used an any for the suggestion 61 | // return). This example doesn't make use of that feature. 62 | // 63 | // `TabDisplayGrid` is the style to output the tab-completion suggestions. 64 | // The grid display is the normal default to use when you don't have 65 | // descriptions. I will cover the other display formats in other examples. 66 | return string(line[:pos]), suggestions, nil, readline.TabDisplayGrid 67 | } 68 | ``` 69 | 70 | ### Create a `readline` instance 71 | 72 | Lastly lets now create the main function which will instantiate `readline` and 73 | print it's returns back to the console: 74 | 75 | 76 | ```go 77 | func main() { 78 | // Create a new readline instance 79 | rl := readline.NewInstance() 80 | 81 | // Attach the tab-completion handler (function defined below) 82 | rl.TabCompleter = Tab 83 | 84 | for { 85 | // Call readline - which will put the terminal into a pseudo-raw mode 86 | // and then read from STDIN. After the user has hit the terminal 87 | // is put back to it's original mode. 88 | // 89 | // In this example, `line` is a returned string of the key presses 90 | // typed into readline. 91 | line, err := rl.Readline() 92 | if err != nil { 93 | fmt.Println("Error:", err) 94 | return 95 | } 96 | 97 | // Print the returned line from readline 98 | fmt.Println("Readline: '" + line + "'") 99 | } 100 | } 101 | ``` 102 | 103 | ### Developing this example further 104 | 105 | The way the `Tab()` function is handled in `readline`'s sister project, _murex_, 106 | is rather than pulling from a pre-defined slice of suggestions, the `Tab()` 107 | function dynamically builds the suggestions based on what text preceded it. 108 | For example, if `cd ` was typed then `Tab()` would return a list of 109 | directories. If `vi ` was typed then `Tab()` would return a list of files. 110 | Or if `git ` was typed then `Tab()` would return a list of parameters for 111 | `git` CLI. 112 | 113 | ## License Information 114 | 115 | All examples are unlicensed - they belong to the public domain. So you can use 116 | these examples in any which way you choose in any project with any licence and 117 | without attribution. However the `readline` package itself is covered under a 118 | difference license. Please see the LICENSE file under the root directory of 119 | this repository for details about `readline`'s license. 120 | -------------------------------------------------------------------------------- /examples/01-getting-started/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lmorg/readline/examples/01-getting-started 2 | 3 | go 1.23.4 4 | 5 | require github.com/lmorg/readline/v4 v4.0.0 6 | 7 | require ( 8 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 // indirect 9 | github.com/mattn/go-runewidth v0.0.16 // indirect 10 | github.com/rivo/uniseg v0.2.0 // indirect 11 | golang.org/x/sys v0.30.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /examples/01-getting-started/go.sum: -------------------------------------------------------------------------------- 1 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 h1:JCEM0M0HAN8m1T7GeNjIaJJUyBR/ykRvGR15/aZcK7s= 2 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4/go.mod h1:flXYnXpFa6j4keWDv0plQDvy5gfyw0ifSa28IT95cZA= 3 | github.com/lmorg/readline/v4 v4.0.0 h1:2Zu31spjZMxlDzOk/+44sx/tEcg9s8Z5s7wWMfkChtM= 4 | github.com/lmorg/readline/v4 v4.0.0/go.mod h1:TCfBYH70Stopqx0EuWvnem2fWj2sDChMrNu5T3Joe2Y= 5 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 6 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 7 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 8 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 9 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 10 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | -------------------------------------------------------------------------------- /examples/01-getting-started/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/lmorg/readline/v4" 8 | ) 9 | 10 | // This is an example program using readline at a basic level. Please feel free 11 | // to hack this around - change values here and there - to get an understanding 12 | // of how the readline API works. 13 | // 14 | // This example covers tab-completion. Other - as of yet unwritten - examples 15 | // will cover other features of readline. 16 | 17 | func main() { 18 | // Create a new readline instance 19 | rl := readline.NewInstance() 20 | 21 | // Attach the tab-completion handler (function defined below) 22 | rl.TabCompleter = Tab 23 | 24 | for { 25 | // Call readline - which will put the terminal into a pseudo-raw mode 26 | // and then read from STDIN. After the user has hit the terminal 27 | // is put back to it's original mode. 28 | // 29 | // In this example, `line` is a returned string of the key presses 30 | // typed into readline. 31 | line, err := rl.Readline() 32 | if err != nil { 33 | fmt.Println("Error:", err) 34 | return 35 | } 36 | 37 | // Print the returned line from readline 38 | fmt.Printf("Readline: '%s'\n", line) 39 | } 40 | } 41 | 42 | // items is an example list of possible suggestions to display in readline's 43 | // tab-completion. For the purpose of this example, I basically just grabbed 44 | // a few entries from some random dictionary of terms. 45 | var items = []string{ 46 | "abaya", 47 | "abomasum", 48 | "absquatulate", 49 | "adscititious", 50 | "afreet", 51 | "Albertopolis", 52 | "alcazar", 53 | "amphibology", 54 | "amphisbaena", 55 | "anfractuous", 56 | "anguilliform", 57 | "apoptosis", 58 | "apple-knocker", 59 | "argle-bargle", 60 | "Argus-eyed", 61 | "argute", 62 | "ariel", 63 | "aristotle", 64 | "aspergillum", 65 | "astrobleme", 66 | "Attic", 67 | "autotomy", 68 | "badmash", 69 | "bandoline", 70 | "bardolatry", 71 | "Barmecide", 72 | "barn", 73 | "bashment", 74 | "bawbee", 75 | "benthos", 76 | "bergschrund", 77 | "bezoar", 78 | "bibliopole", 79 | "bichon", 80 | "bilboes", 81 | "bindlestiff", 82 | "bingle", 83 | "blatherskite", 84 | "bleeding", 85 | "blind", 86 | "bobsy-die", 87 | "boffola", 88 | "boilover", 89 | "borborygmus", 90 | "breatharian", 91 | "Brobdingnagian", 92 | "bruxism", 93 | "bumbo", 94 | } 95 | 96 | // Tab is the tab-completion handler for this readline example program 97 | func Tab(line []rune, pos int, dtx readline.DelayedTabContext) *readline.TabCompleterReturnT { 98 | ret := new(readline.TabCompleterReturnT) 99 | 100 | for i := range items { 101 | // Since in this example we don't want all items to be suggested, only 102 | // those that we have already started typing, lets build a new slice 103 | // from `items` with the matched suggestions 104 | if strings.HasPrefix(items[i], string(line)) { 105 | // The append that happens here should be mostly self explanatory 106 | // however there is one surprise in that we are also cropping the 107 | // string. Basically readline will output the completion suggestions 108 | // verbatim. This means if your user types "foo" and your suggestion 109 | // is "foobar" then the result returned will be "foofoobar". So you 110 | // need to crop the partial string from the suggestions. 111 | // 112 | // I do admit this is a rather ugly and solution. In all honesty I 113 | // don't like this approach much myself however that seems to be 114 | // how the existing readline APIs function (in Go at least) and thus 115 | // I wanted to keep compatibility with them when I started writing 116 | // this alternative. This function has since diverged from them in 117 | // other ways as I've added more features but I've left this 118 | // particular anti-pattern in for the sake of minimizing breaking 119 | // changes. That all said, I fully expect that there might be some 120 | // weird edge case scenarios where this approach might be required 121 | // by whoever picks this package up as they might need some more 122 | // complex completion logic than what I used this for. 123 | ret.Suggestions = append(ret.Suggestions, items[i][pos:]) 124 | } 125 | } 126 | 127 | // `line[:pos]` is a string to prefix the tab-completion suggestions. For 128 | // most use cases this will be the string you're literally just cropped out 129 | // in the `items[i][pos:]` part of the `append` above. While this does seem 130 | // unduly complicated and pointless, there may be some instances where this 131 | // proves a useful feature (for example where the tab-completion suggestion 132 | // needs to differ from what value it returns when selected). It is also 133 | // worth noting that any value you enter here will not be entered on to the 134 | // interactive line you're typing when the suggestion is selected. ie this 135 | // string is a prefix purely for display purposes. 136 | ret.Prefix = string(line[:pos]) 137 | 138 | return ret 139 | } 140 | -------------------------------------------------------------------------------- /find.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "path" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | rFindSearchPart = []rune("partial word match: ") 11 | rFindCancelPart = []rune("Cancelled partial word match") 12 | 13 | rFindSearchGlob = []rune("globbing match: ") 14 | rFindCancelGlob = []rune("Cancelled globbing match") 15 | 16 | rFindSearchRegex = []rune("regexp match: ") 17 | rFindCancelRegex = []rune("Cancelled regexp match") 18 | ) 19 | 20 | type findT interface { 21 | MatchString(string) bool 22 | } 23 | 24 | type fuzzyFindT struct { 25 | mode int 26 | tokens []string 27 | } 28 | 29 | const ( 30 | ffMatchAll = 0 31 | ffMatchSome = iota + 1 32 | ffMatchNone 33 | ffMatchRegexp 34 | ffMatchGlob 35 | ) 36 | 37 | func (ff *fuzzyFindT) MatchString(item string) bool { 38 | switch ff.mode { 39 | 40 | case ffMatchSome: 41 | return ff.matchSome(item) 42 | 43 | case ffMatchNone: 44 | return ff.matchNone(item) 45 | 46 | default: 47 | return ff.matchAll(item) 48 | } 49 | } 50 | 51 | func (ff *fuzzyFindT) matchAll(item string) bool { 52 | if len(ff.tokens) == 0 { 53 | return true 54 | } 55 | 56 | for i := range ff.tokens { 57 | if !strings.Contains(strings.ToLower(item), ff.tokens[i]) { 58 | return false 59 | } 60 | } 61 | 62 | return true 63 | } 64 | 65 | func (ff *fuzzyFindT) matchSome(item string) bool { 66 | if len(ff.tokens) == 0 { 67 | return true 68 | } 69 | 70 | for i := range ff.tokens { 71 | if strings.Contains(strings.ToLower(item), ff.tokens[i]) { 72 | return true 73 | } 74 | } 75 | 76 | return false 77 | } 78 | 79 | func (ff *fuzzyFindT) matchNone(item string) bool { 80 | if len(ff.tokens) == 0 { 81 | return false 82 | } 83 | 84 | for i := range ff.tokens { 85 | if strings.Contains(strings.ToLower(item), ff.tokens[i]) { 86 | return false 87 | } 88 | } 89 | 90 | return true 91 | } 92 | 93 | type globFindT struct{ pattern string } 94 | 95 | func (gf *globFindT) MatchString(item string) bool { 96 | found, _ := path.Match(gf.pattern, item) 97 | return found 98 | } 99 | 100 | func newGlobFind(pattern string) (*globFindT, error) { 101 | gf := new(globFindT) 102 | gf.pattern = pattern 103 | return gf, nil 104 | } 105 | 106 | func newFuzzyFind(pattern string) (findT, []rune, []rune, error) { 107 | pattern = strings.ToLower(pattern) 108 | ff := new(fuzzyFindT) 109 | 110 | ff.tokens = strings.Split(pattern, " ") 111 | 112 | for { 113 | if len(ff.tokens) == 0 { 114 | return ff, rFindSearchPart, rFindCancelPart, nil 115 | } 116 | 117 | if ff.tokens[len(ff.tokens)-1] == "" { 118 | ff.tokens = ff.tokens[:len(ff.tokens)-1] 119 | } else { 120 | break 121 | } 122 | } 123 | 124 | switch ff.tokens[0] { 125 | case "or": 126 | ff.mode = ffMatchSome 127 | ff.tokens = ff.tokens[1:] 128 | 129 | case "!": 130 | ff.mode = ffMatchNone 131 | ff.tokens = ff.tokens[1:] 132 | 133 | case "rx": 134 | ff.mode = ffMatchRegexp 135 | pattern = strings.Join(ff.tokens[1:], " ") 136 | find, err := regexp.Compile("(?i)" + pattern) 137 | return find, rFindSearchRegex, rFindCancelRegex, err 138 | 139 | case "g": 140 | ff.mode = ffMatchGlob 141 | pattern = strings.Join(ff.tokens[1:], " ") 142 | find, err := newGlobFind(pattern) 143 | return find, rFindSearchGlob, rFindCancelGlob, err 144 | 145 | default: 146 | if strings.Contains(pattern, "*") { 147 | ff.mode = ffMatchGlob 148 | find, err := newGlobFind(pattern) 149 | return find, rFindSearchGlob, rFindCancelGlob, err 150 | } 151 | } 152 | 153 | return ff, rFindSearchPart, rFindCancelPart, nil 154 | } 155 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lmorg/readline/v4 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 9 | github.com/mattn/go-runewidth v0.0.16 10 | golang.org/x/sys v0.32.0 11 | ) 12 | 13 | require ( 14 | github.com/rivo/uniseg v0.2.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 h1:JCEM0M0HAN8m1T7GeNjIaJJUyBR/ykRvGR15/aZcK7s= 2 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4/go.mod h1:flXYnXpFa6j4keWDv0plQDvy5gfyw0ifSa28IT95cZA= 3 | github.com/lmorg/readline v1.0.0 h1:llo9Tsphz1T5myTnfek/BszWAJqTGE49S2lvtfrSar8= 4 | github.com/lmorg/readline v1.0.0/go.mod h1:Nev5yti2InXJ5YtQNnHLvbx4fS5gUijIcgTWe8KtmH0= 5 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 6 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 7 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 8 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 9 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 10 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 11 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 12 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 13 | 14 | -------------------------------------------------------------------------------- /godoc.go: -------------------------------------------------------------------------------- 1 | // Package readline is a pure-Go re-imagining of the UNIX readline API 2 | package readline 3 | -------------------------------------------------------------------------------- /hint.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "strings" 4 | 5 | func (rl *Instance) getHintText() { 6 | if rl.HintText == nil { 7 | rl.resetHintText() 8 | return 9 | } 10 | 11 | hint := rl.cacheHint.Get(rl.line.Runes()) 12 | if len(hint) > 0 { 13 | rl.hintText = hint 14 | return 15 | } 16 | 17 | rl.hintText = rl.HintText(rl.line.Runes(), rl.line.RunePos()) 18 | rl.cacheHint.Append(rl.line.Runes(), rl.hintText) 19 | } 20 | 21 | func (rl *Instance) writeHintTextStr() string { 22 | rl.tabMutex.Lock() 23 | defer rl.tabMutex.Unlock() 24 | 25 | if rl.HintText == nil { 26 | rl.hintY = 0 27 | return "" 28 | } 29 | 30 | if len(rl.hintText) == 0 { 31 | rl.hintText = []rune{' '} 32 | } 33 | 34 | hintText := string(rl.hintText) 35 | 36 | if rl.modeTabCompletion && rl.tcDisplayType == TabDisplayGrid && 37 | !rl.modeTabFind && rl.modeViMode != vimCommand && len(rl.tcSuggestions) > 0 { 38 | cell := (rl.tcMaxX * (rl.tcPosY - 1)) + rl.tcOffset + rl.tcPosX - 1 39 | description := rl.tcDescriptions[rl.tcSuggestions[cell]] 40 | 41 | if description != "" { 42 | hintText = description 43 | } 44 | } 45 | 46 | s := rl._writeHintTextStr(hintText) 47 | return s 48 | } 49 | 50 | // ForceHintTextUpdate is a nasty function for force writing a new hint text. Use sparingly! 51 | func (rl *Instance) ForceHintTextUpdate(s string) { 52 | rl.hintText = []rune(s) 53 | 54 | rl.tabMutex.Lock() 55 | rl.print(rl._writeHintTextStr(s)) 56 | rl.tabMutex.Unlock() 57 | } 58 | 59 | // _writeHintTextStr doesn't contain any mutex locks. This will have to be 60 | // handled from the calling function. eg writeHintTextStr() 61 | func (rl *Instance) _writeHintTextStr(hintText string) string { 62 | // fix bug https://github.com/lmorg/murex/issues/376 63 | if rl.termWidth() == 0 { 64 | rl.cacheTermWidth() 65 | } 66 | 67 | // Determine how many lines hintText spans over 68 | // (Currently there is no support for carriage returns / new lines) 69 | hintLength := strLen(hintText) 70 | n := float64(hintLength) / float64(rl.termWidth()) 71 | if float64(int(n)) != n { 72 | n++ 73 | } 74 | rl.hintY = int(n) 75 | 76 | if rl.hintY > 3 { 77 | rl.hintY = 3 78 | hintText = hintText[:(rl.termWidth()*3)-2] + "…" 79 | } else { 80 | padding := (rl.hintY * rl.termWidth()) - len(hintText) 81 | if padding < 0 { 82 | padding = 0 83 | } 84 | hintText += strings.Repeat(" ", padding) 85 | } 86 | 87 | _, lineY := rl.lineWrapCellLen() 88 | posX, posY := rl.lineWrapCellPos() 89 | y := lineY - posY 90 | write := moveCursorDownStr(y) 91 | 92 | write += "\r\n" + rl.HintFormatting + hintText + seqReset 93 | 94 | write += moveCursorUpStr(rl.hintY + lineY - posY) 95 | write += moveCursorBackwardsStr(rl.termWidth()) 96 | write += moveCursorForwardsStr(posX) 97 | 98 | return write 99 | } 100 | 101 | func (rl *Instance) resetHintText() { 102 | rl.hintY = 0 103 | rl.hintText = []rune{} 104 | } 105 | -------------------------------------------------------------------------------- /history.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // History is an interface to allow you to write your own history logging 10 | // tools. eg sqlite backend instead of a file system. 11 | // By default readline will just use the dummyLineHistory interface which only 12 | // logs the history to memory ([]string to be precise). 13 | type History interface { 14 | // Append takes the line and returns an updated number of lines or an error 15 | Write(string) (int, error) 16 | 17 | // GetLine takes the historic line number and returns the line or an error 18 | GetLine(int) (string, error) 19 | 20 | // Len returns the number of history lines 21 | Len() int 22 | 23 | // Dump returns everything in readline. The return is an interface{} because 24 | // not all LineHistory implementations will want to structure the history in 25 | // the same way. And since Dump() is not actually used by the readline API 26 | // internally, this methods return can be structured in whichever way is most 27 | // convenient for your own applications (or even just create an empty 28 | //function which returns `nil` if you don't require Dump() either) 29 | Dump() interface{} 30 | } 31 | 32 | // ExampleHistory is an example of a LineHistory interface: 33 | type ExampleHistory struct { 34 | items []string 35 | } 36 | 37 | // Write to history 38 | func (h *ExampleHistory) Write(s string) (int, error) { 39 | h.items = append(h.items, s) 40 | return len(h.items), nil 41 | } 42 | 43 | // GetLine returns a line from history 44 | func (h *ExampleHistory) GetLine(i int) (string, error) { 45 | switch { 46 | case i < 0: 47 | return "", errors.New("requested history item out of bounds: < 0") 48 | case i > h.Len()-1: 49 | return "", errors.New("requested history item out of bounds: > Len()") 50 | default: 51 | return h.items[i], nil 52 | } 53 | } 54 | 55 | // Len returns the number of lines in history 56 | func (h *ExampleHistory) Len() int { 57 | return len(h.items) 58 | } 59 | 60 | // Dump returns the entire history 61 | func (h *ExampleHistory) Dump() interface{} { 62 | return h.items 63 | } 64 | 65 | // NullHistory is a null History interface for when you don't want line 66 | // entries remembered eg password input. 67 | type NullHistory struct{} 68 | 69 | // Write to history 70 | func (h *NullHistory) Write(s string) (int, error) { 71 | return 0, nil 72 | } 73 | 74 | // GetLine returns a line from history 75 | func (h *NullHistory) GetLine(i int) (string, error) { 76 | return "", nil 77 | } 78 | 79 | // Len returns the number of lines in history 80 | func (h *NullHistory) Len() int { 81 | return 0 82 | } 83 | 84 | // Dump returns the entire history 85 | func (h *NullHistory) Dump() interface{} { 86 | return []string{} 87 | } 88 | 89 | // Browse historic lines 90 | func (rl *Instance) walkHistory(i int) { 91 | if rl.previewRef == previewRefLine { 92 | return // don't walk history if [f9] preview open 93 | } 94 | 95 | line := rl.line.String() 96 | line = strings.TrimSpace(line) 97 | rl._walkHistory(i, line) 98 | } 99 | 100 | func (rl *Instance) _walkHistory(i int, oldLine string) { 101 | var ( 102 | newLine string 103 | err error 104 | ) 105 | 106 | switch rl.histPos + i { 107 | case -1, rl.History.Len() + 1: 108 | return 109 | 110 | case rl.History.Len(): 111 | rl.clearPrompt() 112 | rl.histPos += i 113 | if len(rl.viUndoHistory) > 0 && rl.viUndoHistory[len(rl.viUndoHistory)-1].String() != "" { 114 | rl.line = rl.lineBuf.Duplicate() 115 | } 116 | 117 | default: 118 | newLine, err = rl.History.GetLine(rl.histPos + i) 119 | if err != nil { 120 | rl.resetHelpers() 121 | rl.print("\r\n" + err.Error() + "\r\n") 122 | rl.print(rl.prompt) 123 | return 124 | } 125 | 126 | if rl.histPos-i == rl.History.Len() { 127 | rl.lineBuf = rl.line.Duplicate() 128 | } 129 | 130 | rl.histPos += i 131 | if oldLine == newLine { 132 | rl._walkHistory(i, newLine) 133 | return 134 | } 135 | 136 | if len(rl.viUndoHistory) > 0 { 137 | last := rl.viUndoHistory[len(rl.viUndoHistory)-1] 138 | if !strings.HasPrefix(newLine, strings.TrimSpace(last.String())) { 139 | rl._walkHistory(i, oldLine) 140 | return 141 | } 142 | } 143 | 144 | rl.clearPrompt() 145 | 146 | rl.line = new(UnicodeT) 147 | rl.line.Set(rl, []rune(newLine)) 148 | } 149 | 150 | if i > 0 { 151 | _, y := rl.lineWrapCellLen() 152 | rl.print(strings.Repeat("\r\n", y)) 153 | rl.line.SetRunePos(rl.line.RuneLen()) 154 | } else { 155 | rl.line.SetCellPos(rl.termWidth() - rl.promptLen - 1) 156 | } 157 | rl.print(rl.echoStr()) 158 | rl.print(rl.updateHelpersStr()) 159 | } 160 | 161 | func (rl *Instance) autocompleteHistory() ([]string, map[string]string) { 162 | if rl.AutocompleteHistory != nil { 163 | rl.tcPrefix = rl.line.String() 164 | return rl.AutocompleteHistory(rl.tcPrefix) 165 | } 166 | 167 | var ( 168 | items []string 169 | descs = make(map[string]string) 170 | 171 | line string 172 | num string 173 | err error 174 | ) 175 | 176 | rl.tcPrefix = rl.line.String() 177 | for i := rl.History.Len() - 1; i >= 0; i-- { 178 | line, err = rl.History.GetLine(i) 179 | if err != nil { 180 | continue 181 | } 182 | 183 | if !strings.HasPrefix(line, rl.tcPrefix) { 184 | continue 185 | } 186 | 187 | line = strings.Replace(line, "\n", ` `, -1)[rl.line.RuneLen():] 188 | 189 | if descs[line] != "" { 190 | continue 191 | } 192 | 193 | items = append(items, line) 194 | num = strconv.Itoa(i) 195 | 196 | descs[line] = num 197 | } 198 | 199 | return items, descs 200 | } 201 | -------------------------------------------------------------------------------- /hotkey_functions.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | func HkFnCursorMoveToStartOfLine(rl *Instance) { 4 | rl.viUndoSkipAppend = true 5 | if rl.line.RuneLen() == 0 { 6 | return 7 | } 8 | output := rl.clearHelpersStr() 9 | rl.line.SetCellPos(0) 10 | output += rl.echoStr() 11 | output += moveCursorForwardsStr(1) 12 | rl.print(output) 13 | } 14 | 15 | func HkFnCursorMoveToEndOfLine(rl *Instance) { 16 | rl.viUndoSkipAppend = true 17 | if rl.line.RuneLen() == 0 { 18 | return 19 | } 20 | output := rl.clearHelpersStr() 21 | rl.line.SetRunePos(rl.line.RuneLen()) 22 | output += rl.echoStr() 23 | output += moveCursorForwardsStr(1) 24 | rl.print(output) 25 | } 26 | 27 | func HkFnClearAfterCursor(rl *Instance) { 28 | if rl.line.RuneLen() == 0 { 29 | return 30 | } 31 | output := rl.clearHelpersStr() 32 | rl.line.Set(rl, rl.line.Runes()[:rl.line.RunePos()]) 33 | output += rl.echoStr() 34 | output += moveCursorForwardsStr(1) 35 | rl.print(output) 36 | } 37 | 38 | func HkFnClearLine(rl *Instance) { 39 | rl.clearPrompt() 40 | rl.resetHelpers() 41 | } 42 | 43 | func HkFnCursorJumpForwards(rl *Instance) { 44 | rl.viUndoSkipAppend = true 45 | output := rl.moveCursorByRuneAdjustStr(rl.viJumpE(tokeniseLine)) 46 | rl.print(output) 47 | } 48 | 49 | func HkFnCursorJumpBackwards(rl *Instance) { 50 | rl.viUndoSkipAppend = true 51 | output := rl.moveCursorByRuneAdjustStr(rl.viJumpB(tokeniseLine)) 52 | rl.print(output) 53 | } 54 | 55 | func _hkFnCancelActionModeAutoFind(rl *Instance) string { 56 | rl.viUndoSkipAppend = true 57 | output := rl.resetTabFindStr() 58 | output += rl.clearHelpersStr() 59 | rl.resetTabCompletion() 60 | output += rl.renderHelpersStr() 61 | return output 62 | } 63 | func _hkFnCancelActionModeTabFind(rl *Instance) string { 64 | rl.viUndoSkipAppend = true 65 | return rl.resetTabFindStr() 66 | } 67 | func _hkFnCancelActionModeViModeVimCommand(rl *Instance) string { 68 | rl.viUndoSkipAppend = true 69 | rl.vimCommandModeCancel() 70 | return rl.updateHelpersStr() 71 | } 72 | func _hkFnCancelActionModeTabCompletion(rl *Instance) string { 73 | rl.viUndoSkipAppend = true 74 | output := rl.clearHelpersStr() 75 | rl.resetTabCompletion() 76 | output += rl.renderHelpersStr() 77 | return output 78 | } 79 | func _hkFnCancelActionDefault(rl *Instance) string { 80 | rl.viUndoSkipAppend = true 81 | rl.modeViMode = vimKeys 82 | rl.viIteration = "" 83 | return rl.viHintMessageStr() 84 | } 85 | 86 | func HkFnRecallWord1(rl *Instance) { hkFnRecallWord(rl, 1) } 87 | func HkFnRecallWord2(rl *Instance) { hkFnRecallWord(rl, 2) } 88 | func HkFnRecallWord3(rl *Instance) { hkFnRecallWord(rl, 3) } 89 | func HkFnRecallWord4(rl *Instance) { hkFnRecallWord(rl, 4) } 90 | func HkFnRecallWord5(rl *Instance) { hkFnRecallWord(rl, 5) } 91 | func HkFnRecallWord6(rl *Instance) { hkFnRecallWord(rl, 6) } 92 | func HkFnRecallWord7(rl *Instance) { hkFnRecallWord(rl, 7) } 93 | func HkFnRecallWord8(rl *Instance) { hkFnRecallWord(rl, 8) } 94 | func HkFnRecallWord9(rl *Instance) { hkFnRecallWord(rl, 9) } 95 | func HkFnRecallWord10(rl *Instance) { hkFnRecallWord(rl, 10) } 96 | func HkFnRecallWord11(rl *Instance) { hkFnRecallWord(rl, 11) } 97 | func HkFnRecallWord12(rl *Instance) { hkFnRecallWord(rl, 12) } 98 | func HkFnRecallWordLast(rl *Instance) { hkFnRecallWord(rl, -1) } 99 | 100 | func HkFnUndo(rl *Instance) { 101 | rl.viUndoSkipAppend = true 102 | if len(rl.viUndoHistory) == 0 { 103 | return 104 | } 105 | output := rl.undoLastStr() 106 | rl.viUndoSkipAppend = true 107 | rl.line.SetRunePos(rl.line.RuneLen()) 108 | output += moveCursorForwardsStr(1) 109 | rl.print(output) 110 | } 111 | 112 | func HkFnClearScreen(rl *Instance) { 113 | if rl.isNoTty { 114 | return 115 | } 116 | 117 | rl.viUndoSkipAppend = true 118 | if rl.previewMode != previewModeClosed { 119 | HkFnModePreviewToggle(rl) 120 | } 121 | output := seqSetCursorPosTopLeft + seqClearScreen 122 | output += rl.echoStr() 123 | output += rl.renderHelpersStr() 124 | rl.print(output) 125 | } 126 | 127 | func HkFnModeFuzzyFind(rl *Instance) { 128 | if rl.isNoTty { 129 | return 130 | } 131 | 132 | rl.viUndoSkipAppend = true 133 | if !rl.modeTabCompletion { 134 | rl.modeAutoFind = true 135 | rl.getTabCompletion() 136 | } 137 | 138 | rl.modeTabFind = true 139 | rl.print(rl.updateTabFindStr([]rune{})) 140 | } 141 | 142 | func HkFnModeSearchHistory(rl *Instance) { 143 | if rl.isNoTty { 144 | return 145 | } 146 | 147 | rl.viUndoSkipAppend = true 148 | rl.modeAutoFind = true 149 | rl.tcOffset = 0 150 | rl.modeTabCompletion = true 151 | rl.tcDisplayType = TabDisplayMap 152 | rl.tabMutex.Lock() 153 | rl.tcSuggestions, rl.tcDescriptions = rl.autocompleteHistory() 154 | rl.tabMutex.Unlock() 155 | rl.initTabCompletion() 156 | 157 | rl.modeTabFind = true 158 | rl.print(rl.updateTabFindStr([]rune{})) 159 | } 160 | 161 | func HkFnModeAutocomplete(rl *Instance) { 162 | if rl.isNoTty { 163 | return 164 | } 165 | 166 | rl.viUndoSkipAppend = true 167 | if rl.modeTabCompletion { 168 | rl.moveTabCompletionHighlight(1, 0) 169 | } else { 170 | rl.getTabCompletion() 171 | } 172 | 173 | if rl.previewMode == previewModeOpen || rl.previewRef == previewRefLine { 174 | rl.previewMode = previewModeAutocomplete 175 | } 176 | 177 | rl.print(rl.renderHelpersStr()) 178 | } 179 | 180 | func HkFnCancelAction(rl *Instance) { 181 | switch { 182 | case rl.modeAutoFind && !rl.isNoTty: 183 | rl.print(_hkFnCancelActionModeAutoFind(rl)) 184 | 185 | case rl.modeTabFind && !rl.isNoTty: 186 | rl.print(_hkFnCancelActionModeTabFind(rl)) 187 | 188 | case rl.modeViMode == vimCommand: 189 | rl.print(_hkFnCancelActionModeViModeVimCommand(rl)) 190 | 191 | case rl.modeTabCompletion && !rl.isNoTty: 192 | rl.print(_hkFnCancelActionModeTabCompletion(rl)) 193 | 194 | default: 195 | rl.print(_hkFnCancelActionDefault(rl)) 196 | } 197 | } 198 | 199 | func HkFnModePreviewToggle(rl *Instance) { 200 | if rl.isNoTty || rl.PreviewLine == nil { 201 | return 202 | } 203 | if !rl.modeAutoFind && !rl.modeTabCompletion && !rl.modeTabFind && 204 | rl.previewMode == previewModeClosed { 205 | 206 | if rl.modeTabCompletion { 207 | rl.moveTabCompletionHighlight(1, 0) 208 | } else { 209 | rl.getTabCompletion() 210 | } 211 | defer func() { rl.previewMode++ }() 212 | } 213 | 214 | _fnPreviewToggle(rl) 215 | } 216 | 217 | func _fnPreviewToggle(rl *Instance) { 218 | if rl.isNoTty { 219 | return 220 | } 221 | 222 | rl.viUndoSkipAppend = true 223 | var output string 224 | 225 | switch rl.previewMode { 226 | case previewModeClosed: 227 | output = curPosSave + seqSaveBuffer + seqClearScreen 228 | rl.previewMode++ 229 | size, _ := rl.getPreviewXY() 230 | if size != nil { 231 | output += rl.previewMoveToPromptStr(size) 232 | } 233 | 234 | case previewModeOpen: 235 | rl.print(rl.clearPreviewStr()) 236 | 237 | case previewModeAutocomplete: 238 | rl.print(rl.clearPreviewStr()) 239 | rl.resetHelpers() 240 | } 241 | 242 | output += rl.echoStr() 243 | output += rl.renderHelpersStr() 244 | rl.print(output) 245 | } 246 | 247 | func HkFnModePreviewLine(rl *Instance) { 248 | if rl.isNoTty || rl.PreviewLine == nil { 249 | return 250 | } 251 | 252 | if rl.PreviewInit != nil { 253 | // forced rerun of command line preview 254 | rl.PreviewInit() 255 | rl.previewCache = nil 256 | } 257 | 258 | if !rl.modeAutoFind && !rl.modeTabCompletion && !rl.modeTabFind && 259 | rl.previewMode == previewModeClosed { 260 | defer func() { rl.previewMode++ }() 261 | } 262 | 263 | rl.previewRef = previewRefLine 264 | 265 | if rl.previewMode == previewModeClosed { 266 | _fnPreviewToggle(rl) 267 | } else { 268 | rl.print(rl.renderHelpersStr()) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /hotkey_recall.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "fmt" 4 | 5 | const errCannotRecallWord = "Cannot recall word" 6 | 7 | func hkFnRecallWord(rl *Instance, i int) { 8 | tokens, err := rl.getLastLineTokensBySpace() 9 | if err != nil { 10 | recallWordErr(rl, err.Error(), i) 11 | return 12 | } 13 | 14 | switch { 15 | case len(tokens) == 0: 16 | recallWordErr(rl, "last line contained no words", i) 17 | 18 | case i == -1: 19 | i = len(tokens) 20 | 21 | case i >= 0: 22 | if i > len(tokens) { 23 | recallWordErr(rl, "previous line contained fewer words", i) 24 | return 25 | } 26 | 27 | default: 28 | recallWordErr(rl, "invalid recall value", i) 29 | return 30 | 31 | } 32 | 33 | word := rTrimWhiteSpace(tokens[i-1]) 34 | output := rl.insertStr([]rune(word + " ")) 35 | rl.print(output) 36 | } 37 | 38 | // getLastLineTokensBySpace is a method because we might see value in reusing this 39 | func (rl *Instance) getLastLineTokensBySpace() ([]string, error) { 40 | line, err := rl.History.GetLine(rl.History.Len() - 1) 41 | if err != nil { 42 | return nil, fmt.Errorf("empty history") 43 | } 44 | 45 | tokens, _, _ := tokeniseSplitSpaces([]rune(line), 0) 46 | return tokens, nil 47 | } 48 | 49 | // recallWordErr is a function so the Go compiler can inline it 50 | func recallWordErr(rl *Instance, msg string, i int) { 51 | rl.ForceHintTextUpdate(fmt.Sprintf("%s %d: %s", errCannotRecallWord, i, msg)) 52 | } 53 | -------------------------------------------------------------------------------- /instance.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | var ForceCrLf = true 10 | 11 | type HintCacheFuncT func(prefix string, items []string) []string 12 | type PreviewFuncT func(ctx context.Context, line []rune, item string, incImages bool, size *PreviewSizeT, callback PreviewFuncCallbackT) 13 | type PreviewFuncCallbackT func(lines []string, pos int, err error) 14 | 15 | type TabCompleterReturnT struct { 16 | Prefix string 17 | Suggestions []string 18 | Descriptions map[string]string 19 | DisplayType TabDisplayType 20 | HintCache HintCacheFuncT 21 | Preview PreviewFuncT 22 | } 23 | 24 | // Instance is used to encapsulate the parameter group and run time of any given 25 | // readline instance so that you can reuse the readline API for multiple entry 26 | // captures without having to repeatedly unload configuration. 27 | type Instance struct { 28 | fdMutex sync.Mutex 29 | //fdMutex debug.BadMutex 30 | 31 | Active bool 32 | closeSigwinch func() 33 | 34 | // PasswordMask is what character to hide password entry behind. 35 | // Once enabled, set to 0 (zero) to disable the mask again. 36 | PasswordMask rune 37 | 38 | // SyntaxHighlight is a helper function to provide syntax highlighting. 39 | // Once enabled, set to nil to disable again. 40 | SyntaxHighlighter func([]rune) string 41 | 42 | // History is an interface for querying the readline history. 43 | // This is exposed as an interface to allow you the flexibility to define how 44 | // you want your history managed (eg file on disk, database, cloud, or even 45 | // no history at all). By default it uses a dummy interface that only stores 46 | // historic items in memory. 47 | History History 48 | 49 | // HistoryAutoWrite defines whether items automatically get written to 50 | // history. 51 | // Enabled by default. Set to false to disable. 52 | HistoryAutoWrite bool 53 | 54 | // TabCompleter is a function that offers completion suggestions. 55 | TabCompleter func([]rune, int, DelayedTabContext) *TabCompleterReturnT 56 | delayedTabContext DelayedTabContext 57 | 58 | tcr *TabCompleterReturnT 59 | 60 | MinTabItemLength int 61 | MaxTabItemLength int 62 | 63 | // MaxTabCompletionRows is the maximum number of rows to display in the tab 64 | // completion grid. 65 | MaxTabCompleterRows int 66 | 67 | // SyntaxCompletion is used to autocomplete code syntax (like braces and 68 | // quotation marks). If you want to complete words or phrases then you might 69 | // be better off using the TabCompletion function. 70 | // SyntaxCompletion takes the line ([]rune), change (string) and cursor 71 | // position, and returns the new line and cursor position. 72 | SyntaxCompleter func([]rune, string, int) ([]rune, int) 73 | 74 | // DelayedSyntaxWorker allows for syntax highlighting happen to the line 75 | // after the line has been drawn. 76 | DelayedSyntaxWorker func([]rune) []rune 77 | delayedSyntaxCount int32 78 | 79 | // HintText is a helper function which displays hint text the prompt. 80 | // HintText takes the line input from the prompt and the cursor position. 81 | // It returns the hint text to display. 82 | HintText func([]rune, int) []rune 83 | 84 | // HintColor any ANSI escape codes you wish to use for hint formatting. By 85 | // default this will just be blue. 86 | HintFormatting string 87 | 88 | // AutocompleteHistory is another customization allowing for alternative 89 | // results when [ctrl]+[r] 90 | AutocompleteHistory func(string) ([]string, map[string]string) 91 | 92 | // TempDirectory is the path to write temporary files when editing a line in 93 | // $EDITOR. This will default to os.TempDir() 94 | TempDirectory string 95 | 96 | // GetMultiLine is a callback to your host program. Since multiline support 97 | // is handled by the application rather than readline itself, this callback 98 | // is required when calling $EDITOR. However if this function is not set 99 | // then readline will just use the current line. 100 | GetMultiLine func([]rune) []rune 101 | 102 | MaxCacheSize int 103 | cacheHint cacheSliceRune 104 | cacheSyntax cacheString 105 | //cacheSyntaxHighlight cacheString 106 | //cacheSyntaxDelayed cacheSliceRune 107 | 108 | // readline operating parameters 109 | prompt string // = ">>> " 110 | promptLen int // = 4 111 | line *UnicodeT 112 | lineChange string // cache what had changed from previous line 113 | _termWidth int 114 | multiline []byte 115 | multiSplit []string 116 | skipStdinRead bool 117 | 118 | // history 119 | lineBuf *UnicodeT 120 | histPos int 121 | 122 | // hint text 123 | hintY int 124 | hintText []rune 125 | 126 | ScreenRefresh func() 127 | 128 | PreviewInit func() 129 | previewMode previewModeT 130 | previewRef previewRefT 131 | previewItem string 132 | previewCache *previewCacheT 133 | PreviewImages bool 134 | previewCancel context.CancelFunc 135 | PreviewLine PreviewFuncT 136 | 137 | // tab completion 138 | modeTabCompletion bool 139 | tabMutex sync.Mutex 140 | tcPrefix string 141 | tcSuggestions []string 142 | tcDescriptions map[string]string 143 | tcDisplayType TabDisplayType 144 | tcOffset int 145 | tcPosX int 146 | tcPosY int 147 | tcMaxX int 148 | tcMaxY int 149 | tcUsedY int 150 | tcMaxLength int 151 | 152 | // tab find 153 | modeTabFind bool 154 | rFindSearch []rune // searching message 155 | rFindCancel []rune // search cancelled message 156 | tfLine []rune 157 | tfSuggestions []string 158 | modeAutoFind bool // for when invoked via ^R or ^F outside of [tab] 159 | 160 | // vim 161 | modeViMode viMode //= vimInsert 162 | viIteration string 163 | viUndoHistory []*UnicodeT 164 | viUndoSkipAppend bool 165 | viYankBuffer string 166 | viCommandLine []rune 167 | 168 | // event 169 | evtKeyPress map[string]keyPressEventCallbackT 170 | 171 | EnableGetCursorPos bool 172 | 173 | // no TTY 174 | isNoTty bool 175 | _noTtyKeyPress chan []byte 176 | _noTtyCallback chan *NoTtyCallbackT 177 | } 178 | 179 | type NoTtyCallbackT struct { 180 | Line *UnicodeT 181 | Hint string 182 | } 183 | 184 | func (rl *Instance) MakeNoTtyChan(termWidth int) chan *NoTtyCallbackT { 185 | rl.isNoTty = true 186 | rl._noTtyKeyPress = make(chan []byte) 187 | rl._noTtyCallback = make(chan *NoTtyCallbackT) 188 | rl._termWidth = termWidth 189 | rl.SetPrompt("") 190 | return rl._noTtyCallback 191 | } 192 | 193 | func noTtyCallback(rl *Instance) { 194 | if !rl.isNoTty { 195 | return 196 | } 197 | 198 | rl._noTtyCallback <- &NoTtyCallbackT{ 199 | Line: rl.line.Duplicate(), 200 | Hint: string(rl.hintText), 201 | } 202 | } 203 | 204 | // close is only needed if you're not running as a TTY 205 | func (rl *Instance) close() { 206 | if rl.isNoTty { 207 | close(rl._noTtyKeyPress) 208 | close(rl._noTtyCallback) 209 | } 210 | } 211 | 212 | // NewInstance is used to create a readline instance and initialise it with sane 213 | // defaults. 214 | func NewInstance() *Instance { 215 | rl := new(Instance) 216 | 217 | rl.line = new(UnicodeT) 218 | rl.lineBuf = new(UnicodeT) 219 | rl.History = new(ExampleHistory) 220 | rl.HistoryAutoWrite = true 221 | rl.MaxTabCompleterRows = 4 222 | rl.prompt = ">>> " 223 | rl.promptLen = 4 224 | rl.HintFormatting = seqFgBlue 225 | rl.evtKeyPress = make(map[string]keyPressEventCallbackT) 226 | 227 | rl.TempDirectory = os.TempDir() 228 | 229 | rl.MaxCacheSize = 256 230 | rl.cacheHint.Init(rl) 231 | rl.cacheSyntax.Init(rl) 232 | 233 | //rl.ForceCrLf = true 234 | 235 | return rl 236 | } 237 | -------------------------------------------------------------------------------- /preview.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type previewModeT int 11 | 12 | const ( 13 | previewModeClosed previewModeT = 0 14 | previewModeOpen previewModeT = 1 15 | previewModeAutocomplete previewModeT = 2 16 | ) 17 | 18 | type previewRefT int 19 | 20 | const ( 21 | previewRefDefault previewRefT = 0 22 | previewRefLine previewRefT = 1 23 | ) 24 | 25 | const ( 26 | previewHeadingHeight = 3 27 | previewPromptHSpace = 3 28 | ) 29 | 30 | const ( 31 | boxTL = "┏" 32 | boxTR = "┓" 33 | boxBL = "┗" 34 | boxBR = "┛" 35 | boxH = "━" 36 | boxHN = "─" // narrow 37 | boxHD = "╶" // dashed 38 | boxV = "┃" 39 | boxVL = "┠" 40 | boxVR = "┨" 41 | ) 42 | 43 | const ( 44 | headingTL = "╔" 45 | headingTR = "╗" 46 | headingBL = "╚" 47 | headingBR = "╝" 48 | headingH = "═" 49 | headingV = "║" 50 | headingVL = "╟" 51 | headingVR = "╢" 52 | ) 53 | 54 | const ( 55 | glyphScrollBar = "█" 56 | ) 57 | 58 | // previewPos should be a percentage represented as a decimal value (eg 0.5 == 50%) 59 | func getScrollBarSize(previewHeight int, previewPos float64) int { 60 | size := int((float64(previewHeight) + 2) * previewPos) 61 | /*if previewPos < 1 && size >= previewHeight { 62 | size-- 63 | }*/ 64 | return size 65 | } 66 | 67 | func getPreviewPos(rl *Instance) float64 { 68 | if rl.previewCache == nil { 69 | return 0 70 | } 71 | 72 | return (float64(rl.previewCache.pos) + float64(rl.previewCache.size.Height) + 2) / float64(len(rl.previewCache.lines)) 73 | } 74 | 75 | func getPreviewWidth(width int) (preview, forward int) { 76 | preview = width - 3 77 | 78 | forward = width - preview 79 | forward -= 2 80 | return 81 | } 82 | 83 | type PreviewSizeT struct { 84 | Height int 85 | Width int 86 | Forward int 87 | } 88 | 89 | type previewCacheT struct { 90 | item string 91 | pos int 92 | len int 93 | lines []string 94 | size *PreviewSizeT 95 | } 96 | 97 | func (rl *Instance) getPreviewXY() (*PreviewSizeT, error) { 98 | if rl.isNoTty { 99 | return nil, errors.New("not supported in no TTY mode") 100 | } 101 | 102 | width, height, err := GetSize(int(os.Stdout.Fd())) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | if height == 0 { 108 | height = 25 109 | } 110 | 111 | if width == 0 { 112 | width = 80 113 | } 114 | 115 | rl.previewAutocompleteHeight(height) 116 | 117 | preview, forward := getPreviewWidth(width) 118 | size := &PreviewSizeT{ 119 | Height: height - rl.MaxTabCompleterRows - 10, // hintText, multi-line prompts, etc 120 | Width: preview, 121 | Forward: forward, 122 | } 123 | 124 | return size, nil 125 | } 126 | 127 | func (rl *Instance) previewAutocompleteHeight(height int) { 128 | switch { 129 | case height < 20: 130 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 2) 131 | case height < 25: 132 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 3) 133 | case height < 28: 134 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 4) 135 | case height < 31: 136 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 5) 137 | case height < 34: 138 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 6) 139 | case height < 37: 140 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 7) 141 | case height < 40: 142 | rl.MaxTabCompleterRows = noLargerThan(rl.MaxTabCompleterRows, 8) 143 | } 144 | } 145 | 146 | func noLargerThan(src, max int) int { 147 | if src > max { 148 | return max 149 | } 150 | return src 151 | } 152 | 153 | func (rl *Instance) writePreviewStr() string { 154 | if rl.previewMode == previewModeClosed { 155 | rl.previewCache = nil 156 | return "" 157 | } 158 | 159 | if rl.previewCancel != nil { 160 | rl.previewCancel() 161 | } 162 | 163 | var fn PreviewFuncT 164 | if rl.previewRef == previewRefLine { 165 | fn = rl.PreviewLine 166 | } else { 167 | if rl.tcr == nil { 168 | rl.previewCache = nil 169 | return "" 170 | } 171 | fn = rl.tcr.Preview 172 | } 173 | 174 | if fn == nil { 175 | rl.previewCache = nil 176 | return "" 177 | } 178 | 179 | size, err := rl.getPreviewXY() 180 | if err != nil || size.Height < 4 || size.Width < 10 { 181 | rl.previewCache = nil 182 | return previewTerminalTooSmall 183 | } 184 | 185 | item := rl.previewItem 186 | item = strings.ReplaceAll(item, "\\", "") 187 | item = strings.TrimSpace(item) 188 | 189 | go delayedPreviewTimer(rl, fn, size, item) 190 | 191 | return "" 192 | } 193 | 194 | var previewTerminalTooSmall = fmt.Sprintf("%s%sTerminal too small to display preview%s", curPosSave, curHome, curPosRestore) 195 | 196 | const ( 197 | curHome = "\x1b[H" 198 | curPosSave = "\x1b[s" 199 | curPosRestore = "\x1b[u" 200 | ) 201 | 202 | func (rl *Instance) previewDrawStr(preview []string, size *PreviewSizeT) (string, error) { 203 | var ( 204 | output string 205 | scrollBar = glyphScrollBar 206 | scrollHeight = getScrollBarSize(size.Height-1, getPreviewPos(rl)) 207 | ) 208 | 209 | pf := fmt.Sprintf("%s%%-%ds%s\r\n", boxV, size.Width, scrollBar) 210 | pj := fmt.Sprintf("%s%%-%ds%s\r\n", boxVL, size.Width, scrollBar) 211 | 212 | output += curHome 213 | 214 | output += fmt.Sprintf(cursorForwf, size.Forward) 215 | hr := strings.Repeat(headingH, size.Width) 216 | output += headingTL + hr + headingTR + "\r\n " 217 | output += headingV + rl.previewTitleStr(size.Width) + headingV + "\r\n " 218 | output += headingBL + hr + headingBR + "\r\n " 219 | 220 | hr = strings.Repeat(boxH, size.Width) 221 | output += boxTL + hr + boxTR + "\r\n" 222 | 223 | for i := 0; i <= size.Height; i++ { 224 | if i == scrollHeight { 225 | scrollBar = boxV 226 | pf = fmt.Sprintf("%s%%-%ds%s\r\n", boxV, size.Width, scrollBar) 227 | pj = fmt.Sprintf("%s%%-%ds%s\r\n", boxVL, size.Width, boxVR) 228 | } 229 | 230 | output += fmt.Sprintf(cursorForwf, size.Forward) 231 | 232 | if i >= len(preview) { 233 | blank := strings.Repeat(" ", size.Width) 234 | output += boxV + blank + scrollBar + "\r\n" 235 | continue 236 | } 237 | 238 | if strings.HasPrefix(preview[i], boxHN) || strings.HasPrefix(preview[i], boxHD) { 239 | output += fmt.Sprintf(pj, preview[i]) 240 | } else { 241 | output += fmt.Sprintf(pf, preview[i]) 242 | } 243 | } 244 | 245 | output += fmt.Sprintf(cursorForwf, size.Forward) 246 | output += boxBL + hr + boxBR + "\r\n" 247 | 248 | output += rl.previewMoveToPromptStr(size) 249 | return output, nil 250 | } 251 | 252 | func (rl *Instance) previewTitleStr(width int) string { 253 | var title string 254 | 255 | if rl.previewRef == previewRefDefault { 256 | title = " Autocomplete Preview" + title 257 | } else { 258 | title = " Command Line Preview" + title 259 | } 260 | title += " | [F1] to exit | [ENTER] to commit" 261 | 262 | l := len(title) + 1 263 | switch { 264 | case l > width: 265 | return title[:width-2] + "… " 266 | case l == width: 267 | return title + " " 268 | default: 269 | return title + strings.Repeat(" ", width-l+1) 270 | } 271 | } 272 | 273 | func (rl *Instance) previewMoveToPromptStr(size *PreviewSizeT) string { 274 | output := curHome 275 | output += moveCursorDownStr(size.Height + previewPromptHSpace + previewHeadingHeight) 276 | output += rl.moveCursorFromStartToLinePosStr() 277 | return output 278 | } 279 | 280 | func (rl *Instance) previewPreviousSectionStr() string { 281 | if rl.previewCache == nil || rl.previewCache.pos == 0 { 282 | return "" 283 | } 284 | 285 | for rl.previewCache.pos -= 2; rl.previewCache.pos > 0; rl.previewCache.pos-- { 286 | if strings.HasPrefix(rl.previewCache.lines[rl.previewCache.pos], boxHN) { 287 | if rl.previewCache.pos < len(rl.previewCache.lines)-1 { 288 | rl.previewCache.pos++ 289 | } 290 | break 291 | } 292 | } 293 | 294 | if rl.previewCache.pos > len(rl.previewCache.lines)-rl.previewCache.len-1 { 295 | rl.previewCache.pos = len(rl.previewCache.lines) - rl.previewCache.len - 1 296 | } 297 | if rl.previewCache.pos < 0 { 298 | rl.previewCache.pos = 0 299 | } 300 | 301 | output, _ := rl.previewDrawStr(rl.previewCache.lines[rl.previewCache.pos:], rl.previewCache.size) 302 | return output 303 | } 304 | 305 | func (rl *Instance) previewNextSectionStr() string { 306 | if rl.previewCache == nil { 307 | return "" 308 | } 309 | 310 | for ; rl.previewCache.pos < len(rl.previewCache.lines)-rl.previewCache.len; rl.previewCache.pos++ { 311 | if strings.HasPrefix(rl.previewCache.lines[rl.previewCache.pos], boxHN) { 312 | if rl.previewCache.pos < len(rl.previewCache.lines)-1 { 313 | rl.previewCache.pos++ 314 | } 315 | break 316 | } 317 | } 318 | 319 | if rl.previewCache.pos > len(rl.previewCache.lines)-rl.previewCache.len-1 { 320 | rl.previewCache.pos = len(rl.previewCache.lines) - rl.previewCache.len - 1 321 | } 322 | if rl.previewCache.pos < 0 { 323 | rl.previewCache.pos = 0 324 | } 325 | 326 | output, _ := rl.previewDrawStr(rl.previewCache.lines[rl.previewCache.pos:], rl.previewCache.size) 327 | return output 328 | } 329 | 330 | func (rl *Instance) previewPageUpStr() string { 331 | if rl.previewCache == nil { 332 | return "" 333 | } 334 | 335 | rl.previewCache.pos -= rl.previewCache.len 336 | if rl.previewCache.pos < 0 { 337 | rl.previewCache.pos = 0 338 | } 339 | 340 | output, _ := rl.previewDrawStr(rl.previewCache.lines[rl.previewCache.pos:], rl.previewCache.size) 341 | return output 342 | } 343 | 344 | func (rl *Instance) previewPageDownStr() string { 345 | if rl.previewCache == nil { 346 | return "" 347 | } 348 | 349 | rl.previewCache.pos += rl.previewCache.len 350 | if rl.previewCache.pos > len(rl.previewCache.lines)-rl.previewCache.len-1 { 351 | rl.previewCache.pos = len(rl.previewCache.lines) - rl.previewCache.len - 1 352 | if rl.previewCache.pos < 0 { 353 | rl.previewCache.pos = 0 354 | } 355 | } 356 | 357 | output, _ := rl.previewDrawStr(rl.previewCache.lines[rl.previewCache.pos:], rl.previewCache.size) 358 | return output 359 | } 360 | 361 | func (rl *Instance) clearPreviewStr() string { 362 | var output string 363 | 364 | if rl.previewCancel != nil { 365 | rl.previewCancel() 366 | } 367 | 368 | if rl.PreviewInit != nil { 369 | rl.PreviewInit() 370 | } 371 | 372 | if rl.previewMode > previewModeClosed { 373 | output = seqRestoreBuffer + curPosRestore 374 | output += rl.echoStr() 375 | rl.previewMode = previewModeClosed 376 | rl.previewRef = previewRefDefault 377 | } 378 | 379 | return output 380 | } 381 | -------------------------------------------------------------------------------- /preview_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | /*func TestGetPreviewWidth(t *testing.T) { 4 | tests := []struct { 5 | Term int 6 | Preview int 7 | Forward int 8 | }{ 9 | { 10 | Term: 79, 11 | Preview: 75, 12 | Forward: 2, 13 | }, 14 | { 15 | Term: 92, 16 | Preview: 80, 17 | Forward: 10, 18 | }, 19 | { 20 | Term: 80, 21 | Preview: 76, 22 | Forward: 2, 23 | }, 24 | { 25 | Term: 120, 26 | Preview: 116, 27 | Forward: 2, 28 | }, 29 | { 30 | Term: 300, 31 | Preview: 120, 32 | Forward: 178, 33 | }, 34 | } 35 | 36 | count.Tests(t, len(tests)) 37 | 38 | for i, test := range tests { 39 | preview, forward := getPreviewWidth(test.Term) 40 | if preview != test.Preview || forward != test.Forward { 41 | t.Errorf("Maths fail in test %d", i) 42 | t.Logf(" Term Width: %d", test.Term) 43 | t.Logf(" Exp Preview: %d", test.Preview) 44 | t.Logf(" Act Preview: %d", preview) 45 | t.Logf(" Exp Forward: %d", test.Forward) 46 | t.Logf(" Act Forward: %d", forward) 47 | } 48 | } 49 | }*/ 50 | -------------------------------------------------------------------------------- /raw/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /raw_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 6 | // +build darwin dragonfly freebsd netbsd openbsd 7 | 8 | package readline 9 | 10 | import "golang.org/x/sys/unix" 11 | 12 | const ioctlReadTermios = unix.TIOCGETA 13 | const ioctlWriteTermios = unix.TIOCSETA 14 | -------------------------------------------------------------------------------- /raw_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package readline 5 | 6 | import "github.com/lmorg/murex/utils/virtualterm" 7 | 8 | // VTern is a virtual terminal 9 | var VTerm = virtualterm.NewTerminal(120, 40) 10 | 11 | type State struct { 12 | state virtualterm.PtyState 13 | } 14 | 15 | func MakeRaw(_ int) (*State, error) { 16 | state := State{state: VTerm.MakeRaw()} 17 | return &state, nil 18 | } 19 | 20 | func Restore(_ int, state *State) error { 21 | VTerm.Restore(state.state) 22 | return nil 23 | } 24 | 25 | // GetSize the default terminal size in the webpage 26 | func GetSize(_ int) (width, height int, err error) { 27 | return VTerm.GetSize() 28 | } 29 | -------------------------------------------------------------------------------- /raw_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package readline 9 | 10 | import "golang.org/x/sys/unix" 11 | 12 | const ioctlReadTermios = unix.TCGETS 13 | const ioctlWriteTermios = unix.TCSETS 14 | 15 | //const OXTABS = unix.XTABS 16 | -------------------------------------------------------------------------------- /raw_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package terminal provides support functions for dealing with terminals, as 6 | // commonly found on UNIX systems. 7 | // 8 | // Putting a terminal into raw mode is the most common requirement: 9 | // 10 | // oldState, err := terminal.MakeRaw(0) 11 | // if err != nil { 12 | // panic(err) 13 | // } 14 | // defer terminal.Restore(0, oldState) 15 | 16 | //go:build plan9 17 | // +build plan9 18 | 19 | package readline 20 | 21 | import ( 22 | "fmt" 23 | "runtime" 24 | ) 25 | 26 | type State struct{} 27 | 28 | // IsTerminal returns true if the given file descriptor is a terminal. 29 | func IsTerminal(fd int) bool { 30 | return false 31 | } 32 | 33 | // MakeRaw put the terminal connected to the given file descriptor into raw 34 | // mode and returns the previous state of the terminal so that it can be 35 | // restored. 36 | func MakeRaw(fd int) (*State, error) { 37 | return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 38 | } 39 | 40 | // GetState returns the current state of a terminal which may be useful to 41 | // restore the terminal after a signal. 42 | func GetState(fd int) (*State, error) { 43 | return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 44 | } 45 | 46 | // Restore restores the terminal connected to the given file descriptor to a 47 | // previous state. 48 | func Restore(fd int, state *State) error { 49 | return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 50 | } 51 | 52 | // GetSize returns the dimensions of the given terminal. 53 | func GetSize(fd int) (width, height int, err error) { 54 | return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 55 | } 56 | -------------------------------------------------------------------------------- /raw_solaris.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build solaris 6 | // +build solaris 7 | 8 | package readline 9 | 10 | import ( 11 | "syscall" 12 | 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | // State contains the state of a terminal. 17 | type State struct { 18 | state *unix.Termios 19 | } 20 | 21 | // IsTerminal returns true if the given file descriptor is a terminal. 22 | func IsTerminal(fd int) bool { 23 | _, err := unix.IoctlGetTermio(fd, unix.TCGETA) 24 | return err == nil 25 | } 26 | 27 | // MakeRaw puts the terminal connected to the given file descriptor into raw 28 | // mode and returns the previous state of the terminal so that it can be 29 | // restored. 30 | // see http://cr.illumos.org/~webrev/andy_js/1060/ 31 | func MakeRaw(fd int) (*State, error) { 32 | oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) 33 | if err != nil { 34 | return nil, err 35 | } 36 | oldTermios := *oldTermiosPtr 37 | 38 | newTermios := oldTermios 39 | newTermios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON 40 | //newTermios.Oflag &^= syscall.OPOST 41 | newTermios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN 42 | newTermios.Cflag &^= syscall.CSIZE | syscall.PARENB 43 | newTermios.Cflag |= syscall.CS8 44 | newTermios.Cc[unix.VMIN] = 1 45 | newTermios.Cc[unix.VTIME] = 0 46 | 47 | if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newTermios); err != nil { 48 | return nil, err 49 | } 50 | 51 | return &State{ 52 | state: oldTermiosPtr, 53 | }, nil 54 | } 55 | 56 | // Restore restores the terminal connected to the given file descriptor to a 57 | // previous state. 58 | func Restore(fd int, oldState *State) error { 59 | return unix.IoctlSetTermios(fd, unix.TCSETS, oldState.state) 60 | } 61 | 62 | // GetState returns the current state of a terminal which may be useful to 63 | // restore the terminal after a signal. 64 | func GetState(fd int) (*State, error) { 65 | oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return &State{ 71 | state: oldTermiosPtr, 72 | }, nil 73 | } 74 | 75 | // GetSize returns the dimensions of the given TTY. 76 | func GetSize(fd int) (width, height int, err error) { 77 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 78 | if err != nil { 79 | return 0, 0, err 80 | } 81 | return int(ws.Col), int(ws.Row), nil 82 | } 83 | -------------------------------------------------------------------------------- /raw_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd 6 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 7 | 8 | package readline 9 | 10 | import ( 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // State contains the state of a terminal. 15 | type State struct { 16 | termios unix.Termios 17 | } 18 | 19 | // IsTerminal returns true if the given file descriptor is a terminal. 20 | func IsTerminal(fd int) bool { 21 | _, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 22 | return err == nil 23 | } 24 | 25 | // MakeRaw put the terminal connected to the given file descriptor into raw 26 | // mode and returns the previous state of the terminal so that it can be 27 | // restored. 28 | func MakeRaw(fd int) (*State, error) { 29 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | oldState := State{termios: *termios} 35 | 36 | // This attempts to replicate the behaviour documented for cfmakeraw in 37 | // the termios(3) manpage. 38 | termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON 39 | //termios.Oflag &^= unix.OPOST 40 | //termios.Oflag &^= OXTABS 41 | termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN 42 | termios.Cflag &^= unix.CSIZE | unix.PARENB 43 | termios.Cflag |= unix.CS8 44 | termios.Cc[unix.VMIN] = 1 45 | termios.Cc[unix.VTIME] = 0 46 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil { 47 | return nil, err 48 | } 49 | 50 | return &oldState, nil 51 | } 52 | 53 | // GetState returns the current state of a terminal which may be useful to 54 | // restore the terminal after a signal. 55 | func GetState(fd int) (*State, error) { 56 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return &State{termios: *termios}, nil 62 | } 63 | 64 | // Restore restores the terminal connected to the given file descriptor to a 65 | // previous state. 66 | func Restore(fd int, state *State) error { 67 | return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios) 68 | } 69 | 70 | // GetSize returns the dimensions of the given TTY. 71 | func GetSize(fd int) (width, height int, err error) { 72 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 73 | if err != nil { 74 | return -1, -1, err 75 | } 76 | return int(ws.Col), int(ws.Row), nil 77 | } 78 | -------------------------------------------------------------------------------- /raw_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | // Package terminal provides support functions for dealing with terminals, as 9 | // commonly found on UNIX systems. 10 | // 11 | // Putting a terminal into raw mode is the most common requirement: 12 | // 13 | // oldState, err := terminal.MakeRaw(0) 14 | // if err != nil { 15 | // panic(err) 16 | // } 17 | // defer terminal.Restore(0, oldState) 18 | package readline 19 | 20 | import ( 21 | "golang.org/x/sys/windows" 22 | ) 23 | 24 | type State struct { 25 | mode uint32 26 | } 27 | 28 | // IsTerminal returns true if the given file descriptor is a terminal. 29 | func IsTerminal(fd int) bool { 30 | var st uint32 31 | err := windows.GetConsoleMode(windows.Handle(fd), &st) 32 | return err == nil 33 | } 34 | 35 | // MakeRaw put the terminal connected to the given file descriptor into raw 36 | // mode and returns the previous state of the terminal so that it can be 37 | // restored. 38 | func MakeRaw(fd int) (*State, error) { 39 | var st uint32 40 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 41 | return nil, err 42 | } 43 | raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) 44 | raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT 45 | if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { 46 | return nil, err 47 | } 48 | return &State{st}, nil 49 | } 50 | 51 | // GetState returns the current state of a terminal which may be useful to 52 | // restore the terminal after a signal. 53 | func GetState(fd int) (*State, error) { 54 | var st uint32 55 | if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { 56 | return nil, err 57 | } 58 | return &State{st}, nil 59 | } 60 | 61 | // Restore restores the terminal connected to the given file descriptor to a 62 | // previous state. 63 | func Restore(fd int, state *State) error { 64 | return windows.SetConsoleMode(windows.Handle(fd), state.mode) 65 | } 66 | 67 | // GetSize returns the dimensions of the given terminal. 68 | func GetSize(fd int) (width, height int, err error) { 69 | var info windows.ConsoleScreenBufferInfo 70 | if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { 71 | return 0, 0, err 72 | } 73 | return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil 74 | } 75 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | func removeNonPrintableChars(s []byte) int { 4 | var ( 5 | i int 6 | next int 7 | ) 8 | 9 | for next = 0; next < len(s); next++ { 10 | if s[next] < ' ' && s[next] != charEOF && s[next] != charEscape && 11 | s[next] != charTab && s[next] != charBackspace { 12 | 13 | continue 14 | 15 | } else { 16 | s[i] = s[next] 17 | i++ 18 | } 19 | } 20 | 21 | return i 22 | } 23 | 24 | func (rl *Instance) KeyPress(b []byte) { 25 | if !rl.isNoTty { 26 | panic("missing NoTTY call") 27 | } 28 | 29 | go func() { 30 | rl._noTtyKeyPress <- b 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /read_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package readline 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | var Stdin = make(chan string, 0) 11 | 12 | func (rl *Instance) read(b []byte) (int, error) { 13 | stdin := <-Stdin 14 | 15 | if len(stdin) > len(b) { 16 | return 0, errors.New("wasm keystrokes > b (this is a bug)") 17 | } 18 | 19 | for i := range stdin { 20 | b[i] = stdin[i] 21 | } 22 | 23 | return len(stdin), nil 24 | } 25 | -------------------------------------------------------------------------------- /read_nojs.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package readline 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | ) 10 | 11 | func (rl *Instance) read(p []byte) (int, error) { 12 | if rl.isNoTty { 13 | return rl._readNoTty(p) 14 | } 15 | 16 | return os.Stdin.Read(p) 17 | } 18 | 19 | func (rl *Instance) _readNoTty(p []byte) (int, error) { 20 | b, ok := <-rl._noTtyKeyPress 21 | 22 | if !ok { 23 | return 0, errors.New("channel closed") 24 | } 25 | 26 | copy(p, b) 27 | return len(b), nil 28 | } 29 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRemoveNonPrintableChars(t *testing.T) { 8 | tests := []struct { 9 | Slice string 10 | Expected string 11 | }{ 12 | { 13 | Slice: "", 14 | Expected: "", 15 | }, 16 | { 17 | Slice: "a", 18 | Expected: "a", 19 | }, 20 | { 21 | Slice: "abc", 22 | Expected: "abc", 23 | }, 24 | { 25 | Slice: "\t", 26 | Expected: "\t", 27 | }, 28 | { 29 | Slice: "\ta", 30 | Expected: "\ta", 31 | }, 32 | { 33 | Slice: "a\t", 34 | Expected: "a\t", 35 | }, 36 | { 37 | Slice: "a\tb", 38 | Expected: "a\tb", 39 | }, 40 | { 41 | Slice: "a\tb\tc", 42 | Expected: "a\tb\tc", 43 | }, 44 | { 45 | Slice: "a\t\tb\t\tc", 46 | Expected: "a\t\tb\t\tc", 47 | }, 48 | 49 | // non printable 50 | 51 | { 52 | Slice: "\x16", 53 | Expected: "", 54 | }, 55 | { 56 | Slice: "\x16a", 57 | Expected: "a", 58 | }, 59 | { 60 | Slice: "a\x16", 61 | Expected: "a", 62 | }, 63 | { 64 | Slice: "a\x16b", 65 | Expected: "ab", 66 | }, 67 | { 68 | Slice: "a\x16b\x16c", 69 | Expected: "abc", 70 | }, 71 | { 72 | Slice: "a\x16\x16b\x16\x16c", 73 | Expected: "abc", 74 | }, 75 | 76 | // unicode 77 | 78 | { 79 | Slice: "世界", 80 | Expected: "世界", 81 | }, 82 | { 83 | Slice: "\x16世\x16界\x16", 84 | Expected: "世界", 85 | }, 86 | { 87 | Slice: "\x16世界\x16世界\x16", 88 | Expected: "世界世界", 89 | }, 90 | { 91 | Slice: "\x16\x16世界\x16\x16世界\x16\x16", 92 | Expected: "世界世界", 93 | }, 94 | { 95 | Slice: "😀😁😂", 96 | Expected: "😀😁😂", 97 | }, 98 | { 99 | Slice: "\x16😀\x16😁\x16😂", 100 | Expected: "😀😁😂", 101 | }, 102 | { 103 | Slice: "\x16😀😁😂\x16😀😁😂\x16", 104 | Expected: "😀😁😂😀😁😂", 105 | }, 106 | { 107 | Slice: "\x16\x16😀😁😂\x16\x16😀😁😂\x16\x16", 108 | Expected: "😀😁😂😀😁😂", 109 | }, 110 | } 111 | 112 | for i, test := range tests { 113 | s := []byte(test.Slice) 114 | actual := string(s[:removeNonPrintableChars(s)]) 115 | 116 | if test.Expected != actual { 117 | t.Errorf("Expected does not match actual in test %d", i) 118 | t.Logf(" Slice: '%s'", test.Slice) 119 | t.Logf(" Expected: '%s'", test.Expected) 120 | t.Logf(" Actual: '%s'", actual) 121 | t.Logf(" s bytes: '%v'", []byte(test.Slice)) 122 | t.Logf(" e bytes: '%v'", []byte(test.Expected)) 123 | t.Logf(" a bytes: '%v'", []byte(actual)) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /readline.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "sync/atomic" 10 | ) 11 | 12 | var rxMultiline = regexp.MustCompile(`[\r\n]+`) 13 | 14 | // Readline displays the readline prompt. 15 | // It will return a string (user entered data) or an error. 16 | func (rl *Instance) Readline() (string, error) { return rl.readline("") } 17 | 18 | // Readline displays the readline prompt primed with a default value. 19 | // It will return a string (user entered data) or an error. 20 | // Discussion: https://github.com/lmorg/readline/issues/12 21 | func (rl *Instance) ReadlineWithDefault(defaultValue string) (string, error) { 22 | return rl.readline(defaultValue) 23 | } 24 | 25 | func (rl *Instance) readline(defaultValue string) (_ string, err error) { 26 | var state *State 27 | 28 | rl.fdMutex.Lock() 29 | rl.Active = true 30 | if !rl.isNoTty { 31 | state, err = MakeRaw(int(os.Stdin.Fd())) 32 | rl.sigwinch() 33 | } 34 | rl.fdMutex.Unlock() 35 | 36 | if err != nil { 37 | return "", fmt.Errorf("unable to modify fd %d: %s", os.Stdout.Fd(), err.Error()) 38 | } 39 | 40 | if rl.HintText == nil { 41 | rl.HintText = func(_ []rune, _ int) []rune { return nil } 42 | } 43 | 44 | defer func() { 45 | rl.print(rl.clearPreviewStr()) 46 | 47 | rl.fdMutex.Lock() 48 | rl.Active = false 49 | if !rl.isNoTty { 50 | rl.closeSigwinch() 51 | 52 | // return an error if Restore fails. However we don't want to return 53 | // `nil` if there is no error because there might be a CtrlC or EOF 54 | // that needs to be returned 55 | r := Restore(int(os.Stdin.Fd()), state) 56 | if r != nil { 57 | err = r 58 | } 59 | } 60 | rl.fdMutex.Unlock() 61 | 62 | rl.close() 63 | }() 64 | 65 | rl.line.Set(rl, []rune(defaultValue)) 66 | rl.line.SetRunePos(len(defaultValue)) 67 | rl.lineChange = defaultValue 68 | rl.viUndoHistory = []*UnicodeT{rl.line.Duplicate()} 69 | rl.histPos = rl.History.Len() 70 | rl.modeViMode = vimInsert 71 | atomic.StoreInt32(&rl.delayedSyntaxCount, 0) 72 | rl.resetHintText() 73 | rl.resetTabCompletion() 74 | 75 | rl.forceNewLine() 76 | rl.print(rl.prompt + rl.line.String()) 77 | 78 | if len(rl.multiSplit) > 0 { 79 | r := []rune(rl.multiSplit[0]) 80 | rl.print(rl.readlineInputStr(r)) 81 | rl.print(rl.carriageReturnStr()) 82 | if len(rl.multiSplit) > 1 { 83 | rl.multiSplit = rl.multiSplit[1:] 84 | } else { 85 | rl.multiSplit = []string{} 86 | } 87 | return rl.line.String(), nil 88 | } 89 | 90 | rl.cacheTermWidth() 91 | rl.getHintText() 92 | rl.print(rl.renderHelpersStr()) 93 | 94 | readKey: 95 | for { 96 | noTtyCallback(rl) 97 | if rl.line.RuneLen() == 0 { 98 | // clear the cache when the line is cleared 99 | rl.cacheHint.Init(rl) 100 | rl.cacheSyntax.Init(rl) 101 | } 102 | 103 | go delayedSyntaxTimer(rl, atomic.LoadInt32(&rl.delayedSyntaxCount)) 104 | rl.viUndoSkipAppend = false 105 | b := make([]byte, 1024*1024) 106 | var i int 107 | 108 | if !rl.skipStdinRead { 109 | i, err = rl.read(b) 110 | if err != nil { 111 | return "", err 112 | } 113 | rl.cacheTermWidth() 114 | } 115 | atomic.AddInt32(&rl.delayedSyntaxCount, 1) 116 | 117 | rl.skipStdinRead = false 118 | r := []rune(string(b)) 119 | 120 | if isMultiline(r[:i]) || len(rl.multiline) > 0 { 121 | rl.multiline = append(rl.multiline, b[:i]...) 122 | 123 | if !rl.allowMultiline(rl.multiline) { 124 | rl.multiline = []byte{} 125 | continue 126 | } 127 | 128 | s := string(rl.multiline) 129 | rl.multiSplit = rxMultiline.Split(s, -1) 130 | 131 | r = []rune(rl.multiSplit[0]) 132 | rl.modeViMode = vimInsert 133 | rl.print(rl.readlineInputStr(r)) 134 | rl.print(rl.carriageReturnStr()) 135 | rl.multiline = []byte{} 136 | if len(rl.multiSplit) > 1 { 137 | rl.multiSplit = rl.multiSplit[1:] 138 | } else { 139 | rl.multiSplit = []string{} 140 | } 141 | return rl.line.String(), nil 142 | } 143 | 144 | keyPress := string(r[:i]) 145 | if rl.evtKeyPress[keyPress] != nil { 146 | var id int 147 | evtState := rl.newEventState(keyPress) 148 | nextEvent: 149 | rl.print(rl.clearHelpersStr()) 150 | rl.print("\r" + seqClearLine) 151 | evt := rl.evtKeyPress[keyPress](id, evtState) 152 | rl.forceNewLine() 153 | 154 | rl.line.Set(rl, evt.SetLine) 155 | rl.line.SetRunePos(evt.SetPos) 156 | 157 | for _, function := range evt.Actions { 158 | function(rl) 159 | } 160 | 161 | if len(evt.Actions) == 0 { 162 | output := rl.echoStr() 163 | output += rl.renderHelpersStr() 164 | rl.print(output) 165 | } 166 | 167 | if len(evt.HintText) > 0 { 168 | rl.ForceHintTextUpdate(string(evt.HintText)) 169 | } 170 | 171 | switch { 172 | case evt.MoreEvents && evt.Continue: 173 | id++ 174 | goto nextEvent 175 | 176 | case evt.Continue: 177 | break 178 | 179 | default: 180 | goto readKey 181 | } 182 | } 183 | 184 | i = removeNonPrintableChars(b[:i]) 185 | 186 | // Used for syntax completion 187 | rl.lineChange = string(b[:i]) 188 | 189 | // Slow or invisible tab completions shouldn't lock up cursor movement 190 | rl.tabMutex.Lock() 191 | lenTcS := len(rl.tcSuggestions) 192 | rl.tabMutex.Unlock() 193 | if rl.modeTabCompletion && lenTcS == 0 { 194 | if rl.delayedTabContext.cancel != nil { 195 | rl.delayedTabContext.cancel() 196 | } 197 | rl.modeTabCompletion = false 198 | rl.print(rl.updateHelpersStr()) 199 | } 200 | 201 | switch b[0] { 202 | case charCtrlA: 203 | HkFnCursorMoveToStartOfLine(rl) 204 | 205 | case charCtrlC: 206 | output := rl.clearPreviewStr() 207 | output += rl.clearHelpersStr() 208 | rl.print(output) 209 | return "", ErrCtrlC 210 | 211 | case charEOF: 212 | if rl.line.RuneLen() == 0 { 213 | output := rl.clearPreviewStr() 214 | output += rl.clearHelpersStr() 215 | rl.print(output) 216 | return "", ErrEOF 217 | } 218 | 219 | case charCtrlE: 220 | HkFnCursorMoveToEndOfLine(rl) 221 | 222 | case charCtrlF: 223 | HkFnModeFuzzyFind(rl) 224 | 225 | case charCtrlG: 226 | HkFnCancelAction(rl) 227 | 228 | case charCtrlK: 229 | HkFnClearAfterCursor(rl) 230 | 231 | case charCtrlL: 232 | HkFnClearScreen(rl) 233 | 234 | case charCtrlR: 235 | HkFnModeSearchHistory(rl) 236 | 237 | case charCtrlU: 238 | HkFnClearLine(rl) 239 | 240 | case charCtrlZ: 241 | HkFnUndo(rl) 242 | 243 | case charTab: 244 | HkFnModeAutocomplete(rl) 245 | 246 | case '\r': 247 | fallthrough 248 | case '\n': 249 | if rl.modeViMode == vimCommand { 250 | rl.print(rl.vimCommandModeReturnStr()) 251 | continue 252 | } 253 | 254 | var output string 255 | rl.tabMutex.Lock() 256 | var suggestions *suggestionsT 257 | if rl.modeTabFind { 258 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 259 | } else { 260 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 261 | } 262 | rl.tabMutex.Unlock() 263 | 264 | switch { 265 | case rl.previewMode == previewModeOpen: 266 | output += rl.clearPreviewStr() 267 | output += rl.clearHelpersStr() 268 | rl.print(output) 269 | continue 270 | case rl.previewMode == previewModeAutocomplete: 271 | rl.previewMode = previewModeOpen 272 | if !rl.modeTabCompletion { 273 | output += rl.clearPreviewStr() 274 | output += rl.clearHelpersStr() 275 | rl.print(output) 276 | continue 277 | } 278 | } 279 | 280 | if rl.modeTabCompletion || len(rl.tfLine) != 0 /*&& len(suggestions) > 0*/ { 281 | tfLine := rl.tfLine 282 | cell := (rl.tcMaxX * (rl.tcPosY - 1)) + rl.tcOffset + rl.tcPosX - 1 283 | output += rl.clearHelpersStr() 284 | rl.resetTabCompletion() 285 | output += rl.renderHelpersStr() 286 | if suggestions.Len() > 0 { 287 | prefix, line := suggestions.ItemCompletionReturn(cell) 288 | if len(prefix) == 0 && len(rl.tcPrefix) > 0 { 289 | l := -len(rl.tcPrefix) 290 | if l == -1 && rl.line.RuneLen() > 0 && rl.line.RunePos() == rl.line.RuneLen() { 291 | rl.line.Set(rl, rl.line.Runes()[:rl.line.RuneLen()-1]) 292 | } else { 293 | output += rl.viDeleteByAdjustStr(l) 294 | } 295 | } 296 | output += rl.insertStr([]rune(line)) 297 | } else { 298 | output += rl.insertStr(tfLine) 299 | } 300 | rl.print(output) 301 | continue 302 | } 303 | output += rl.carriageReturnStr() 304 | rl.print(output) 305 | return rl.line.String(), nil 306 | 307 | case charBackspace, charBackspace2: 308 | switch { 309 | case rl.modeTabFind: 310 | rl.print(rl.backspaceTabFindStr()) 311 | rl.viUndoSkipAppend = true 312 | case rl.modeViMode == vimCommand: 313 | rl.print(rl.vimCommandModeBackspaceStr()) 314 | default: 315 | rl.print(rl.backspaceStr()) 316 | } 317 | 318 | case charEscape: 319 | rl.print(rl.escapeSeq(r[:i])) 320 | 321 | default: 322 | if rl.modeTabFind { 323 | rl.print(rl.updateTabFindStr(r[:i])) 324 | rl.viUndoSkipAppend = true 325 | } else { 326 | rl.print(rl.readlineInputStr(r[:i])) 327 | if len(rl.multiline) > 0 && rl.modeViMode == vimKeys { 328 | rl.skipStdinRead = true 329 | } 330 | } 331 | } 332 | 333 | rl.undoAppendHistory() 334 | } 335 | } 336 | 337 | func (rl *Instance) escapeSeq(r []rune) string { 338 | var output string 339 | switch string(r) { 340 | case seqEscape: 341 | HkFnCancelAction(rl) 342 | 343 | case seqDelete: 344 | if rl.modeTabFind { 345 | output += rl.backspaceTabFindStr() 346 | } else { 347 | output += rl.deleteStr() 348 | } 349 | 350 | case seqUp: 351 | rl.viUndoSkipAppend = true 352 | 353 | if rl.modeTabCompletion { 354 | rl.moveTabCompletionHighlight(0, -1) 355 | output += rl.renderHelpersStr() 356 | return output 357 | } 358 | 359 | // are we midway through a long line that wrap multiple terminal lines? 360 | posX, posY := rl.lineWrapCellPos() 361 | if posY > 0 { 362 | pos := rl.line.CellPos() - rl.termWidth() + rl.promptLen 363 | rl.line.SetCellPos(pos) 364 | 365 | newX, _ := rl.lineWrapCellPos() 366 | offset := newX - posX 367 | switch { 368 | case offset > 0: 369 | output += moveCursorForwardsStr(offset) 370 | case offset < 0: 371 | output += moveCursorBackwardsStr(offset * -1) 372 | } 373 | 374 | output += moveCursorUpStr(1) 375 | return output 376 | } 377 | 378 | rl.walkHistory(-1) 379 | 380 | case seqDown: 381 | rl.viUndoSkipAppend = true 382 | 383 | if rl.modeTabCompletion { 384 | rl.moveTabCompletionHighlight(0, 1) 385 | output += rl.renderHelpersStr() 386 | return output 387 | } 388 | 389 | // are we midway through a long line that wrap multiple terminal lines? 390 | posX, posY := rl.lineWrapCellPos() 391 | _, lineY := rl.lineWrapCellLen() 392 | if posY < lineY { 393 | pos := rl.line.CellPos() + rl.termWidth() - rl.promptLen 394 | rl.line.SetCellPos(pos) 395 | 396 | newX, _ := rl.lineWrapCellPos() 397 | offset := newX - posX 398 | switch { 399 | case offset > 0: 400 | output += moveCursorForwardsStr(offset) 401 | case offset < 0: 402 | output += moveCursorBackwardsStr(offset * -1) 403 | } 404 | 405 | output += moveCursorDownStr(1) 406 | return output 407 | } 408 | 409 | rl.walkHistory(1) 410 | 411 | case seqBackwards: 412 | if rl.modeTabCompletion { 413 | rl.moveTabCompletionHighlight(-1, 0) 414 | output += rl.renderHelpersStr() 415 | return output 416 | } 417 | 418 | output += rl.moveCursorByRuneAdjustStr(-1) 419 | rl.viUndoSkipAppend = true 420 | 421 | case seqForwards: 422 | if rl.modeTabCompletion { 423 | rl.moveTabCompletionHighlight(1, 0) 424 | output += rl.renderHelpersStr() 425 | return output 426 | } 427 | 428 | //if (rl.modeViMode == vimInsert && rl.line.RunePos() < rl.line.RuneLen()) || 429 | // (rl.modeViMode != vimInsert && rl.line.RunePos() < rl.line.RuneLen()-1) { 430 | output += rl.moveCursorByRuneAdjustStr(1) 431 | //} 432 | rl.viUndoSkipAppend = true 433 | 434 | case seqHome, seqHomeSc: 435 | switch { 436 | case rl.previewMode != previewModeClosed: 437 | output += rl.previewPreviousSectionStr() 438 | return output 439 | 440 | case rl.modeTabCompletion: 441 | return output 442 | 443 | default: 444 | output += rl.moveCursorByRuneAdjustStr(-rl.line.RunePos()) 445 | rl.viUndoSkipAppend = true 446 | } 447 | 448 | case seqEnd, seqEndSc: 449 | switch { 450 | case rl.previewMode != previewModeClosed: 451 | output += rl.previewNextSectionStr() 452 | return output 453 | 454 | case rl.modeTabCompletion: 455 | return output 456 | 457 | default: 458 | output += rl.moveCursorByRuneAdjustStr(rl.line.RuneLen() - rl.line.RunePos()) 459 | rl.viUndoSkipAppend = true 460 | } 461 | 462 | case seqShiftTab: 463 | if rl.modeTabCompletion { 464 | rl.moveTabCompletionHighlight(-1, 0) 465 | output += rl.renderHelpersStr() 466 | return output 467 | } 468 | 469 | case seqPageUp, seqOptUp, seqCtrlUp: 470 | output += rl.previewPageUpStr() 471 | return output 472 | 473 | case seqPageDown, seqOptDown, seqCtrlDown: 474 | output += rl.previewPageDownStr() 475 | return output 476 | 477 | case seqF1, seqF1VT100: 478 | HkFnModePreviewToggle(rl) 479 | return output 480 | 481 | case seqF9: 482 | HkFnModePreviewLine(rl) 483 | return output 484 | 485 | case seqAltF, seqOptRight, seqCtrlRight: 486 | switch { 487 | case rl.previewMode != previewModeClosed: 488 | output += rl.previewNextSectionStr() 489 | return output 490 | 491 | default: 492 | HkFnCursorJumpForwards(rl) 493 | } 494 | 495 | case seqAltB, seqOptLeft, seqCtrlLeft: 496 | switch { 497 | case rl.previewMode != previewModeClosed: 498 | output += rl.previewPreviousSectionStr() 499 | return output 500 | 501 | default: 502 | HkFnCursorJumpBackwards(rl) 503 | } 504 | 505 | case seqShiftF1: 506 | HkFnRecallWord1(rl) 507 | case seqShiftF2: 508 | HkFnRecallWord2(rl) 509 | case seqShiftF3: 510 | HkFnRecallWord3(rl) 511 | case seqShiftF4: 512 | HkFnRecallWord4(rl) 513 | case seqShiftF5: 514 | HkFnRecallWord5(rl) 515 | case seqShiftF6: 516 | HkFnRecallWord6(rl) 517 | case seqShiftF7: 518 | HkFnRecallWord7(rl) 519 | case seqShiftF8: 520 | HkFnRecallWord8(rl) 521 | case seqShiftF9: 522 | HkFnRecallWord9(rl) 523 | case seqShiftF10: 524 | HkFnRecallWord10(rl) 525 | case seqShiftF11: 526 | HkFnRecallWord11(rl) 527 | case seqShiftF12: 528 | HkFnRecallWord12(rl) 529 | 530 | default: 531 | if rl.modeTabFind /*|| rl.modeAutoFind*/ { 532 | //rl.modeTabFind = false 533 | //rl.modeAutoFind = false 534 | return output 535 | } 536 | // alt+numeric append / delete 537 | if len(r) == 2 && '1' <= r[1] && r[1] <= '9' { 538 | if rl.modeViMode == vimDelete { 539 | output += rl.vimDeleteStr(r) 540 | return output 541 | } 542 | 543 | } else { 544 | rl.viUndoSkipAppend = true 545 | } 546 | } 547 | 548 | return output 549 | } 550 | 551 | // readlineInput is an unexported function used to determine what mode of text 552 | // entry readline is currently configured for and then update the line entries 553 | // accordingly. 554 | func (rl *Instance) readlineInputStr(r []rune) string { 555 | if len(r) == 0 { 556 | return "" 557 | } 558 | 559 | var output string 560 | 561 | switch rl.modeViMode { 562 | case vimKeys: 563 | output += rl.vi(r[0]) 564 | output += rl.viHintMessageStr() 565 | 566 | case vimDelete: 567 | output += rl.vimDeleteStr(r) 568 | output += rl.viHintMessageStr() 569 | 570 | case vimReplaceOnce: 571 | rl.modeViMode = vimKeys 572 | output += rl.deleteStr() 573 | output += rl.insertStr([]rune{r[0]}) 574 | output += rl.viHintMessageStr() 575 | 576 | case vimReplaceMany: 577 | for _, char := range r { 578 | output += rl.deleteStr() 579 | output += rl.insertStr([]rune{char}) 580 | } 581 | output += rl.viHintMessageStr() 582 | 583 | case vimCommand: 584 | output += rl.vimCommandModeInput(r) 585 | output += rl.viHintMessageStr() 586 | 587 | default: 588 | output += rl.insertStr(r) 589 | } 590 | 591 | return output 592 | } 593 | 594 | // SetPrompt will define the readline prompt string. 595 | // It also calculates the runes in the string as well as any non-printable 596 | // escape codes. 597 | func (rl *Instance) SetPrompt(s string) { 598 | s = strings.ReplaceAll(s, "\r", "") 599 | s = strings.ReplaceAll(s, "\t", " ") 600 | split := strings.Split(s, "\n") 601 | if len(split) > 1 { 602 | rl.print(strings.Join(split[:len(split)-1], "\r\n") + "\r\n") 603 | s = split[len(split)-1] 604 | } 605 | rl.prompt = s 606 | rl.promptLen = strLen(s) 607 | } 608 | 609 | func (rl *Instance) carriageReturnStr() string { 610 | output := rl.clearHelpersStr() 611 | output += "\r\n" 612 | if rl.HistoryAutoWrite { 613 | var err error 614 | rl.histPos, err = rl.History.Write(rl.line.String()) 615 | if err != nil { 616 | output += err.Error() + "\r\n" 617 | } 618 | } 619 | return output 620 | } 621 | 622 | func isMultiline(r []rune) bool { 623 | for i := range r { 624 | if (r[i] == '\r' || r[i] == '\n') && i != len(r)-1 { 625 | return true 626 | } 627 | } 628 | return false 629 | } 630 | 631 | func (rl *Instance) allowMultiline(data []byte) bool { 632 | rl.printf("%s\r\nWARNING: %d bytes of multiline data was dumped into the shell!", 633 | rl.clearHelpersStr(), len(data)) 634 | 635 | for { 636 | rl.print("\r\nDo you wish to proceed (yes|no|preview)? [y/n/p] ") 637 | 638 | b := make([]byte, 1024*1024) 639 | 640 | i, err := rl.read(b) 641 | if err != nil { 642 | return false 643 | } 644 | 645 | if i > 1 { 646 | rl.multiline = append(rl.multiline, b[:i]...) 647 | rl.print(moveCursorUpStr(2)) 648 | return rl.allowMultiline(append(data, b[:i]...)) 649 | } 650 | 651 | s := string(b[:i]) 652 | rl.print(s) 653 | 654 | switch s { 655 | case "y", "Y": 656 | rl.print("\r\n" + rl.prompt) 657 | return true 658 | 659 | case "n", "N": 660 | rl.print("\r\n" + rl.prompt) 661 | return false 662 | 663 | case "p", "P": 664 | preview := string(bytes.Replace(data, []byte{'\r'}, []byte{'\r', '\n'}, -1)) 665 | if rl.SyntaxHighlighter != nil { 666 | preview = rl.SyntaxHighlighter([]rune(preview)) 667 | } 668 | rl.print("\r\n" + preview) 669 | 670 | default: 671 | rl.print("\r\nInvalid response. Please answer `y` (yes), `n` (no) or `p` (preview)") 672 | } 673 | } 674 | } 675 | 676 | func (rl *Instance) forceNewLine() { 677 | x, _ := rl.getCursorPos() 678 | switch x { 679 | case -1: 680 | rl.print(string(rl.leftMost())) 681 | case 0: 682 | // do nothing 683 | default: 684 | rl.print("\r\n") 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /runecache.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "github.com/mattn/go-runewidth" 5 | ) 6 | 7 | //var _runeWidthTruncate = make(map[string]string) 8 | 9 | func runeWidthTruncate(s string, maxLength int) string { 10 | /*key := fmt.Sprintf("%s:%d", s, maxLength) 11 | 12 | r, ok := _runeWidthTruncate[key] 13 | if ok { 14 | return r 15 | } 16 | 17 | r = runewidth.Truncate(s, maxLength, "…") 18 | _runeWidthTruncate[key] = r 19 | return r*/ 20 | 21 | return runewidth.Truncate(s, maxLength, "…") 22 | } 23 | 24 | //var _runeWidthFillRight = make(map[string]string) 25 | 26 | func runeWidthFillRight(s string, maxLength int) string { 27 | /*key := fmt.Sprintf("%s:%d", s, maxLength) 28 | 29 | r, ok := _runeWidthFillRight[key] 30 | if ok { 31 | return r 32 | } 33 | 34 | r = runewidth.FillRight(s, maxLength) 35 | _runeWidthFillRight[key] = r 36 | return r*/ 37 | 38 | return runewidth.FillRight(s, maxLength) 39 | } 40 | -------------------------------------------------------------------------------- /signal_fallback.go: -------------------------------------------------------------------------------- 1 | //go:build windows || js || plan9 2 | // +build windows js plan9 3 | 4 | package readline 5 | 6 | func (rl *Instance) sigwinch() { 7 | rl.closeSigwinch = func() { 8 | // empty function because SIGWINCH isn't supported on these platforms 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /signal_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !js && !plan9 2 | // +build !windows,!js,!plan9 3 | 4 | package readline 5 | 6 | import ( 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func (rl *Instance) sigwinch() { 13 | if rl.isNoTty { 14 | rl.closeSigwinch = func() {} 15 | return 16 | } 17 | 18 | ch := make(chan os.Signal, 1) 19 | signal.Notify(ch, syscall.SIGWINCH) 20 | go func() { 21 | for range ch { 22 | 23 | width := GetTermWidth() 24 | 25 | switch { 26 | case !rl.modeTabCompletion || width == rl.termWidth(): 27 | // no nothing 28 | 29 | case width < rl.termWidth(): 30 | rl._termWidth = width 31 | HkFnClearScreen(rl) 32 | 33 | default: 34 | rl._termWidth = width 35 | } 36 | 37 | } 38 | }() 39 | 40 | rl.closeSigwinch = func() { 41 | signal.Stop(ch) 42 | close(ch) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /suggestions.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "github.com/mattn/go-runewidth" 4 | 5 | type suggestionsT struct { 6 | rl *Instance 7 | suggestions []string 8 | prefixWidth int 9 | } 10 | 11 | func newSuggestionsT(rl *Instance, suggestions []string) *suggestionsT { 12 | return &suggestionsT{ 13 | rl: rl, 14 | suggestions: suggestions, 15 | prefixWidth: runewidth.StringWidth(rl.tcPrefix), 16 | } 17 | } 18 | 19 | func (s *suggestionsT) Len() int { 20 | return len(s.suggestions) 21 | } 22 | 23 | func (s *suggestionsT) ItemLen(index int) int { 24 | // fast 25 | switch { 26 | case len(s.suggestions[index]) == 0: 27 | return s.prefixWidth 28 | 29 | case s.suggestions[index][0] == 2: 30 | if len(s.suggestions[index]) == 1 { 31 | return 0 32 | } 33 | return len(s.suggestions[index][1:]) 34 | default: 35 | return len(s.suggestions[index]) + s.prefixWidth 36 | } 37 | 38 | // accurate 39 | /*switch { 40 | case len(s.suggestions[index]) == 0: 41 | return runewidth.StringWidth(s.rl.tcPrefix) 42 | 43 | case s.suggestions[index][0] == 2: 44 | if len(s.suggestions[index]) == 1 { 45 | return 0 46 | } 47 | return runewidth.StringWidth(s.suggestions[index][1:]) 48 | default: 49 | return runewidth.StringWidth(s.suggestions[index]) + s.prefixWidth 50 | }*/ 51 | } 52 | 53 | func (s *suggestionsT) ItemValue(index int) string { 54 | switch { 55 | case len(s.suggestions[index]) == 0: 56 | return s.rl.tcPrefix 57 | 58 | case s.suggestions[index][0] == 2: 59 | if len(s.suggestions[index]) == 1 { 60 | return "" 61 | } 62 | return s.suggestions[index][1:] 63 | default: 64 | return s.rl.tcPrefix + s.suggestions[index] 65 | } 66 | } 67 | 68 | func (s *suggestionsT) ItemLookupValue(index int) string { 69 | return s.suggestions[index] 70 | /*switch { 71 | case len(s.suggestions[index]) == 0: 72 | return "" 73 | 74 | case s.suggestions[index][0] == 2: 75 | if len(s.suggestions[index]) == 1 { 76 | return "" 77 | } 78 | return s.suggestions[index][1:] 79 | default: 80 | return s.suggestions[index] 81 | }*/ 82 | } 83 | 84 | func (s *suggestionsT) ItemCompletionReturn(index int) (string, string) { 85 | switch { 86 | case len(s.suggestions[index]) == 0: 87 | return s.rl.tcPrefix, "" 88 | 89 | case s.suggestions[index][0] == 2: 90 | if len(s.suggestions[index]) == 1 { 91 | return s.rl.tcPrefix, "" 92 | } 93 | return "", s.suggestions[index][1:] 94 | default: 95 | return s.rl.tcPrefix, s.suggestions[index] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /syntax.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | func (rl *Instance) syntaxCompletion() { 4 | if rl.SyntaxCompleter == nil { 5 | return 6 | } 7 | 8 | newLine, newPos := rl.SyntaxCompleter(rl.line.Runes(), rl.lineChange, rl.line.RunePos()-1) 9 | if string(newLine) == rl.line.String() { 10 | return 11 | } 12 | 13 | newPos++ 14 | 15 | rl.line.Set(rl, newLine) 16 | rl.line.SetRunePos(newPos) 17 | } 18 | -------------------------------------------------------------------------------- /tab.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "context" 5 | "os" 6 | ) 7 | 8 | // TabDisplayType defines how the autocomplete suggestions display 9 | type TabDisplayType int 10 | 11 | const ( 12 | // TabDisplayGrid is the default. It's where the screen below the prompt is 13 | // divided into a grid with each suggestion occupying an individual cell. 14 | TabDisplayGrid = iota 15 | 16 | // TabDisplayList is where suggestions are displayed as a list with a 17 | // description. The suggestion gets highlighted but both are searchable (ctrl+f) 18 | TabDisplayList 19 | 20 | // TabDisplayMap is where suggestions are displayed as a list with a 21 | // description however the description is what gets highlighted and only 22 | // that is searchable (ctrl+f). The benefit of TabDisplayMap is when your 23 | // autocomplete suggestions are IDs rather than human terms. 24 | TabDisplayMap 25 | ) 26 | 27 | func (rl *Instance) getTabCompletion() { 28 | rl.tcOffset = 0 29 | if rl.TabCompleter == nil { 30 | return 31 | } 32 | 33 | if rl.delayedTabContext.cancel != nil { 34 | rl.delayedTabContext.cancel() 35 | } 36 | 37 | rl.delayedTabContext = DelayedTabContext{rl: rl} 38 | rl.delayedTabContext.Context, rl.delayedTabContext.cancel = context.WithCancel(context.Background()) 39 | 40 | if rl.modeViMode == vimCommand { 41 | rl.tcr = rl.vimCommandModeSuggestions() 42 | 43 | } else { 44 | 45 | rl.tcr = rl.TabCompleter(rl.line.Runes(), rl.line.RunePos(), rl.delayedTabContext) 46 | } 47 | if rl.tcr == nil { 48 | return 49 | } 50 | 51 | rl.tabMutex.Lock() 52 | rl.tcPrefix, rl.tcSuggestions, rl.tcDescriptions, rl.tcDisplayType = rl.tcr.Prefix, rl.tcr.Suggestions, rl.tcr.Descriptions, rl.tcr.DisplayType 53 | if len(rl.tcDescriptions) == 0 { 54 | // probably not needed, but just in case someone doesn't initialize the 55 | // map in their API call. 56 | rl.tcDescriptions = make(map[string]string) 57 | } 58 | rl.tabMutex.Unlock() 59 | 60 | rl.initTabCompletion() 61 | } 62 | 63 | func (rl *Instance) initTabCompletion() { 64 | rl.modeTabCompletion = true 65 | rl.autocompleteHeightAdjust() 66 | 67 | if rl.tcDisplayType == TabDisplayGrid { 68 | rl.initTabGrid() 69 | } else { 70 | rl.initTabMap() 71 | } 72 | } 73 | 74 | func (rl *Instance) autocompleteHeightAdjust() { 75 | var ( 76 | height int 77 | err error 78 | ) 79 | 80 | if rl.isNoTty { 81 | height = 25 82 | } else { 83 | _, height, err = GetSize(int(os.Stdout.Fd())) 84 | if err != nil { 85 | height = 25 86 | } 87 | } 88 | 89 | rl.previewAutocompleteHeight(height) 90 | 91 | switch { 92 | case height <= 4: 93 | rl.MaxTabCompleterRows = 1 94 | case height-4 <= rl.MaxTabCompleterRows: 95 | rl.MaxTabCompleterRows = height - 4 96 | } 97 | } 98 | 99 | func (rl *Instance) moveTabCompletionHighlight(x, y int) { 100 | if rl.tcDisplayType == TabDisplayGrid { 101 | rl.moveTabGridHighlight(x, y) 102 | } else { 103 | rl.moveTabMapHighlight(x, y) 104 | } 105 | } 106 | 107 | func (rl *Instance) writeTabCompletionStr() string { 108 | if !rl.modeTabCompletion { 109 | return "" 110 | } 111 | 112 | posX, posY := rl.lineWrapCellPos() 113 | _, lineY := rl.lineWrapCellLen() 114 | output := moveCursorDownStr(rl.hintY + lineY - posY) 115 | output += "\r\n" + seqClearScreenBelow 116 | 117 | switch rl.tcDisplayType { 118 | case TabDisplayGrid: 119 | output += rl.writeTabGridStr() 120 | 121 | case TabDisplayMap: 122 | output += rl.writeTabMapStr() 123 | 124 | case TabDisplayList: 125 | output += rl.writeTabMapStr() 126 | 127 | default: 128 | output += rl.writeTabGridStr() 129 | } 130 | 131 | output += moveCursorUpStr(rl.hintY + rl.tcUsedY + lineY - posY) 132 | output += "\r" + moveCursorForwardsStr(posX) 133 | 134 | return output 135 | } 136 | 137 | func (rl *Instance) resetTabCompletion() { 138 | rl.modeTabCompletion = false 139 | rl.tcOffset = 0 140 | rl.tcUsedY = 0 141 | rl.modeTabFind = false 142 | rl.modeAutoFind = false 143 | rl.tfLine = []rune{} 144 | } 145 | -------------------------------------------------------------------------------- /tabfind.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "strings" 4 | 5 | func (rl *Instance) backspaceTabFindStr() string { 6 | if len(rl.tfLine) > 0 { 7 | rl.tfLine = rl.tfLine[:len(rl.tfLine)-1] 8 | } 9 | return rl.updateTabFindStr([]rune{}) 10 | } 11 | 12 | func _updateTabFindHelpersStr(rl *Instance) (output string) { 13 | rl.tabMutex.Unlock() 14 | output = rl.clearHelpersStr() 15 | rl.initTabCompletion() 16 | output += rl.renderHelpersStr() 17 | return 18 | } 19 | 20 | func (rl *Instance) updateTabFindStr(r []rune) string { 21 | rl.tfLine = append(rl.tfLine, r...) 22 | 23 | rl.tabMutex.Lock() 24 | 25 | if len(rl.tfLine) == 0 { 26 | rl.hintText = rFindSearchPart 27 | rl.tfSuggestions = append(rl.tcSuggestions, []string{}...) 28 | return _updateTabFindHelpersStr(rl) 29 | } 30 | 31 | var ( 32 | find findT 33 | err error 34 | ) 35 | 36 | find, rl.rFindSearch, rl.rFindCancel, err = newFuzzyFind(string(rl.tfLine)) 37 | if err != nil { 38 | rl.tfSuggestions = []string{err.Error()} 39 | return _updateTabFindHelpersStr(rl) 40 | } 41 | 42 | rl.hintText = append(rl.rFindSearch, rl.tfLine...) 43 | rl.hintText = append(rl.hintText, []rune(seqReset+seqBlink+"_"+seqReset)...) 44 | 45 | rl.tfSuggestions = make([]string, 0) 46 | for i := range rl.tcSuggestions { 47 | if find.MatchString(strings.TrimSpace(rl.tcSuggestions[i])) { 48 | rl.tfSuggestions = append(rl.tfSuggestions, rl.tcSuggestions[i]) 49 | 50 | } else if rl.tcDisplayType == TabDisplayList && find.MatchString(rl.tcDescriptions[rl.tcSuggestions[i]]) { 51 | // this is a list so lets also check the descriptions 52 | rl.tfSuggestions = append(rl.tfSuggestions, rl.tcSuggestions[i]) 53 | } 54 | } 55 | 56 | return _updateTabFindHelpersStr(rl) 57 | } 58 | 59 | func (rl *Instance) resetTabFindStr() string { 60 | rl.modeTabFind = false 61 | rl.tfLine = []rune{} 62 | if rl.modeAutoFind { 63 | rl.hintText = []rune{} 64 | } else { 65 | rl.hintText = rl.rFindCancel 66 | } 67 | rl.modeAutoFind = false 68 | 69 | output := rl.clearHelpersStr() 70 | rl.initTabCompletion() 71 | output += rl.renderHelpersStr() 72 | return output 73 | } 74 | -------------------------------------------------------------------------------- /tabgrid.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/mattn/go-runewidth" 8 | ) 9 | 10 | func (rl *Instance) initTabGrid() { 11 | rl.tabMutex.Lock() 12 | defer rl.tabMutex.Unlock() 13 | 14 | var suggestions *suggestionsT 15 | if rl.modeTabFind { 16 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 17 | } else { 18 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 19 | } 20 | 21 | rl.tcMaxLength = rl.MinTabItemLength 22 | 23 | for i := 0; i < suggestions.Len(); i++ { 24 | l := suggestions.ItemLen(i) 25 | if l > rl.tcMaxLength { 26 | rl.tcMaxLength = l 27 | } 28 | } 29 | 30 | if rl.tcMaxLength > rl.MaxTabItemLength && rl.MaxTabItemLength > 0 && rl.MaxTabItemLength > rl.MinTabItemLength { 31 | rl.tcMaxLength = rl.MaxTabItemLength 32 | } 33 | if rl.tcMaxLength == 0 { 34 | rl.tcMaxLength = 20 35 | } 36 | 37 | rl.tcPosX = 1 38 | rl.tcPosY = 1 39 | rl.tcMaxX = rl.termWidth() / (rl.tcMaxLength + 2) 40 | rl.tcOffset = 0 41 | 42 | // avoid a divide by zero error 43 | if rl.tcMaxX < 1 { 44 | rl.tcMaxX = 1 45 | } 46 | 47 | rl.tcMaxY = rl.MaxTabCompleterRows 48 | 49 | // pre-cache 50 | max := rl.tcMaxX * rl.tcMaxY 51 | if max > len(rl.tcSuggestions) { 52 | max = len(rl.tcSuggestions) 53 | } 54 | subset := rl.tcSuggestions[:max] 55 | 56 | if rl.tcr.HintCache == nil { 57 | return 58 | } 59 | 60 | go rl.tabHintCache(subset) 61 | } 62 | 63 | func (rl *Instance) tabHintCache(subset []string) { 64 | hints := rl.tcr.HintCache(rl.tcPrefix, subset) 65 | if len(hints) != len(subset) { 66 | return 67 | } 68 | 69 | rl.tabMutex.Lock() 70 | for i := range subset { 71 | rl.tcDescriptions[subset[i]] = hints[i] 72 | } 73 | rl.tabMutex.Unlock() 74 | 75 | } 76 | 77 | func (rl *Instance) moveTabGridHighlight(x, y int) { 78 | rl.tabMutex.Lock() 79 | defer rl.tabMutex.Unlock() 80 | 81 | var suggestions *suggestionsT 82 | if rl.modeTabFind { 83 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 84 | } else { 85 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 86 | } 87 | 88 | rl.tcPosX += x 89 | rl.tcPosY += y 90 | 91 | if rl.tcPosX < 1 { 92 | rl.tcPosX = rl.tcMaxX 93 | rl.tcPosY-- 94 | } 95 | 96 | if rl.tcPosX > rl.tcMaxX { 97 | rl.tcPosX = 1 98 | rl.tcPosY++ 99 | } 100 | 101 | if rl.tcPosY < 1 { 102 | rl.tcPosY = rl.tcUsedY 103 | } 104 | 105 | if rl.tcPosY > rl.tcUsedY { 106 | rl.tcPosY = 1 107 | } 108 | 109 | if rl.tcPosY == rl.tcUsedY && (rl.tcMaxX*(rl.tcPosY-1))+rl.tcPosX > suggestions.Len() { 110 | if x < 0 { 111 | rl.tcPosX = suggestions.Len() - (rl.tcMaxX * (rl.tcPosY - 1)) 112 | } 113 | 114 | if x > 0 { 115 | rl.tcPosX = 1 116 | rl.tcPosY = 1 117 | } 118 | 119 | if y < 0 { 120 | rl.tcPosY-- 121 | } 122 | 123 | if y > 0 { 124 | rl.tcPosY = 1 125 | } 126 | } 127 | } 128 | 129 | func (rl *Instance) writeTabGridStr() string { 130 | rl.tabMutex.Lock() 131 | defer rl.tabMutex.Unlock() 132 | 133 | var suggestions *suggestionsT 134 | if rl.modeTabFind { 135 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 136 | } else { 137 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 138 | } 139 | 140 | iCellWidth := (rl.termWidth() / rl.tcMaxX) - 2 141 | cellWidth := strconv.Itoa(iCellWidth) 142 | 143 | x := 0 144 | y := 1 145 | rl.previewItem = "" 146 | var output string 147 | 148 | for i := 0; i < suggestions.Len(); i++ { 149 | x++ 150 | if x > rl.tcMaxX { 151 | x = 1 152 | y++ 153 | if y > rl.tcMaxY { 154 | y-- 155 | break 156 | } else { 157 | output += "\r\n" 158 | } 159 | } 160 | 161 | if x == rl.tcPosX && y == rl.tcPosY { 162 | output += seqBgWhite + seqFgBlack 163 | rl.previewItem = suggestions.ItemValue(i) 164 | } 165 | 166 | value := suggestions.ItemValue(i) 167 | caption := cropCaption(value, rl.tcMaxLength, iCellWidth) 168 | if caption != value { 169 | rl.tcDescriptions[suggestions.ItemLookupValue(i)] = value 170 | } 171 | 172 | output += fmt.Sprintf(" %-"+cellWidth+"s %s", caption, seqReset) 173 | } 174 | 175 | rl.tcUsedY = y 176 | 177 | return output 178 | } 179 | 180 | func cropCaption(caption string, tcMaxLength int, iCellWidth int) string { 181 | switch { 182 | case iCellWidth == 0: 183 | // this condition shouldn't ever happen but lets cover it just in case 184 | return "" 185 | 186 | case runewidth.StringWidth(caption) != len(caption): 187 | // string length != rune width. So lets not do anything too clever 188 | //return runewidth.Truncate(caption, iCellWidth, "…") 189 | return runeWidthTruncate(caption, iCellWidth) 190 | 191 | case len(caption) < tcMaxLength, 192 | len(caption) < 5, 193 | len(caption) <= iCellWidth: 194 | return caption 195 | 196 | case len(caption)-iCellWidth+6 < 1: 197 | // truncate the end 198 | return caption[:iCellWidth-1] + "…" 199 | 200 | case len(caption) > 5+len(caption)-iCellWidth+6: 201 | // truncate long lines in the middle 202 | return caption[:5] + "…" + caption[len(caption)-iCellWidth+6:] 203 | 204 | default: 205 | // edge case reached. lets truncate the most conservative way we can, 206 | // just in case 207 | return runewidth.Truncate(caption, iCellWidth, "…") 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tabgrid_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestCropCaption(t *testing.T) { 9 | // We aren't really bothered about the quality of the output here, just 10 | // testing that the function doesn't generate any slice out of bounds 11 | // exceptions 12 | 13 | var caption, maxLen, cellWidth int 14 | 15 | defer func() { 16 | if r := recover(); r != nil { 17 | t.Errorf("panic raised on iteration %d,%d,%d: %s", caption, maxLen, cellWidth, r) 18 | } 19 | }() 20 | 21 | for caption = 0; caption < 10; caption++ { 22 | for maxLen = 0; maxLen < 10; maxLen++ { 23 | for cellWidth = 0; cellWidth < 10; cellWidth++ { 24 | _ = cropCaption(strings.Repeat("s", caption), maxLen, cellWidth) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tabmap.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "fmt" 4 | 5 | func (rl *Instance) initTabMap() { 6 | rl.tabMutex.Lock() 7 | defer rl.tabMutex.Unlock() 8 | 9 | var suggestions *suggestionsT 10 | if rl.modeTabFind { 11 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 12 | } else { 13 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 14 | } 15 | 16 | rl.tcMaxLength = 1 17 | //for i := range suggestions { 18 | for i := 0; i < suggestions.Len(); i++ { 19 | if rl.tcDisplayType == TabDisplayList { 20 | if suggestions.ItemLen(i) > rl.tcMaxLength { 21 | rl.tcMaxLength = suggestions.ItemLen(i) 22 | } 23 | 24 | } else { 25 | if len(rl.tcDescriptions[suggestions.ItemLookupValue(i)]) > rl.tcMaxLength { 26 | rl.tcMaxLength = len(rl.tcDescriptions[suggestions.ItemLookupValue(i)]) 27 | } 28 | } 29 | } 30 | 31 | rl.tcPosX = 1 32 | rl.tcPosY = 1 33 | rl.tcOffset = 0 34 | rl.tcMaxX = 1 35 | 36 | if suggestions.Len() > rl.MaxTabCompleterRows { 37 | rl.tcMaxY = rl.MaxTabCompleterRows 38 | } else { 39 | rl.tcMaxY = suggestions.Len() 40 | } 41 | } 42 | 43 | func (rl *Instance) moveTabMapHighlight(x, y int) { 44 | rl.tabMutex.Lock() 45 | defer rl.tabMutex.Unlock() 46 | 47 | var suggestions *suggestionsT 48 | if rl.modeTabFind { 49 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 50 | } else { 51 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 52 | } 53 | 54 | rl.tcPosY += x 55 | rl.tcPosY += y 56 | 57 | if rl.tcPosY < 1 { 58 | rl.tcPosY = 1 59 | rl.tcOffset-- 60 | } 61 | 62 | if rl.tcPosY > rl.tcMaxY { 63 | rl.tcPosY-- 64 | rl.tcOffset++ 65 | } 66 | 67 | if rl.tcOffset+rl.tcPosY < 1 && suggestions.Len() > 0 { 68 | rl.tcPosY = rl.tcMaxY 69 | rl.tcOffset = suggestions.Len() - rl.tcMaxY 70 | } 71 | 72 | if rl.tcOffset < 0 { 73 | rl.tcOffset = 0 74 | } 75 | 76 | if rl.tcOffset+rl.tcPosY > suggestions.Len() { 77 | rl.tcPosY = 1 78 | rl.tcOffset = 0 79 | } 80 | } 81 | 82 | func (rl *Instance) writeTabMapStr() string { 83 | rl.tabMutex.Lock() 84 | defer rl.tabMutex.Unlock() 85 | 86 | var suggestions *suggestionsT 87 | if rl.modeTabFind { 88 | suggestions = newSuggestionsT(rl, rl.tfSuggestions) 89 | } else { 90 | suggestions = newSuggestionsT(rl, rl.tcSuggestions) 91 | } 92 | 93 | if rl.termWidth() < 10 { 94 | // terminal too small. Probably better we do nothing instead of crash 95 | return "" 96 | } 97 | 98 | maxLength := rl.tcMaxLength 99 | if maxLength > rl.termWidth()-9 { 100 | maxLength = rl.termWidth() - 9 101 | } 102 | maxDescWidth := rl.termWidth() - maxLength - 4 103 | 104 | y := 0 105 | rl.previewItem = "" 106 | 107 | // bit of a kludge. Really should find where the code is "\n"ing 108 | output := moveCursorUpStr(1) 109 | 110 | isTabDisplayList := rl.tcDisplayType == TabDisplayList 111 | 112 | var item, description string 113 | for i := rl.tcOffset; i < suggestions.Len(); i++ { 114 | y++ 115 | if y > rl.tcMaxY { 116 | break 117 | } 118 | 119 | if isTabDisplayList { 120 | item = runeWidthTruncate(suggestions.ItemValue(i), maxLength) 121 | description = runeWidthTruncate(rl.tcDescriptions[suggestions.ItemLookupValue(i)], maxDescWidth) 122 | 123 | } else { 124 | item = runeWidthTruncate(suggestions.ItemValue(i), maxDescWidth) 125 | description = runeWidthTruncate(rl.tcDescriptions[suggestions.ItemLookupValue(i)], maxLength) 126 | } 127 | 128 | if isTabDisplayList { 129 | output += fmt.Sprintf("\r\n%s %s %s %s", 130 | highlight(rl, y), runeWidthFillRight(item, maxLength), 131 | seqReset, description) 132 | 133 | } else { 134 | output += fmt.Sprintf("\r\n %s %s %s %s", 135 | runeWidthFillRight(description, maxLength), highlight(rl, y), 136 | runeWidthFillRight(item, maxDescWidth), seqReset) 137 | 138 | } 139 | 140 | if y == rl.tcPosY { 141 | rl.previewItem = suggestions.ItemValue(i) 142 | } 143 | } 144 | 145 | if suggestions.Len() < rl.tcMaxX { 146 | rl.tcUsedY = suggestions.Len() 147 | } else { 148 | rl.tcUsedY = rl.tcMaxY 149 | } 150 | 151 | return output 152 | } 153 | 154 | func highlight(rl *Instance, y int) string { 155 | if y == rl.tcPosY { 156 | return seqBgWhite + seqFgBlack 157 | } 158 | return "" 159 | } 160 | -------------------------------------------------------------------------------- /term.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "os" 4 | 5 | // GetTermWidth returns the width of Stdout or 80 if the width cannot be established 6 | func GetTermWidth() (termWidth int) { 7 | var err error 8 | fd := int(os.Stdout.Fd()) 9 | termWidth, _, err = GetSize(fd) 10 | if err != nil { 11 | termWidth = 80 // default to 80 with term width unknown as that is the de factor standard on older terms. 12 | } 13 | 14 | return 15 | } 16 | 17 | func (rl *Instance) termWidth() int { 18 | return rl._termWidth 19 | } 20 | 21 | func (rl *Instance) cacheTermWidth() { 22 | if rl.isNoTty { 23 | return 24 | } 25 | 26 | rl._termWidth = GetTermWidth() 27 | } 28 | -------------------------------------------------------------------------------- /test_notty/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lmorg/readline/test_notty 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/lmorg/readline/v4 v4.0.1 7 | golang.org/x/term v0.31.0 8 | ) 9 | 10 | require ( 11 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 // indirect 12 | github.com/mattn/go-runewidth v0.0.16 // indirect 13 | github.com/rivo/uniseg v0.2.0 // indirect 14 | golang.org/x/sys v0.32.0 // indirect 15 | ) 16 | 17 | replace github.com/lmorg/readline/v4 => ../../readline 18 | -------------------------------------------------------------------------------- /test_notty/go.sum: -------------------------------------------------------------------------------- 1 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4 h1:JCEM0M0HAN8m1T7GeNjIaJJUyBR/ykRvGR15/aZcK7s= 2 | github.com/lmorg/murex v0.0.0-20250115225944-b4c429617fd4/go.mod h1:flXYnXpFa6j4keWDv0plQDvy5gfyw0ifSa28IT95cZA= 3 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 4 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 5 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 6 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 7 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 8 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 9 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 10 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 11 | -------------------------------------------------------------------------------- /test_notty/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/lmorg/readline/v4" 8 | "golang.org/x/term" 9 | ) 10 | 11 | func main() { 12 | rl := readline.NewInstance() 13 | 14 | ch := rl.MakeNoTtyChan() 15 | 16 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer term.Restore(int(os.Stdin.Fd()), oldState) 21 | 22 | go func() { 23 | for { 24 | p := make([]byte, 1024) 25 | i, err := os.Stdin.Read(p) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | rl.KeyPress(p[:i]) 31 | } 32 | }() 33 | 34 | go func() { 35 | for { 36 | callback := <-ch 37 | fmt.Printf("\r\n>>> %s\r\n(%s)", callback.Line.String(), callback.Hint) 38 | } 39 | }() 40 | 41 | _, _ = rl.Readline() 42 | } 43 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | 7 | "github.com/lmorg/murex/utils/lists" 8 | ) 9 | 10 | func delayedSyntaxTimer(rl *Instance, i int32) { 11 | if rl.PasswordMask != 0 || rl.DelayedSyntaxWorker == nil { 12 | return 13 | } 14 | 15 | if rl.cacheSyntax.Get(rl.line.Runes()) != "" { 16 | return 17 | } 18 | 19 | if rl.line.CellLen()+rl.promptLen > rl.termWidth() { 20 | // line wraps, which is hard to do with random ANSI escape sequences 21 | // so better we don't bother trying. 22 | return 23 | } 24 | 25 | newLine := rl.DelayedSyntaxWorker(rl.line.Runes()) 26 | var sLine string 27 | 28 | if rl.SyntaxHighlighter != nil { 29 | sLine = rl.SyntaxHighlighter(newLine) 30 | } else { 31 | sLine = string(newLine) 32 | } 33 | rl.cacheSyntax.Append(rl.line.Runes(), sLine) 34 | 35 | if atomic.LoadInt32(&rl.delayedSyntaxCount) != i { 36 | return 37 | } 38 | 39 | output := rl.moveCursorToStartStr() 40 | output += sLine 41 | output += rl.moveCursorFromEndToLinePosStr() 42 | rl.print(output) 43 | } 44 | 45 | // DelayedTabContext is a custom context interface for async updates to the tab completions 46 | type DelayedTabContext struct { 47 | rl *Instance 48 | Context context.Context 49 | cancel context.CancelFunc 50 | } 51 | 52 | // AppendSuggestions updates the tab completions with additional suggestions asynchronously 53 | func (dtc *DelayedTabContext) AppendSuggestions(suggestions []string) { 54 | if dtc == nil || dtc.rl == nil { 55 | return 56 | } 57 | 58 | if !dtc.rl.modeTabCompletion { 59 | return 60 | } 61 | 62 | max := dtc.rl.MaxTabCompleterRows * 20 63 | 64 | if len(dtc.rl.tcSuggestions) == 0 { 65 | dtc.rl.ForceHintTextUpdate(" ") 66 | } 67 | 68 | dtc.rl.tabMutex.Lock() 69 | 70 | if dtc.rl.tcDescriptions == nil { 71 | dtc.rl.tcDescriptions = make(map[string]string) 72 | } 73 | 74 | for i := range suggestions { 75 | select { 76 | case <-dtc.Context.Done(): 77 | dtc.rl.tabMutex.Unlock() 78 | return 79 | 80 | default: 81 | if dtc.rl.tcDescriptions[suggestions[i]] != "" || 82 | (len(dtc.rl.tcSuggestions) < max && lists.Match(dtc.rl.tcSuggestions, suggestions[i])) { 83 | // dedup 84 | continue 85 | } 86 | dtc.rl.tcDescriptions[suggestions[i]] = dtc.rl.tcPrefix + suggestions[i] 87 | dtc.rl.tcSuggestions = append(dtc.rl.tcSuggestions, suggestions[i]) 88 | } 89 | } 90 | 91 | dtc.rl.tabMutex.Unlock() 92 | 93 | output := dtc.rl.clearHelpersStr() 94 | //dtc.rl.ForceHintTextUpdate(" ") 95 | output += dtc.rl.renderHelpersStr() 96 | dtc.rl.print(output) 97 | } 98 | 99 | // AppendDescriptions updates the tab completions with additional suggestions + descriptions asynchronously 100 | func (dtc *DelayedTabContext) AppendDescriptions(suggestions map[string]string) { 101 | if dtc.rl == nil { 102 | // This might legitimately happen with some tests 103 | return 104 | } 105 | 106 | if !dtc.rl.modeTabCompletion { 107 | return 108 | } 109 | 110 | max := dtc.rl.MaxTabCompleterRows * 20 111 | 112 | if len(dtc.rl.tcSuggestions) == 0 { 113 | dtc.rl.ForceHintTextUpdate(" ") 114 | } 115 | 116 | dtc.rl.tabMutex.Lock() 117 | 118 | for k := range suggestions { 119 | select { 120 | case <-dtc.Context.Done(): 121 | dtc.rl.tabMutex.Unlock() 122 | return 123 | 124 | default: 125 | if dtc.rl.tcDescriptions[k] != "" || 126 | (len(dtc.rl.tcSuggestions) < max && lists.Match(dtc.rl.tcSuggestions, k)) { 127 | // dedup 128 | continue 129 | } 130 | dtc.rl.tcDescriptions[k] = suggestions[k] 131 | dtc.rl.tcSuggestions = append(dtc.rl.tcSuggestions, k) 132 | } 133 | } 134 | 135 | dtc.rl.tabMutex.Unlock() 136 | 137 | output := dtc.rl.clearHelpersStr() 138 | //dtc.rl.ForceHintTextUpdate(" ") 139 | output += dtc.rl.renderHelpersStr() 140 | dtc.rl.print(output) 141 | } 142 | 143 | func delayedPreviewTimer(rl *Instance, fn PreviewFuncT, size *PreviewSizeT, item string) { 144 | var ctx context.Context 145 | 146 | callback := func(lines []string, pos int, err error) { 147 | if pos == -1 { 148 | if rl.previewCache != nil && rl.previewCache.pos < len(lines) { 149 | pos = rl.previewCache.pos 150 | } else { 151 | pos = 0 152 | } 153 | } 154 | 155 | select { 156 | case <-ctx.Done(): 157 | return 158 | default: 159 | // continue 160 | } 161 | 162 | if err != nil { 163 | rl.ForceHintTextUpdate(err.Error()) 164 | return 165 | } 166 | 167 | rl.previewCache = &previewCacheT{ 168 | item: item, 169 | pos: pos, 170 | len: size.Height, 171 | lines: lines, 172 | size: size, 173 | } 174 | 175 | output, err := rl.previewDrawStr(lines[pos:], size) 176 | 177 | if err != nil { 178 | rl.previewCache = nil 179 | rl.print(output) 180 | return 181 | } 182 | 183 | rl.print(output) 184 | } 185 | 186 | ctx, rl.previewCancel = context.WithCancel(context.Background()) 187 | fn(ctx, rl.line.Runes(), item, rl.PreviewImages, size, callback) 188 | } 189 | -------------------------------------------------------------------------------- /tokenise.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "strings" 4 | 5 | func tokeniseLine(line []rune, linePos int) ([]string, int, int) { 6 | if len(line) == 0 { 7 | return nil, 0, 0 8 | } 9 | 10 | var adjust int 11 | if linePos >= len(line) { 12 | adjust = linePos - len(line) - 1 13 | linePos = len(line) - 1 14 | } 15 | 16 | var index, pos int 17 | var punc bool 18 | 19 | split := make([]string, 1) 20 | 21 | for i, r := range line { 22 | switch { 23 | case (r >= 33 && 47 >= r) || 24 | (r >= 58 && 64 >= r) || 25 | (r >= 91 && 94 >= r) || 26 | r == 96 || 27 | (r >= 123 && 126 >= r): 28 | 29 | if i > 0 && line[i-1] != r { 30 | split = append(split, "") 31 | } 32 | split[len(split)-1] += string(r) 33 | punc = true 34 | 35 | case r == ' ' || r == '\t': 36 | split[len(split)-1] += string(r) 37 | punc = true 38 | 39 | default: 40 | if punc { 41 | split = append(split, "") 42 | } 43 | split[len(split)-1] += string(r) 44 | punc = false 45 | } 46 | 47 | if i == linePos { 48 | index = len(split) - 1 49 | pos = len(split[index]) - 1 50 | } 51 | } 52 | 53 | return split, index, pos - adjust 54 | } 55 | 56 | func tokeniseSplitSpaces(line []rune, linePos int) ([]string, int, int) { 57 | if len(line) == 0 { 58 | return nil, 0, 0 59 | } 60 | 61 | var adjust int 62 | if linePos >= len(line) { 63 | adjust = linePos - len(line) - 1 64 | linePos = len(line) - 1 65 | } 66 | 67 | var index, pos int 68 | split := make([]string, 1) 69 | 70 | for i, r := range line { 71 | switch { 72 | case r == ' ' || r == '\t': 73 | split[len(split)-1] += string(r) 74 | 75 | default: 76 | if i > 0 && (line[i-1] == ' ' || line[i-1] == '\t') { 77 | split = append(split, "") 78 | } 79 | split[len(split)-1] += string(r) 80 | } 81 | 82 | if i == linePos { 83 | index = len(split) - 1 84 | pos = len(split[index]) - 1 85 | } 86 | } 87 | 88 | return split, index, pos - adjust 89 | } 90 | 91 | func tokeniseBrackets(line []rune, linePos int) ([]string, int, int) { 92 | var ( 93 | open, close rune 94 | split []string 95 | count int 96 | pos = make(map[int]int) 97 | match int 98 | single, double bool 99 | ) 100 | 101 | switch line[linePos] { 102 | case '(', ')': 103 | open = '(' 104 | close = ')' 105 | 106 | case '{', '[': 107 | open = line[linePos] 108 | close = line[linePos] + 2 109 | 110 | case '}', ']': 111 | open = line[linePos] - 2 112 | close = line[linePos] 113 | 114 | default: 115 | return nil, 0, 0 116 | } 117 | 118 | for i := range line { 119 | switch line[i] { 120 | case '\'': 121 | if !single { 122 | double = !double 123 | } 124 | 125 | case '"': 126 | if !double { 127 | single = !single 128 | } 129 | 130 | case open: 131 | if !single && !double { 132 | count++ 133 | pos[count] = i 134 | if i == linePos { 135 | match = count 136 | split = []string{string(line[:i-1])} 137 | } 138 | 139 | } else if i == linePos { 140 | return nil, 0, 0 141 | } 142 | 143 | case close: 144 | if !single && !double { 145 | if match == count { 146 | split = append(split, string(line[pos[count]:i])) 147 | return split, 1, 0 148 | } 149 | if i == linePos { 150 | split = []string{ 151 | string(line[:pos[count]-1]), 152 | string(line[pos[count]:i]), 153 | } 154 | return split, 1, len(split[1]) 155 | } 156 | count-- 157 | 158 | } else if i == linePos { 159 | return nil, 0, 0 160 | } 161 | } 162 | } 163 | 164 | return nil, 0, 0 165 | } 166 | 167 | func rTrimWhiteSpace(oldString string) (newString string) { 168 | return strings.TrimRight(oldString, " ") 169 | // TODO: support tab chars 170 | /* 171 | newString = oldString 172 | for len(oldString) > 0 { 173 | if newString[len(newString)-1] == ' ' || newString[len(newString)-1] == '\t' { 174 | newString = newString[:len(newString)-1] 175 | } else { 176 | break 177 | } 178 | } 179 | return*/ 180 | } 181 | -------------------------------------------------------------------------------- /undo.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func (rl *Instance) undoAppendHistory() { 8 | if rl.viUndoSkipAppend { 9 | rl.viUndoSkipAppend = false 10 | return 11 | } 12 | 13 | rl.viUndoHistory = append(rl.viUndoHistory, rl.line.Duplicate()) 14 | } 15 | 16 | func (rl *Instance) undoLastStr() string { 17 | var undo *UnicodeT 18 | for { 19 | if len(rl.viUndoHistory) == 0 { 20 | return "" 21 | } 22 | undo = rl.viUndoHistory[len(rl.viUndoHistory)-1] 23 | rl.viUndoHistory = rl.viUndoHistory[:len(rl.viUndoHistory)-1] 24 | if undo.String() != rl.line.String() { 25 | break 26 | } 27 | } 28 | 29 | output := rl.clearHelpersStr() 30 | 31 | output += moveCursorBackwardsStr(rl.line.CellPos()) 32 | output += strings.Repeat(" ", rl.line.CellLen()) 33 | output += moveCursorBackwardsStr(rl.line.CellLen()) 34 | output += moveCursorForwardsStr(undo.CellPos()) 35 | 36 | rl.line = undo.Duplicate() 37 | 38 | output += rl.echoStr() 39 | 40 | // TODO: check me 41 | if rl.modeViMode != vimInsert && rl.line.RuneLen() > 0 && rl.line.RunePos() == rl.line.RuneLen() { 42 | rl.line.SetRunePos(rl.line.RuneLen() - 1) 43 | output += moveCursorBackwardsStr(1) 44 | } 45 | 46 | return output 47 | } 48 | -------------------------------------------------------------------------------- /unicode.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "github.com/mattn/go-runewidth" 4 | 5 | type UnicodeT struct { 6 | rl *Instance 7 | value []rune 8 | rPos int 9 | cPos int 10 | } 11 | 12 | func (u *UnicodeT) Set(rl *Instance, r []rune) { 13 | u.rl = rl 14 | u.value = r 15 | u.cPos = u.cellPos() 16 | } 17 | 18 | func (u *UnicodeT) Runes() []rune { 19 | return u.value 20 | } 21 | 22 | func (u *UnicodeT) String() string { 23 | return string(u.value) 24 | } 25 | 26 | func (u *UnicodeT) RuneLen() int { 27 | return len(u.value) 28 | } 29 | 30 | func (u *UnicodeT) RunePos() int { 31 | return u.rPos 32 | } 33 | 34 | func (u *UnicodeT) _offByOne(i int) int { 35 | if len(u.value) == 0 { 36 | return 0 37 | } 38 | if i == len(u.value) && (u.rl == nil || u.rl.modeViMode != vimInsert) { 39 | i = len(u.value) 40 | } 41 | return i 42 | } 43 | 44 | func (u *UnicodeT) SetRunePos(i int) { 45 | if i < 0 { 46 | i = 0 47 | } 48 | if i > len(u.value) { 49 | i = len(u.value) 50 | } 51 | 52 | u.rPos = u._offByOne(i) 53 | u.cPos = u.cellPos() 54 | } 55 | 56 | func (u *UnicodeT) Duplicate() *UnicodeT { 57 | dup := new(UnicodeT) 58 | dup.value = make([]rune, len(u.value)) 59 | copy(dup.value, u.value) 60 | dup.rPos = u.rPos 61 | dup.cPos = u.cPos 62 | return dup 63 | } 64 | 65 | func (u *UnicodeT) CellLen() int { 66 | return runewidth.StringWidth(u.String()) 67 | } 68 | 69 | func (u *UnicodeT) cellPos() int { 70 | var cPos, i, last int 71 | for ; i < len(u.value) && i < u.rPos; i++ { 72 | w := runewidth.RuneWidth(u.value[i]) 73 | cPos += w 74 | last = w 75 | } 76 | if last == 2 { 77 | cPos-- 78 | } 79 | 80 | return cPos 81 | } 82 | 83 | func (u *UnicodeT) CellPos() int { 84 | return u.cPos 85 | } 86 | 87 | func (u *UnicodeT) SetCellPos(cPos int) { 88 | u._setCellPos(cPos) 89 | i := u._offByOne(u.rPos) 90 | if i != u.rPos { 91 | u.rPos-- 92 | u.cPos -= runewidth.RuneWidth(u.value[u.rPos]) 93 | } 94 | } 95 | 96 | func (u *UnicodeT) _setCellPos(cPos int) { 97 | if len(u.value) == 0 { 98 | return 99 | } 100 | 101 | u.cPos = 0 102 | var last int 103 | for u.rPos = 0; u.rPos < len(u.value); u.rPos++ { 104 | if u.cPos >= cPos { 105 | if last == 2 { 106 | u.cPos-- 107 | } 108 | return 109 | } 110 | w := runewidth.RuneWidth(u.value[u.rPos]) 111 | u.cPos += w 112 | last = w 113 | } 114 | 115 | if last == 2 { 116 | u.cPos-- 117 | } 118 | u.rPos = len(u.value) 119 | if u.rPos < 0 { 120 | u.rPos = 0 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /unicode_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetRunePos(t *testing.T) { 8 | tests := []struct { 9 | Value string 10 | Start int 11 | RunePos int 12 | ExpectedRPos int 13 | ExpectedCPos int 14 | }{ 15 | { 16 | Value: "hello world!", 17 | Start: 5, 18 | RunePos: 7, 19 | ExpectedRPos: 7, 20 | ExpectedCPos: 7, 21 | }, 22 | { 23 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 24 | Start: 2, 25 | RunePos: 13, 26 | ExpectedRPos: 13, 27 | ExpectedCPos: 25, 28 | }, 29 | { 30 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 31 | Start: 2, 32 | RunePos: 14, 33 | ExpectedRPos: 14, 34 | ExpectedCPos: 27, 35 | }, 36 | { 37 | Value: "foo举手之劳就可以使办公室更加环保,比如,使用再生纸。", 38 | Start: 2, 39 | RunePos: 6, 40 | ExpectedRPos: 6, 41 | ExpectedCPos: 8, 42 | }, 43 | { 44 | Value: "foo举手之劳就可以使办公室更加环保,比如,使用再生纸。", 45 | Start: 2, 46 | RunePos: 7, 47 | ExpectedRPos: 7, 48 | ExpectedCPos: 10, 49 | }, 50 | { 51 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 52 | Start: 2, 53 | RunePos: 3, 54 | ExpectedRPos: 3, 55 | ExpectedCPos: 5, 56 | }, 57 | { 58 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 59 | Start: 2, 60 | RunePos: 4, 61 | ExpectedRPos: 4, 62 | ExpectedCPos: 7, 63 | }, 64 | } 65 | 66 | for i, test := range tests { 67 | u := new(UnicodeT) 68 | u.Set(new(Instance), []rune(test.Value)) 69 | u.SetRunePos(test.Start) 70 | u.SetRunePos(test.RunePos) 71 | rPos := u.RunePos() 72 | cPos := u.CellPos() 73 | 74 | if rPos != test.ExpectedRPos || cPos != test.ExpectedCPos { 75 | t.Errorf("Unexpected position in test %d", i) 76 | t.Logf(" Value: '%s'", test.Value) 77 | t.Logf(" Start: %d", test.Start) 78 | t.Logf(" SetRunePos: %d", test.RunePos) 79 | t.Logf(" exp rPos: %d", test.ExpectedRPos) 80 | t.Logf(" act rPos: %d", rPos) 81 | t.Logf(" exp cPos: %d", test.ExpectedCPos) 82 | t.Logf(" act cPos: %d", cPos) 83 | } 84 | } 85 | } 86 | 87 | func TestSetCellPos(t *testing.T) { 88 | tests := []struct { 89 | Value string 90 | Start int 91 | CellPos int 92 | ExpectedRPos int 93 | ExpectedCPos int 94 | }{ 95 | { 96 | Value: "hello world!", 97 | Start: 5, 98 | CellPos: 7, 99 | ExpectedRPos: 7, 100 | ExpectedCPos: 7, 101 | }, 102 | { 103 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 104 | Start: 2, 105 | CellPos: 25, 106 | ExpectedRPos: 13, 107 | ExpectedCPos: 25, 108 | }, 109 | { 110 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 111 | Start: 2, 112 | CellPos: 26, 113 | ExpectedRPos: 13, 114 | ExpectedCPos: 25, 115 | }, 116 | { 117 | Value: "foo举手之劳就可以使办公室更加环保,比如,使用再生纸。", 118 | Start: 2, 119 | CellPos: 8, 120 | ExpectedRPos: 6, 121 | ExpectedCPos: 8, 122 | }, 123 | { 124 | Value: "foo举手之劳就可以使办公室更加环保,比如,使用再生纸。", 125 | Start: 2, 126 | CellPos: 9, 127 | ExpectedRPos: 6, 128 | ExpectedCPos: 8, 129 | }, 130 | { 131 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 132 | Start: 2, 133 | CellPos: 5, 134 | ExpectedRPos: 3, 135 | ExpectedCPos: 5, 136 | }, 137 | { 138 | Value: "举手之劳就可以使办公室更加环保,比如,使用再生纸。", 139 | Start: 2, 140 | CellPos: 6, 141 | ExpectedRPos: 3, 142 | ExpectedCPos: 5, 143 | }, 144 | } 145 | 146 | for i, test := range tests { 147 | u := new(UnicodeT) 148 | u.Set(new(Instance), []rune(test.Value)) 149 | u.SetRunePos(test.Start) 150 | u.SetCellPos(test.CellPos) 151 | rPos := u.RunePos() 152 | cPos := u.CellPos() 153 | 154 | if rPos != test.ExpectedRPos || cPos != test.ExpectedCPos { 155 | t.Errorf("Unexpected position in test %d", i) 156 | t.Logf(" Value: '%s'", test.Value) 157 | t.Logf(" Start: %d", test.Start) 158 | t.Logf(" SetCellPos: %d", test.CellPos) 159 | t.Logf(" exp rPos: %d", test.ExpectedRPos) 160 | t.Logf(" act rPos: %d", rPos) 161 | t.Logf(" exp cPos: %d", test.ExpectedCPos) 162 | t.Logf(" act cPos: %d", cPos) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | func (rl *Instance) insertStr(r []rune) string { 4 | for { 5 | // I don't really understand why `0` is creeping in at the end of the 6 | // array but it only happens with unicode characters. Also have a similar 7 | // annoyance with \r when copy/pasting from iTerm. 8 | if len(r) > 1 && (r[len(r)-1] == 0 || r[len(r)-1] == '\r') { 9 | r = r[:len(r)-1] 10 | continue 11 | } 12 | break 13 | } 14 | 15 | switch { 16 | case rl.line.RuneLen() == 0: 17 | rl.line.Set(rl, r) 18 | 19 | case rl.line.RunePos() == 0: 20 | rl.line.Set(rl, append(r, rl.line.Runes()...)) 21 | 22 | case rl.line.RunePos() < rl.line.RuneLen(): 23 | value := rl.line.Runes() 24 | new := append(r, value[rl.line.RunePos():]...) 25 | new = append(value[:rl.line.RunePos()], new...) 26 | rl.line.Set(rl, new) 27 | 28 | default: 29 | rl.line.Set(rl, append(rl.line.Runes(), r...)) 30 | } 31 | 32 | output := rl.moveCursorByRuneAdjustStr(len(r)) 33 | output += rl.echoStr() 34 | 35 | // TODO: check me 36 | if rl.modeViMode == vimInsert { 37 | output += rl.updateHelpersStr() 38 | } 39 | 40 | return output 41 | } 42 | 43 | func (rl *Instance) backspaceStr() string { 44 | if rl.line.RuneLen() == 0 || rl.line.RunePos() == 0 { 45 | return "" 46 | } 47 | 48 | rl.line.SetRunePos(rl.line.RunePos() - 1) 49 | return rl.deleteStr() 50 | } 51 | 52 | func (rl *Instance) deleteStr() string { 53 | var output string 54 | switch { 55 | case rl.line.RuneLen() == 0: 56 | return "" 57 | 58 | case rl.line.RunePos() == 0: 59 | rl.line.Set(rl, rl.line.Runes()[1:]) 60 | output = rl.echoStr() 61 | 62 | case rl.line.RunePos() > rl.line.RuneLen(): 63 | output = rl.backspaceStr() 64 | return output 65 | 66 | case rl.line.RunePos() == rl.line.RuneLen(): 67 | rl.line.Set(rl, rl.line.Runes()[:rl.line.RunePos()]) 68 | output = rl.echoStr() 69 | 70 | default: 71 | rl.line.Set(rl, append(rl.line.Runes()[:rl.line.RunePos()], rl.line.Runes()[rl.line.RunePos()+1:]...)) 72 | output = rl.echoStr() 73 | } 74 | 75 | output += rl.updateHelpersStr() 76 | return output 77 | } 78 | -------------------------------------------------------------------------------- /vim.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/mattn/go-runewidth" 8 | ) 9 | 10 | type viMode int 11 | 12 | const ( 13 | vimInsert viMode = iota 14 | vimReplaceOnce 15 | vimReplaceMany 16 | vimDelete 17 | vimKeys 18 | vimCommand 19 | ) 20 | 21 | func (rl *Instance) vi(r rune) string { 22 | // This would normally be a massive anti-pattern. But in this instance 23 | // any edge case exceptions are better off ignored and the interactive 24 | // prompt kept alive. The worst case scenario is the cursor might 25 | // become misaligned but that would resolve itself very quickly under 26 | // normal operation. 27 | defer recover() 28 | 29 | var output string 30 | switch r { 31 | case ':': 32 | rl.vimCommandModeInit() 33 | 34 | case 'a': 35 | if rl.line.CellLen() > 0 { 36 | rl.line.SetRunePos(rl.line.RunePos() + 1) 37 | } 38 | rl.modeViMode = vimInsert 39 | rl.viIteration = "" 40 | rl.viUndoSkipAppend = true 41 | 42 | case 'A': 43 | if rl.line.RuneLen() > 0 { 44 | rl.line.SetRunePos(rl.line.RuneLen() + 1) 45 | } 46 | rl.modeViMode = vimInsert 47 | rl.viIteration = "" 48 | rl.viUndoSkipAppend = true 49 | 50 | case 'b': 51 | rl.viUndoSkipAppend = true 52 | vii := rl.getViIterations() 53 | for i := 1; i <= vii; i++ { 54 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpB(tokeniseLine)) 55 | } 56 | 57 | case 'B': 58 | rl.viUndoSkipAppend = true 59 | vii := rl.getViIterations() 60 | for i := 1; i <= vii; i++ { 61 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpB(tokeniseSplitSpaces)) 62 | } 63 | 64 | case 'd': 65 | rl.modeViMode = vimDelete 66 | rl.viUndoSkipAppend = true 67 | 68 | case 'D': 69 | output = moveCursorBackwardsStr(rl.line.CellPos()) 70 | output += strings.Repeat(" ", rl.line.CellLen()) 71 | 72 | output += moveCursorBackwardsStr(rl.line.CellLen() - rl.line.CellPos()) 73 | rl.line.Set(rl, rl.line.Runes()[:rl.line.RunePos()]) 74 | output += rl.echoStr() 75 | 76 | r := rl.line.Runes()[rl.line.RuneLen()-1] 77 | output += moveCursorBackwardsStr(1 + runewidth.RuneWidth(r)) 78 | rl.line.SetRunePos(rl.line.RunePos() - 1) 79 | rl.viIteration = "" 80 | 81 | case 'e': 82 | rl.viUndoSkipAppend = true 83 | vii := rl.getViIterations() 84 | for i := 1; i <= vii; i++ { 85 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpE(tokeniseLine)) 86 | } 87 | 88 | case 'E': 89 | rl.viUndoSkipAppend = true 90 | vii := rl.getViIterations() 91 | for i := 1; i <= vii; i++ { 92 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpE(tokeniseSplitSpaces)) 93 | } 94 | 95 | case 'h': 96 | if rl.line.RunePos() > 0 { 97 | r := rl.line.Runes()[rl.line.RunePos()-1] 98 | output += moveCursorBackwardsStr(runewidth.RuneWidth(r)) 99 | rl.line.SetRunePos(rl.line.RunePos() - 1) 100 | } 101 | rl.viUndoSkipAppend = true 102 | 103 | case 'i': 104 | rl.modeViMode = vimInsert 105 | rl.viIteration = "" 106 | rl.viUndoSkipAppend = true 107 | 108 | case 'I': 109 | rl.modeViMode = vimInsert 110 | rl.viIteration = "" 111 | rl.viUndoSkipAppend = true 112 | output += moveCursorBackwardsStr(rl.line.CellPos()) 113 | rl.line.SetRunePos(0) 114 | 115 | case 'l': 116 | // TODO: test me 117 | if (rl.modeViMode == vimInsert && rl.line.RunePos() < rl.line.RuneLen()) || 118 | (rl.modeViMode != vimInsert && rl.line.RunePos() < rl.line.RuneLen()-1) { 119 | r := rl.line.Runes()[rl.line.RunePos()+1] 120 | output += moveCursorForwardsStr(runewidth.RuneWidth(r)) 121 | rl.line.SetRunePos(rl.line.RunePos() + 1) 122 | } 123 | rl.viUndoSkipAppend = true 124 | 125 | case 'p': 126 | // paste after 127 | if len(rl.line.Runes()) == 0 { 128 | return "" 129 | } 130 | 131 | rl.viUndoSkipAppend = true 132 | w := runewidth.RuneWidth(rl.line.Runes()[rl.line.RunePos()]) 133 | 134 | rl.line.SetRunePos(rl.line.RunePos() + 1) 135 | output += moveCursorForwardsStr(w) 136 | 137 | vii := rl.getViIterations() 138 | for i := 1; i <= vii; i++ { 139 | output += rl.insertStr([]rune(rl.viYankBuffer)) 140 | } 141 | 142 | rl.line.SetRunePos(rl.line.RunePos() - 1) 143 | output += moveCursorBackwardsStr(w) 144 | 145 | case 'P': 146 | // paste before 147 | rl.viUndoSkipAppend = true 148 | vii := rl.getViIterations() 149 | for i := 1; i <= vii; i++ { 150 | output += rl.insertStr([]rune(rl.viYankBuffer)) 151 | } 152 | 153 | case 'r': 154 | rl.modeViMode = vimReplaceOnce 155 | rl.viIteration = "" 156 | rl.viUndoSkipAppend = true 157 | 158 | case 'R': 159 | rl.modeViMode = vimReplaceMany 160 | rl.viIteration = "" 161 | rl.viUndoSkipAppend = true 162 | 163 | case 'u': 164 | output = rl.undoLastStr() 165 | rl.viUndoSkipAppend = true 166 | 167 | case 'v': 168 | output = rl.clearHelpersStr() 169 | var multiline []rune 170 | if rl.GetMultiLine == nil { 171 | multiline = rl.line.Runes() 172 | } else { 173 | multiline = rl.GetMultiLine(rl.line.Runes()) 174 | } 175 | 176 | new, err := rl.launchEditor(multiline) 177 | if err != nil || len(new) == 0 || string(new) == string(multiline) { 178 | rl.viUndoSkipAppend = true 179 | return "" 180 | } 181 | rl.clearPrompt() 182 | rl.multiline = []byte(string(new)) 183 | 184 | case 'w': 185 | rl.viUndoSkipAppend = true 186 | vii := rl.getViIterations() 187 | for i := 1; i <= vii; i++ { 188 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpW(tokeniseLine)) 189 | } 190 | 191 | case 'W': 192 | rl.viUndoSkipAppend = true 193 | vii := rl.getViIterations() 194 | for i := 1; i <= vii; i++ { 195 | output += rl.moveCursorByRuneAdjustStr(rl.viJumpW(tokeniseSplitSpaces)) 196 | } 197 | 198 | case 'x': 199 | vii := rl.getViIterations() 200 | for i := 1; i <= vii; i++ { 201 | output += rl.deleteStr() 202 | } 203 | if rl.line.RunePos() == rl.line.RuneLen() && rl.line.RuneLen() > 0 { 204 | ///// TODO !!!!!!!!!! 205 | r := rl.line.Runes()[rl.line.RunePos()-1] 206 | output += moveCursorBackwardsStr(runewidth.RuneWidth(r)) 207 | rl.line.SetRunePos(rl.line.RunePos() - 1) 208 | } 209 | 210 | case 'y', 'Y': 211 | rl.viYankBuffer = rl.line.String() 212 | rl.viUndoSkipAppend = true 213 | //rl.hintText = []rune("-- LINE YANKED --") 214 | //rl.renderHelpers() 215 | 216 | case '[': 217 | rl.viUndoSkipAppend = true 218 | output = rl.moveCursorByRuneAdjustStr(rl.viJumpPreviousBrace()) 219 | 220 | case ']': 221 | rl.viUndoSkipAppend = true 222 | output = rl.moveCursorByRuneAdjustStr(rl.viJumpNextBrace()) 223 | 224 | case '$': 225 | output = moveCursorForwardsStr(rl.line.CellLen() - rl.line.CellPos()) 226 | rl.line.SetRunePos(rl.line.RuneLen()) 227 | rl.viUndoSkipAppend = true 228 | 229 | case '%': 230 | rl.viUndoSkipAppend = true 231 | output = rl.moveCursorByRuneAdjustStr(rl.viJumpBracket()) 232 | 233 | default: 234 | if r <= '9' && '0' <= r { 235 | rl.viIteration += string(r) 236 | } 237 | rl.viUndoSkipAppend = true 238 | 239 | } 240 | 241 | return output 242 | } 243 | 244 | func (rl *Instance) getViIterations() int { 245 | i, _ := strconv.Atoi(rl.viIteration) 246 | if i < 1 { 247 | i = 1 248 | } 249 | rl.viIteration = "" 250 | return i 251 | } 252 | 253 | func (rl *Instance) viHintMessageStr() string { 254 | switch rl.modeViMode { 255 | case vimKeys: 256 | rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)") 257 | case vimInsert: 258 | rl.hintText = []rune("-- INSERT --") 259 | case vimReplaceOnce: 260 | rl.hintText = []rune("-- REPLACE CHARACTER --") 261 | case vimReplaceMany: 262 | rl.hintText = []rune("-- REPLACE --") 263 | case vimDelete: 264 | rl.hintText = []rune("-- DELETE --") 265 | case vimCommand: 266 | rl.hintText = rl.vimCommandModeHintText() 267 | default: 268 | rl.getHintText() 269 | } 270 | 271 | output := rl.clearHelpersStr() 272 | output += rl.renderHelpersStr() 273 | return output 274 | } 275 | 276 | func (rl *Instance) viJumpB(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) { 277 | split, index, pos := tokeniser(rl.line.Runes(), rl.line.RunePos()) 278 | switch { 279 | case len(split) == 0: 280 | return 281 | case index == 0 && pos == 0: 282 | return 283 | case pos == 0: 284 | adjust = len(split[index-1]) 285 | default: 286 | adjust = pos 287 | } 288 | return adjust * -1 289 | } 290 | 291 | func (rl *Instance) viJumpE(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) { 292 | split, index, pos := tokeniser(rl.line.Runes(), rl.line.RunePos()) 293 | if len(split) == 0 { 294 | return 295 | } 296 | 297 | word := rTrimWhiteSpace(split[index]) 298 | 299 | switch { 300 | case len(split) == 0: 301 | return 302 | case index == len(split)-1 && pos >= len(word)-1: 303 | return 304 | case pos >= len(word)-1: 305 | word = rTrimWhiteSpace(split[index+1]) 306 | adjust = len(split[index]) - pos 307 | adjust += len(word) - 1 308 | default: 309 | adjust = len(word) - pos - 1 310 | } 311 | return 312 | } 313 | 314 | func (rl *Instance) viJumpW(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) { 315 | split, index, pos := tokeniser(rl.line.Runes(), rl.line.RunePos()) 316 | switch { 317 | case len(split) == 0: 318 | return 319 | case index+1 == len(split): 320 | adjust = rl.line.RuneLen() - 1 - rl.line.RunePos() 321 | default: 322 | adjust = len(split[index]) - pos 323 | } 324 | return 325 | } 326 | 327 | func (rl *Instance) viJumpPreviousBrace() (adjust int) { 328 | if rl.line.RunePos() == 0 { 329 | return 0 330 | } 331 | 332 | for i := rl.line.RunePos() - 1; i != 0; i-- { 333 | if rl.line.Runes()[i] == '{' { 334 | return i - rl.line.RunePos() 335 | } 336 | } 337 | 338 | return 0 339 | } 340 | 341 | func (rl *Instance) viJumpNextBrace() (adjust int) { 342 | if rl.line.RunePos() >= rl.line.RuneLen()-1 { 343 | return 0 344 | } 345 | 346 | for i := rl.line.RunePos() + 1; i < rl.line.RuneLen(); i++ { 347 | if rl.line.Runes()[i] == '{' { 348 | return i - rl.line.RunePos() 349 | } 350 | } 351 | 352 | return 0 353 | } 354 | 355 | func (rl *Instance) viJumpBracket() (adjust int) { 356 | split, index, pos := tokeniseBrackets(rl.line.Runes(), rl.line.RunePos()) 357 | switch { 358 | case len(split) == 0: 359 | return 360 | case pos == 0: 361 | adjust = len(split[index]) 362 | default: 363 | adjust = pos * -1 364 | } 365 | return 366 | } 367 | -------------------------------------------------------------------------------- /vim_command.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | rxHistIndex = regexp.MustCompile(`^[0-9]+$`) 11 | //rxHistRegex = regexp.MustCompile(`\^m/(.*?[^\\])/`) // Scratchpad: https://play.golang.org/p/Iya2Hx1uxb 12 | //rxHistPrefix = regexp.MustCompile(`(\^\^[a-zA-Z]+)`) 13 | //rxHistTag = regexp.MustCompile(`(\^#[-_a-zA-Z0-9]+)`) 14 | //rxHistParam = regexp.MustCompile(`\^\[([-]?[0-9]+)]`) 15 | //rxHistReplace = regexp.MustCompile(`\^s/(.*?[^\\])/(.*?[^\\])/`) 16 | ) 17 | 18 | func (rl *Instance) vimCommandModeInit() { 19 | rl.modeViMode = vimCommand 20 | rl.viUndoSkipAppend = true 21 | rl.getTabCompletion() 22 | } 23 | 24 | func (rl *Instance) vimCommandModeInput(input []rune) string { 25 | for _, r := range input { 26 | switch r { 27 | case '\r', '\n': 28 | continue 29 | case '\t': 30 | rl.viCommandLine = append(rl.viCommandLine, ' ') 31 | default: 32 | rl.viCommandLine = append(rl.viCommandLine, r) 33 | } 34 | } 35 | 36 | rl.getTabCompletion() 37 | return "" //output 38 | } 39 | 40 | func (rl *Instance) vimCommandModeSuggestions() *TabCompleterReturnT { 41 | tcr := &TabCompleterReturnT{ 42 | DisplayType: TabDisplayList, 43 | } 44 | 45 | s := string(rl.viCommandLine) 46 | 47 | switch { 48 | case s == "": 49 | fallthrough 50 | default: 51 | tcr.Suggestions = []string{ 52 | "Valid commands:", 53 | " !!", 54 | " n", 55 | " m/find/", 56 | " s/find/replace/", 57 | } 58 | tcr.Descriptions = map[string]string{ 59 | "Valid commands:": "", 60 | " !!": "Previous command line", 61 | " n": "history item n (where 'n' is an integer)", 62 | " m/find/": "match string (regexp)", 63 | " s/find/replace/": "substitute in current line (regexp)", 64 | } 65 | tcr.Prefix = " " 66 | 67 | case s == "!!": 68 | last := rl.History.Len() - 1 69 | line, err := rl.History.GetLine(last) 70 | if err != nil { 71 | tcr.Suggestions = []string{err.Error()} 72 | tcr.Prefix = " " 73 | return tcr 74 | } 75 | n := strconv.Itoa(last) 76 | tcr.Suggestions = []string{n} 77 | tcr.Descriptions = map[string]string{n: line} 78 | 79 | case rxHistIndex.MatchString(s): 80 | tcr.Descriptions = make(map[string]string) 81 | for i := rl.History.Len() - 1; i >= 0; i-- { 82 | line, err := rl.History.GetLine(i) 83 | if err != nil { 84 | tcr.Suggestions = []string{err.Error()} 85 | tcr.Prefix = " " 86 | return tcr 87 | } 88 | n := strconv.Itoa(i) 89 | if strings.Contains(n, s) { 90 | tcr.Suggestions = append(tcr.Suggestions, n) 91 | tcr.Descriptions[n] = line 92 | } 93 | } 94 | 95 | case s[0] == 'm': 96 | if len(s) == 1 { 97 | tcr.Suggestions = []string{"Usage: m/find/", " (separator can be any 1-byte wide character)"} 98 | tcr.Prefix = " " 99 | return tcr 100 | } 101 | split := strings.Split(s, string(s[1])) 102 | rx, err := regexp.Compile(split[1]) 103 | if err != nil { 104 | tcr.Suggestions = []string{err.Error()} 105 | tcr.Prefix = " " 106 | return tcr 107 | } 108 | tcr.Descriptions = make(map[string]string) 109 | for i := rl.History.Len() - 1; i >= 0; i-- { 110 | line, err := rl.History.GetLine(i) 111 | if err != nil { 112 | tcr.Suggestions = []string{err.Error()} 113 | tcr.Prefix = " " 114 | return tcr 115 | } 116 | if rx.MatchString(line) { 117 | n := strconv.Itoa(i) 118 | tcr.Suggestions = append(tcr.Suggestions, n) 119 | tcr.Descriptions[n] = line 120 | } 121 | } 122 | 123 | case s[0] == 's': 124 | if len(s) == 1 { 125 | tcr.Suggestions = []string{"Usage: s/find/replace/", " (separator can be any 1-byte wide character)"} 126 | tcr.Prefix = " " 127 | return tcr 128 | } 129 | split := strings.Split(s, string(s[1])) 130 | if len(split) < 3 { 131 | tcr.Suggestions = []string{"Usage: s/find/replace/", " (separator can be any 1-byte wide character)"} 132 | tcr.Prefix = " " 133 | return tcr 134 | } 135 | rx, err := regexp.Compile(split[1]) 136 | if err != nil { 137 | tcr.Suggestions = []string{err.Error()} 138 | tcr.Prefix = " " 139 | return tcr 140 | } 141 | substitution := rx.ReplaceAllString(rl.line.String(), split[2]) 142 | tcr.Suggestions = []string{substitution} 143 | } 144 | 145 | return tcr 146 | } 147 | 148 | func (rl *Instance) vimCommandModeReturnStr() string { 149 | var output string 150 | 151 | if len(rl.viCommandLine) == 0 || len(rl.tcSuggestions) == 0 || rl.tcr.Prefix == " " { 152 | return "" 153 | } 154 | 155 | cell := (rl.tcMaxX * (rl.tcPosY - 1)) + rl.tcOffset + rl.tcPosX - 1 156 | v := rl.tcr.Suggestions[cell] 157 | 158 | switch rl.viCommandLine[0] { 159 | case 's': 160 | rl.line.Set(rl, []rune(v)) 161 | rl.line.SetRunePos(len(v)) 162 | 163 | default: 164 | _, err := strconv.Atoi(v) 165 | if err != nil { 166 | output += rl.insertStr([]rune(v)) 167 | } else { 168 | output += rl.insertStr([]rune(rl.tcr.Descriptions[v])) 169 | } 170 | } 171 | 172 | rl.modeViMode = vimInsert 173 | rl.viCommandLine = nil 174 | 175 | rl.resetHelpers() 176 | output += rl.clearHelpersStr() 177 | //rl.resetTabCompletion() 178 | output += rl.renderHelpersStr() 179 | return output 180 | } 181 | 182 | func (rl *Instance) vimCommandModeHintText() []rune { 183 | r := append([]rune("VIM command mode: "), rl.viCommandLine...) 184 | r = append(r, []rune(seqBlink)...) 185 | r = append(r, '_') 186 | return r 187 | } 188 | 189 | func (rl *Instance) vimCommandModeBackspaceStr() string { 190 | if len(rl.viCommandLine) == 0 { 191 | return "\007" // bell 192 | } 193 | 194 | rl.viCommandLine = rl.viCommandLine[:len(rl.viCommandLine)-1] 195 | rl.hintText = rl.vimCommandModeHintText() 196 | return rl.renderHelpersStr() 197 | } 198 | 199 | func (rl *Instance) vimCommandModeCancel() { 200 | rl.modeViMode = vimInsert 201 | rl.viCommandLine = nil 202 | } 203 | -------------------------------------------------------------------------------- /vim_delete.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import "strings" 4 | 5 | func (rl *Instance) vimDeleteStr(r []rune) string { 6 | // TODO: test me 7 | //defer func() { rl.modeViMode = vimKeys }() 8 | 9 | var output string 10 | switch r[0] { 11 | case 'b': 12 | output = rl.viDeleteByAdjustStr(rl.viJumpB(tokeniseLine)) 13 | 14 | case 'B': 15 | output = rl.viDeleteByAdjustStr(rl.viJumpB(tokeniseSplitSpaces)) 16 | 17 | case 'd': 18 | rl.clearPrompt() 19 | rl.resetHelpers() 20 | rl.getHintText() 21 | 22 | case 'e': 23 | output = rl.viDeleteByAdjustStr(rl.viJumpE(tokeniseLine) + 1) 24 | 25 | case 'E': 26 | output = rl.viDeleteByAdjustStr(rl.viJumpE(tokeniseSplitSpaces) + 1) 27 | 28 | case 'w': 29 | output = rl.viDeleteByAdjustStr(rl.viJumpW(tokeniseLine)) 30 | 31 | case 'W': 32 | output = rl.viDeleteByAdjustStr(rl.viJumpW(tokeniseSplitSpaces)) 33 | 34 | case '%': 35 | output = rl.viDeleteByAdjustStr(rl.viJumpBracket()) 36 | 37 | case 27: 38 | if len(r) > 1 && '1' <= r[1] && r[1] <= '9' { 39 | output = rl.vimDeleteTokenStr(r[1]) 40 | if output != "" { 41 | rl.modeViMode = vimKeys 42 | return output 43 | } 44 | } 45 | fallthrough 46 | 47 | default: 48 | rl.viUndoSkipAppend = true 49 | } 50 | 51 | rl.modeViMode = vimKeys 52 | 53 | return output 54 | } 55 | 56 | func (rl *Instance) viDeleteByAdjustStr(adjust int) string { 57 | if adjust == 0 { 58 | rl.viUndoSkipAppend = true 59 | return "" 60 | } 61 | 62 | // Separate out the cursor movement from the logic so we can run tests on 63 | // the logic 64 | newLine, backOne := rl.viDeleteByAdjustLogic(&adjust) 65 | 66 | rl.line.Set(rl, newLine) 67 | 68 | output := rl.echoStr() 69 | 70 | if adjust < 0 { 71 | output += rl.moveCursorByRuneAdjustStr(adjust) 72 | } 73 | 74 | if backOne { 75 | output += moveCursorBackwardsStr(1) 76 | rl.line.SetRunePos(rl.line.RunePos() - 1) 77 | } 78 | 79 | return output 80 | } 81 | 82 | func (rl *Instance) viDeleteByAdjustLogic(adjust *int) (newLine []rune, backOne bool) { 83 | switch { 84 | case rl.line.RuneLen() == 0: 85 | *adjust = 0 86 | newLine = []rune{} 87 | 88 | case rl.line.RunePos()+*adjust > rl.line.RuneLen()-1: 89 | *adjust -= (rl.line.RunePos() + *adjust) - (rl.line.RuneLen() - 1) 90 | fallthrough 91 | 92 | case rl.line.RunePos()+*adjust == rl.line.RuneLen()-1: 93 | newLine = rl.line.Runes()[:rl.line.RunePos()] 94 | backOne = true 95 | 96 | case rl.line.RunePos()+*adjust < 0: 97 | *adjust = rl.line.RunePos() 98 | fallthrough 99 | 100 | case rl.line.RunePos()+*adjust == 0: 101 | newLine = rl.line.Runes()[rl.line.RunePos():] 102 | 103 | case *adjust < 0: 104 | newLine = append(rl.line.Runes()[:rl.line.RunePos()+*adjust], rl.line.Runes()[rl.line.RunePos():]...) 105 | 106 | default: 107 | newLine = append(rl.line.Runes()[:rl.line.RunePos()], rl.line.Runes()[rl.line.RunePos()+*adjust:]...) 108 | } 109 | 110 | return 111 | } 112 | 113 | func (rl *Instance) vimDeleteTokenStr(r rune) string { 114 | tokens, _, _ := tokeniseSplitSpaces(rl.line.Runes(), 0) 115 | pos := int(r) - 48 // convert ASCII to integer 116 | if pos > len(tokens) { 117 | return "" 118 | } 119 | 120 | s := rl.line.String() 121 | newLine := strings.Replace(s, tokens[pos-1], "", -1) 122 | if newLine == s { 123 | return "" 124 | } 125 | 126 | output := moveCursorBackwardsStr(rl.line.CellPos()) 127 | output += strings.Repeat(" ", rl.line.CellLen()) 128 | output += moveCursorBackwardsStr(rl.line.CellLen() - rl.line.CellPos()) 129 | 130 | rl.line.Set(rl, []rune(newLine)) 131 | 132 | output += rl.echoStr() 133 | 134 | if rl.line.RunePos() > rl.line.RuneLen() { 135 | output += "\r" //moveCursorBackwardsStr(GetTermWidth()) 136 | output += moveCursorForwardsStr(rl.promptLen + rl.line.CellLen() - 1) 137 | // ^ this is lazy 138 | rl.line.SetRunePos(rl.line.RuneLen() - 1) 139 | } 140 | 141 | return output 142 | } 143 | -------------------------------------------------------------------------------- /vim_delete_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestViDeleteByAdjustLogicNoPanic just tests that viDeleteByAdjust() doesn't cause 8 | // a panic: 9 | // https://github.com/lmorg/murex/issues/341 10 | 11 | type TestViDeleteByAdjustT struct { 12 | Line string 13 | Pos int 14 | Adjust int 15 | } 16 | 17 | func TestViDeleteByAdjustLogicNoPanic(t *testing.T) { 18 | tests := []TestViDeleteByAdjustT{ 19 | { 20 | Line: "The quick brown fox", 21 | Pos: 0, 22 | Adjust: -1, 23 | }, 24 | { 25 | Line: "The quick brown fox", 26 | Pos: 1, 27 | Adjust: -1, 28 | }, 29 | { 30 | Line: "The quick brown fox", 31 | Pos: 1, 32 | Adjust: -2, 33 | }, 34 | { 35 | Line: "The quick brown fox", 36 | Pos: 2, 37 | Adjust: -2, 38 | }, 39 | { 40 | Line: "The quick brown fox", 41 | Pos: 5, 42 | Adjust: -1, 43 | }, 44 | { 45 | Line: "The quick brown fox", 46 | Pos: 5, 47 | Adjust: 1, 48 | }, 49 | { 50 | Line: "The quick brown fox", 51 | Pos: 5, 52 | Adjust: 100, 53 | }, 54 | } 55 | 56 | for _, test := range tests { 57 | rl := NewInstance() 58 | rl.line.Set(rl, []rune(test.Line)) 59 | rl.line.SetRunePos(test.Pos) 60 | rl.viDeleteByAdjustLogic(&test.Adjust) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/mattn/go-runewidth" 9 | ) 10 | 11 | func (rl *Instance) printf(format string, a ...interface{}) { 12 | s := fmt.Sprintf(format, a...) 13 | rl.print(s) 14 | } 15 | 16 | func (rl *Instance) print(s string) { 17 | if rl.isNoTty { 18 | return 19 | } 20 | 21 | _print(s) 22 | } 23 | 24 | func (rl *Instance) printErr(s string) { 25 | if rl.isNoTty { 26 | return 27 | } 28 | 29 | _printErr(s) 30 | } 31 | 32 | // var rxAnsiSgr = regexp.MustCompile("\x1b\\[[:;0-9]+m") 33 | var rxAnsiSgr = regexp.MustCompile(`\x1b\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]`) 34 | 35 | // Gets the number of runes in a string and 36 | func strLen(s string) int { 37 | s = rxAnsiSgr.ReplaceAllString(s, "") 38 | return runewidth.StringWidth(s) 39 | } 40 | 41 | func (rl *Instance) echoStr() string { 42 | if len(rl.multiSplit) == 0 { 43 | rl.syntaxCompletion() 44 | } 45 | 46 | lineX, lineY := rl.lineWrapCellLen() 47 | posX, posY := rl.lineWrapCellPos() 48 | 49 | // reset cursor to start 50 | line := "\r" 51 | if posY > 0 { 52 | line += fmt.Sprintf(cursorUpf, posY) 53 | } 54 | 55 | // clear the line 56 | line += strings.Repeat("\x1b[2K\n", lineY+1) // clear line + move cursor down 1 57 | line += fmt.Sprintf(cursorUpf, lineY+1) 58 | //line += seqClearScreenBelow 59 | 60 | promptLen := rl.promptLen 61 | if promptLen < rl.termWidth() { 62 | line += rl.prompt 63 | } else { 64 | promptLen = 0 65 | } 66 | 67 | switch { 68 | case rl.PasswordMask != 0: 69 | line += strings.Repeat(string(rl.PasswordMask), rl.line.CellLen()) 70 | 71 | case rl.line.CellLen()+promptLen > rl.termWidth(): 72 | fallthrough 73 | 74 | case rl.SyntaxHighlighter == nil: 75 | line += strings.Join(lineWrap(rl, rl.termWidth()), "\r\n") 76 | 77 | default: 78 | syntax := rl.cacheSyntax.Get(rl.line.Runes()) 79 | if len(syntax) == 0 { 80 | syntax = rl.SyntaxHighlighter(rl.line.Runes()) 81 | 82 | if rl.DelayedSyntaxWorker == nil { 83 | rl.cacheSyntax.Append(rl.line.Runes(), syntax) 84 | } 85 | } 86 | line += syntax 87 | } 88 | 89 | y := lineY - posY 90 | if y > 0 { 91 | line += fmt.Sprintf(cursorUpf, y) 92 | } 93 | x := lineX - posX + 1 94 | if x > 0 { 95 | line += fmt.Sprintf(cursorBackf, x) 96 | } 97 | //print(line) 98 | return line 99 | } 100 | 101 | func lineWrap(rl *Instance, termWidth int) []string { 102 | var promptLen int 103 | if rl.promptLen < termWidth { 104 | promptLen = rl.promptLen 105 | } 106 | 107 | var ( 108 | wrap []string 109 | wrapRunes [][]rune 110 | bufCellLen int 111 | length = termWidth - promptLen 112 | line = rl.line.Runes() //append(rl.line.Runes(), []rune{' ', ' '}...) // double space to work around wide characters 113 | lPos int 114 | ) 115 | 116 | wrapRunes = append(wrapRunes, []rune{}) 117 | 118 | for r := range line { 119 | w := runewidth.RuneWidth(line[r]) 120 | if bufCellLen+w > length { 121 | wrapRunes = append(wrapRunes, []rune(strings.Repeat(" ", promptLen))) 122 | lPos++ 123 | bufCellLen = 0 124 | } 125 | bufCellLen += w 126 | wrapRunes[lPos] = append(wrapRunes[lPos], line[r]) 127 | } 128 | 129 | wrap = make([]string, lPos+1) 130 | for i := range wrap { 131 | wrap[i] = string(wrapRunes[i]) 132 | } 133 | 134 | return wrap 135 | } 136 | 137 | func (rl *Instance) lineWrapCellLen() (x, y int) { 138 | return LineWrappedCellPos(rl.promptLen, rl.line.Runes(), rl.termWidth()) 139 | } 140 | 141 | func (rl *Instance) lineWrapCellPos() (x, y int) { 142 | return LineWrappedCellPos(rl.promptLen, rl.line.Runes()[:rl.line.RunePos()], rl.termWidth()) 143 | } 144 | 145 | // LineWrappedCellPos is a unicode and wide character aware function for 146 | // determining the x/y coordinates of a cell. 147 | func LineWrappedCellPos(promptLen int, line []rune, termWidth int) (x, y int) { 148 | if promptLen >= termWidth { 149 | promptLen = 0 150 | } 151 | 152 | // avoid divide by zero error 153 | if termWidth-promptLen == 0 { 154 | return 0, 0 155 | } 156 | 157 | x = promptLen 158 | for i := range line { 159 | w := runewidth.RuneWidth(line[i]) 160 | if x+w > termWidth { 161 | x = promptLen 162 | y++ 163 | } 164 | x += w 165 | } 166 | 167 | return 168 | } 169 | 170 | func (rl *Instance) clearPrompt() { 171 | if rl.line.RuneLen() == 0 { 172 | return 173 | } 174 | 175 | output := rl.moveCursorToStartStr() 176 | 177 | if rl.termWidth() > rl.promptLen { 178 | output += strings.Repeat(" ", rl.termWidth()-rl.promptLen) 179 | } 180 | output += seqClearScreenBelow 181 | 182 | output += moveCursorBackwardsStr(rl.termWidth()) 183 | output += rl.prompt 184 | 185 | rl.line.Set(rl, []rune{}) 186 | rl.line.SetRunePos(0) 187 | 188 | rl.print(output) 189 | } 190 | 191 | func (rl *Instance) resetHelpers() { 192 | rl.modeAutoFind = false 193 | output := rl.clearPreviewStr() 194 | output += rl.clearHelpersStr() 195 | 196 | rl.resetHintText() 197 | rl.resetTabCompletion() 198 | 199 | rl.print(output) 200 | } 201 | 202 | func (rl *Instance) clearHelpersStr() string { 203 | posX, posY := rl.lineWrapCellPos() 204 | _, lineY := rl.lineWrapCellLen() 205 | 206 | y := lineY - posY 207 | 208 | output := moveCursorDownStr(y) 209 | output += "\r\n" + seqClearScreenBelow 210 | 211 | output += moveCursorUpStr(y + 1) 212 | output += moveCursorForwardsStr(posX) 213 | 214 | return output 215 | } 216 | 217 | func (rl *Instance) renderHelpersStr() string { 218 | output := rl.writeHintTextStr() 219 | output += rl.writeTabCompletionStr() 220 | output += rl.writePreviewStr() 221 | return output 222 | } 223 | 224 | func (rl *Instance) updateHelpersStr() string { 225 | rl.tcOffset = 0 226 | rl.getHintText() 227 | if rl.modeTabCompletion { 228 | rl.getTabCompletion() 229 | } 230 | output := rl.clearHelpersStr() 231 | output += rl.renderHelpersStr() 232 | 233 | return output 234 | } 235 | -------------------------------------------------------------------------------- /write_js.go: -------------------------------------------------------------------------------- 1 | //go:build js 2 | // +build js 3 | 4 | package readline 5 | 6 | import "syscall/js" 7 | 8 | func _print(s string) { 9 | vtermWrite([]rune(s)) 10 | } 11 | 12 | func _printErr(s string) { 13 | vtermWrite([]rune(s)) 14 | } 15 | 16 | func vtermWrite(r []rune) { 17 | VTerm.Write(r) 18 | 19 | //divMutex.Lock() 20 | 21 | html := VTerm.ExportHtml() 22 | 23 | jsDoc := js.Global().Get("document") 24 | outElement := jsDoc.Call("getElementById", "term") 25 | outElement.Set("innerHTML", html) 26 | 27 | //divMutex.Unlock() 28 | } 29 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/mattn/go-runewidth" 9 | ) 10 | 11 | func LazyLogging(v any) string { 12 | b, err := json.Marshal(v) 13 | if err != nil { 14 | return err.Error() 15 | } 16 | return string(b) 17 | } 18 | 19 | func TestLineWrap(t *testing.T) { 20 | type TestLineWrapT struct { 21 | Prompt string 22 | Line string 23 | TermWidth int 24 | Expected []string 25 | } 26 | 27 | tests := []TestLineWrapT{ 28 | { 29 | Prompt: "foobar", 30 | Line: "1234567890", 31 | TermWidth: 80, 32 | Expected: []string{"1234567890"}, 33 | }, 34 | { 35 | Prompt: "foobar", 36 | Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890", 37 | TermWidth: 86, 38 | Expected: []string{"12345678901234567890123456789012345678901234567890123456789012345678901234567890"}, 39 | }, 40 | { 41 | Prompt: "foobar", 42 | Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890", 43 | TermWidth: 87, 44 | Expected: []string{"12345678901234567890123456789012345678901234567890123456789012345678901234567890"}, 45 | }, 46 | { 47 | Prompt: "foobar", 48 | Line: "123456789012345678901234567890", 49 | TermWidth: 20, 50 | Expected: []string{"12345678901234", " 56789012345678", " 90"}, 51 | }, 52 | { 53 | Prompt: "foobar", 54 | Line: "1234567890", 55 | TermWidth: 4, 56 | Expected: []string{"1234", "5678", "90"}, 57 | }, 58 | { 59 | Prompt: "foobar", 60 | Line: "1234567890", 61 | TermWidth: 5, 62 | Expected: []string{"12345", "67890"}, 63 | }, 64 | { 65 | Prompt: "foobar", 66 | Line: "1234567890", 67 | TermWidth: 6, 68 | Expected: []string{"123456", "7890"}, 69 | }, 70 | { 71 | Prompt: "foobar", 72 | Line: "1234567890", 73 | TermWidth: 7, 74 | Expected: []string{"1", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9", " 0"}, 75 | }, 76 | { 77 | Prompt: "foobar", 78 | Line: "1234567890", 79 | TermWidth: 8, 80 | Expected: []string{"12", " 34", " 56", " 78", " 90"}, 81 | }, 82 | { 83 | Prompt: "foobar", 84 | Line: "使用再生纸", 85 | TermWidth: 8, 86 | Expected: []string{"使", " 用", " 再", " 生", " 纸"}, 87 | }, 88 | { 89 | Prompt: "foobar", 90 | Line: "使用再生纸", 91 | TermWidth: 9, 92 | Expected: []string{"使", " 用", " 再", " 生", " 纸"}, 93 | }, 94 | } 95 | 96 | for i, test := range tests { 97 | rl := NewInstance() 98 | rl.SetPrompt(test.Prompt) 99 | rl.line.Set(rl, []rune(test.Line)) 100 | 101 | wrap := lineWrap(rl, test.TermWidth) 102 | if len(wrap) != len(test.Expected) { 103 | t.Error("Slice lens do not match:") 104 | t.Logf(" Test: %d (%s)", i, t.Name()) 105 | t.Logf(" Prompt: '%s'", test.Prompt) 106 | t.Logf(" Line: '%s'", test.Line) 107 | t.Logf(" Width: %d", test.TermWidth) 108 | t.Logf(" len(exp): %d", len(test.Expected)) 109 | t.Logf(" len(act): %d", len(wrap)) 110 | t.Logf(" Slice exp: '%s'", fmt.Sprint(test.Expected)) 111 | t.Logf(" Slice act: '%s'", fmt.Sprint(wrap)) 112 | t.Logf(" Slice json e: %s", LazyLogging(test.Expected)) 113 | t.Logf(" Slice json a: %s", LazyLogging(wrap)) 114 | t.Logf(" rl.promptLen: %d'", rl.promptLen) 115 | t.Logf(" rl.line: '%s'", rl.line.String()) 116 | continue 117 | } 118 | 119 | for j := range wrap { 120 | if wrap[j] != test.Expected[j] { 121 | t.Error("Slice element does not match:") 122 | t.Logf(" Test: %d (%s)", i, t.Name()) 123 | t.Logf(" Prompt: '%s'", test.Prompt) 124 | t.Logf(" Line: '%s'", test.Line) 125 | t.Logf(" Width: %d", test.TermWidth) 126 | t.Logf(" Expected: %s", test.Expected[j]) 127 | t.Logf(" Actual: %s", wrap[j]) 128 | t.Logf(" len(exp): %d", len(test.Expected)) 129 | t.Logf(" len(act): %d", len(wrap)) 130 | t.Logf(" Slice exp:'%s'", fmt.Sprint(test.Expected)) 131 | t.Logf(" Slice act:'%s'", fmt.Sprint(wrap)) 132 | t.Logf(" Slice j e: %s", LazyLogging(test.Expected)) 133 | t.Logf(" Slice j a: %s", LazyLogging(wrap)) 134 | } 135 | } 136 | } 137 | } 138 | 139 | func TestLineWrappedCellPos(t *testing.T) { 140 | type ExpectedT struct { 141 | X, Y int 142 | } 143 | 144 | type TestLineWrapPosT struct { 145 | Prompt string 146 | Line string 147 | TermWidth int 148 | Expected ExpectedT 149 | } 150 | 151 | tests := []TestLineWrapPosT{ 152 | { 153 | Prompt: "12345", 154 | Line: "", 155 | TermWidth: 10, 156 | Expected: ExpectedT{5 + 0, 0}, 157 | }, 158 | ///// 159 | { 160 | Prompt: "12345", 161 | Line: "123", 162 | TermWidth: 10, 163 | Expected: ExpectedT{5 + 3, 0}, 164 | }, 165 | { 166 | Prompt: "12345", 167 | Line: "1234", 168 | TermWidth: 10, 169 | Expected: ExpectedT{5 + 4, 0}, 170 | }, 171 | { 172 | Prompt: "12345", 173 | Line: "12345", 174 | TermWidth: 10, 175 | Expected: ExpectedT{10, 0}, 176 | }, 177 | { 178 | Prompt: "12345", 179 | Line: "123456", 180 | TermWidth: 10, 181 | Expected: ExpectedT{5 + 1, 1}, 182 | }, 183 | { 184 | Prompt: "foobar", 185 | Line: "1234567890", 186 | TermWidth: 80, 187 | Expected: ExpectedT{6 + 10, 0}, 188 | }, 189 | { 190 | Prompt: "foobar", 191 | Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890", 192 | TermWidth: 85, 193 | Expected: ExpectedT{6 + 1, 1}, 194 | }, 195 | { 196 | Prompt: "foobar", 197 | Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890", 198 | TermWidth: 86, 199 | Expected: ExpectedT{86, 0}, 200 | }, 201 | { 202 | Prompt: "foobar", 203 | Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890", 204 | TermWidth: 87, 205 | Expected: ExpectedT{86, 0}, 206 | }, 207 | { 208 | Prompt: "foobar", 209 | Line: "123456789012345678901234567890", 210 | TermWidth: 20, 211 | //Expected: []string{"12345678901234", "56789012345678", "90"}, 212 | Expected: ExpectedT{6 + 2, 2}, 213 | }, 214 | { // 10 215 | Prompt: "foobar", 216 | Line: "1234567890", 217 | TermWidth: 4, 218 | //Expected: []string{"1234", "5678", "90"}, 219 | Expected: ExpectedT{0 + 2, 2}, 220 | }, 221 | { 222 | Prompt: "foobar", 223 | Line: "1234567890", 224 | TermWidth: 5, 225 | //Expected: []string{"12345", "67890"}, 226 | Expected: ExpectedT{5, 1}, 227 | }, 228 | { 229 | Prompt: "foobar", 230 | Line: "1234567890", 231 | TermWidth: 6, 232 | //Expected: []string{"123456", "7890"}, 233 | Expected: ExpectedT{0 + 4, 1}, 234 | }, 235 | { 236 | Prompt: "foobar", 237 | Line: "1234567890", 238 | TermWidth: 7, 239 | //Expected: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, 240 | Expected: ExpectedT{7, 9}, 241 | }, 242 | { 243 | Prompt: "foobar", 244 | Line: "1234567890", 245 | TermWidth: 8, 246 | //Expected: []string{"12", "34", "56", "78", "90"}, 247 | Expected: ExpectedT{6 + 2, 4}, 248 | }, 249 | ///// 250 | { 251 | Prompt: "foobar", 252 | Line: "使用再生纸", 253 | TermWidth: 8, 254 | //Expected: []string{"12", "34", "56", "78", "90"}, 255 | Expected: ExpectedT{6 + 2, 4}, 256 | }, 257 | { 258 | Prompt: "foo", 259 | Line: "使用 再生纸", 260 | TermWidth: 8, 261 | //Expected: []string{"12", "34", "56", "78", "90"}, 262 | Expected: ExpectedT{X: 5, Y: 2}, 263 | }, 264 | { 265 | Prompt: "foo", 266 | Line: "使用 再生纸 使用 再生", 267 | TermWidth: 8, 268 | //Expected: []string{"12", "34", "56", "78", "90"}, 269 | Expected: ExpectedT{X: 5, Y: 4}, 270 | }, 271 | { 272 | Prompt: "使用", 273 | Line: "使用再生纸使用再生", 274 | TermWidth: 8, 275 | //Expected: []string{"12", "34", "56", "78", "90"}, 276 | Expected: ExpectedT{X: 6, Y: 4}, 277 | }, 278 | } 279 | 280 | for i, test := range tests { 281 | promptLen := runewidth.StringWidth(test.Prompt) 282 | x, y := LineWrappedCellPos(promptLen, []rune(test.Line), test.TermWidth) 283 | 284 | if (test.Expected.X != x) || (test.Expected.Y != y) { 285 | t.Error("X or Y does not match:") 286 | t.Logf(" Test: %d (%s)", i, t.Name()) 287 | t.Logf(" Prompt: '%s'", test.Prompt) 288 | t.Logf(" Prompt len %d", promptLen) 289 | t.Logf(" Line: '%s'", test.Line) 290 | t.Logf(" Width: %d", test.TermWidth) 291 | t.Logf(" Expected X:%d", test.Expected.X) 292 | t.Logf(" Actual X:%d", x) 293 | t.Logf(" Expected Y:%d", test.Expected.Y) 294 | t.Logf(" Actual Y:%d", y) 295 | } 296 | 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /write_tty.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package readline 5 | 6 | import "os" 7 | 8 | func _print(s string) { 9 | os.Stdout.WriteString(s) 10 | } 11 | 12 | func _printErr(s string) { 13 | os.Stderr.WriteString(s) 14 | } 15 | --------------------------------------------------------------------------------