mirror of https://github.com/Hilbis/Hilbish
Compare commits
3 Commits
dc53eef829
...
0113a4e0b4
Author | SHA1 | Date |
---|---|---|
TorchedSammy | 0113a4e0b4 | |
TorchedSammy | 5a2e3e4700 | |
TorchedSammy | b05cb30ed7 |
2
exec.go
2
exec.go
|
@ -244,7 +244,7 @@ func splitInput(input string) ([]string, string) {
|
||||||
|
|
||||||
func cmdFinish(code uint8, cmdstr, oldInput string) {
|
func cmdFinish(code uint8, cmdstr, oldInput string) {
|
||||||
// if input has space at the beginning, dont put in history
|
// if input has space at the beginning, dont put in history
|
||||||
if !strings.HasPrefix(oldInput, " ") || interactive {
|
if interactive && !strings.HasPrefix(oldInput, " ") {
|
||||||
handleHistory(strings.TrimSpace(oldInput))
|
handleHistory(strings.TrimSpace(oldInput))
|
||||||
}
|
}
|
||||||
util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command")
|
util.SetField(l, hshMod, "exitCode", lua.LNumber(code), "Exit code of last exected command")
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -16,6 +16,6 @@ require (
|
||||||
|
|
||||||
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e
|
replace mvdan.cc/sh/v3 => github.com/Rosettea/sh/v3 v3.4.0-0.dev.0.20220306140409-795a84b00b4e
|
||||||
|
|
||||||
replace github.com/maxlandon/readline => github.com/Rosettea/readline-1 v0.0.0-20220305123014-31d4d4214c93
|
replace github.com/maxlandon/readline => ./readline
|
||||||
|
|
||||||
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10
|
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.10
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 4.1.0
|
||||||
|
---------
|
||||||
|
|
||||||
|
Many new features and improvements in this version:
|
||||||
|
- New keybindings (working on Emacs, and in `Vim Insert Mode`):
|
||||||
|
* `CtrlW` to cut the previous word at the cursor
|
||||||
|
* `CtrlA` to go back to the beginning of the line
|
||||||
|
* `CtrlY` to paste the laste copy/paste buffer (see Registers)
|
||||||
|
* `CtrlU` to cut the whole line.
|
||||||
|
|
||||||
|
- More precise Vim iterations:
|
||||||
|
* Iterations can now be applied to some Vim actions (`y4w`, `d3b`)
|
||||||
|
|
||||||
|
- Implemented Vim registers:
|
||||||
|
* Yank/paste operations of any sort can occur and be assigned to registers.
|
||||||
|
* The default `""` register
|
||||||
|
* 10 numbered registers, to which bufffers are automatically added
|
||||||
|
* 26 lettered registers (lowercase), to which you can append with `"D` (D being the uppercase of the `"d` register)
|
||||||
|
* Triggered in Insert Mode with `Alt"` (buggy sometimes: goes back to Normal mode selecting a register, will have to fix this)
|
||||||
|
|
||||||
|
- Unified iterations and registers:
|
||||||
|
* To copy to the `d` register the next 4 words: `"d y4w`
|
||||||
|
* To append to this `d` register the cuttend end of line: `"D d$"`
|
||||||
|
* In this example, the `d` register buffer is also the buffer in the default register `""`
|
||||||
|
* You could either:
|
||||||
|
- Paste 3 times this buffer while in Normal mode: `3p`
|
||||||
|
- Paste the buffer once in Insert mode: `CtrlY`
|
||||||
|
|
||||||
|
- History completions:
|
||||||
|
* The binding for the alternative history changed to `AltR` (the normal remains `CtrlR`)
|
||||||
|
* By defaul the history filters only against the search pattern.
|
||||||
|
* If there are matches for this patten, the first occurence is insert (virtually)
|
||||||
|
* This is refreshed as the pattern changes
|
||||||
|
* `CtrlG` to exit the comps, while leaving the current candidate
|
||||||
|
* `CtrlC` to exit and delete the current candidate
|
||||||
|
|
||||||
|
- Completions:
|
||||||
|
* When a candidate is inserted virtually, `CtrlC` to abort both completions and the candidate
|
||||||
|
* Implemented global printing size: If the overall number of completions is biffer, will roll over them.
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
* The `rl.Readline()` function dispatch has some big cases, maybe a bit of refactoring would be nice
|
||||||
|
* The way the buffer storing bytes from key strokes sometimes gives weird results (like `Alt"` for showing Vim registers)
|
||||||
|
* Some defer/cancel calls related to DelayedTabContext that should have been merged from lmorg/readline are still missing.
|
||||||
|
|
||||||
|
|
||||||
|
### 4.0.0-beta
|
||||||
|
---------
|
||||||
|
|
||||||
|
This version is the merge of [maxlandon/readline](https://github.com/maxlandon/readline)
|
||||||
|
and [lmorg/readline](https://github.com/lmorg/readline). Therefore it both integrates parts
|
||||||
|
from both libraries, but also adds a few features, with some API breaking changes (ex: completions),
|
||||||
|
thus the new 4.0.0 version. Remains a beta because maxlandon/readline code has not been thoroughly
|
||||||
|
test neither in nor of itself, and no more against `lmorg/murex`, it's main consumer until now.
|
||||||
|
|
||||||
|
#### Code
|
||||||
|
- Enhance delete/copy buffer in Vim mode
|
||||||
|
- DelayedTabContext now works with completion groups
|
||||||
|
|
||||||
|
#### Packages
|
||||||
|
- Added a `completers` package, with a default tab/hint/syntax completer working with
|
||||||
|
the [go-flags](https://github.com/jessevdk/go-flags) library.
|
||||||
|
- The `examples` package has been enhanced with a more complete -base- application code. See the wiki
|
||||||
|
|
||||||
|
#### Documentation
|
||||||
|
- Merged relevant parts of both READMEs
|
||||||
|
- Use documentation from maxlandon/readline
|
||||||
|
|
||||||
|
#### New features / bindings
|
||||||
|
- CtrlL now clears the screen and reprints the prompt
|
||||||
|
- Added evilsocket's tui colors/effects, for ease of use & integration with shell. Has not yet replaced the current `seqColor` variables everywhere though
|
||||||
|
|
||||||
|
#### Changes I'm not sure of
|
||||||
|
- is the function leftMost() in cursor.go useful ?
|
||||||
|
- is the function getCursorPos() in cursor.go useful ?
|
||||||
|
|
||||||
|
|
||||||
|
### 3.0.0
|
||||||
|
---------
|
||||||
|
|
||||||
|
- Added test (input line, prompt, correct refresh, etc)
|
||||||
|
- Added multiline support
|
||||||
|
- Added `DelayedTabContext` and `DelayedSyntaxWorker`
|
||||||
|
|
||||||
|
|
||||||
|
### 2.1.0
|
||||||
|
---------
|
||||||
|
|
||||||
|
Error returns from `readline` have been created as error a variable, which is
|
||||||
|
more idiomatic to Go than the err constants that existed previously. Currently
|
||||||
|
both are still available to use however I will be deprecating the the constants
|
||||||
|
in a latter release.
|
||||||
|
|
||||||
|
**Deprecated constants:**
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
// ErrCtrlC is returned when ctrl+c is pressed
|
||||||
|
ErrCtrlC = "Ctrl+C"
|
||||||
|
|
||||||
|
// ErrEOF is returned when ctrl+d is pressed
|
||||||
|
ErrEOF = "EOF"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**New error variables:**
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
// CtrlC is returned when ctrl+c is pressed
|
||||||
|
CtrlC = errors.New("Ctrl+C")
|
||||||
|
|
||||||
|
// EOF is returned when ctrl+d is pressed
|
||||||
|
// (this is actually the same value as io.EOF)
|
||||||
|
EOF = errors.New("EOF")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Information
|
||||||
|
|
||||||
|
`readline`'s version numbers are based on Semantic Versioning. More details can
|
||||||
|
be found in the [README.md](README.md#version-information).
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,165 @@
|
||||||
|
|
||||||
|
# Readline - Console library in Go
|
||||||
|
|
||||||
|
![Demo](../assets/readline-demo.gif)
|
||||||
|
*This demo GIF has been made with a Sliver project client.*
|
||||||
|
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
**This project is actually the merging of an original project (github.com/lmorg/readline) and one of its
|
||||||
|
forks (github.com/maxlandon/readline): both introductions are thus here given, in chronological order.**
|
||||||
|
|
||||||
|
#### lmorg
|
||||||
|
|
||||||
|
This project began a few years prior to this git commit history as an API for
|
||||||
|
_[murex](https://github.com/lmorg/murex)_, an alternative UNIX shell, because
|
||||||
|
I wasn't satisfied with the state of existing Go packages for readline (at that
|
||||||
|
time they were either bugger and/or poorly maintained, or lacked features I
|
||||||
|
desired). The state of things for readline in Go may have changed since then
|
||||||
|
however own package had also matured and grown to include many more features
|
||||||
|
that has arisen during the development of _murex_. So it seemed only fair to
|
||||||
|
give back to the community considering it was other peoples readline libraries
|
||||||
|
that allowed me rapidly prototype _murex_ during it's early stages of
|
||||||
|
development.
|
||||||
|
|
||||||
|
#### maxlandon
|
||||||
|
|
||||||
|
This project started out of the wish to make an enhanced console for a security tool (Sliver, see below).
|
||||||
|
There are already several readline libraries available in Go ([github.com/chzyer/readline](https://github.com/chzyer/readline),
|
||||||
|
and [github.com/lmorg/readline](https://github.com/lmorg/readline)), but being stricter readline implementations, their completion
|
||||||
|
|
||||||
|
|
||||||
|
## Features Summary
|
||||||
|
|
||||||
|
This project is not an integrated REPL/command-line application, which means it does not automatically understand nor executes any commands.
|
||||||
|
However, having been developed in a project using the CLI [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags) library,
|
||||||
|
it also includes some default utilities (completers) that are made to work with this library, which I humbly but highly recommend.
|
||||||
|
Please see the [Wiki](https://github.com/maxlandon/readline/wiki) (or the `examples/` directory) for information on how to use these utilities.
|
||||||
|
|
||||||
|
A summarized list of features supported by this library is the following:
|
||||||
|
|
||||||
|
### Input & Editing
|
||||||
|
- Vim / Emacs input and editing modes.
|
||||||
|
- Optional, live-refresh Vim status.
|
||||||
|
- Vim modes (Insert, Normal, Replace, Delete) with visual prompt Vim status indicator
|
||||||
|
- line editing using `$EDITOR` (`vi` in the example - enabled by pressing `[ESC]` followed by `[v]`)
|
||||||
|
- Vim registers (one default, 10 numbered, and 26 lettered)
|
||||||
|
- Vim iterations
|
||||||
|
- Most default keybindings you might find in Emacs-like readline. Some are still missing though
|
||||||
|
|
||||||
|
### Completion engine
|
||||||
|
- 3 types of completion categories (`Grid`, `List` and `Map`)
|
||||||
|
- Stackable, combinable completions (completion groups of any type & size can be proposed simultaneously).
|
||||||
|
- Controlable completion group sizes (if size is greater than completions, the completions will roll automatically)
|
||||||
|
- Virtual insertion of the current candidate, like in Z-shell.
|
||||||
|
- In `List` completion groups, ability to have alternative candidates (used for displaying `--long` and `-s` (short) options, with descriptions)
|
||||||
|
- Completions working anywhere in the input line (your cursor can be anywhere)
|
||||||
|
- Completions are searchable with *Ctrl-F*, like in lmorg's library.
|
||||||
|
|
||||||
|
### Prompt system & Colors
|
||||||
|
- 1-line and 2-line prompts, both being customizable.
|
||||||
|
- Functions for refreshing the prompt, with optional behavior settings.
|
||||||
|
- Optional colors (can be disabled).
|
||||||
|
|
||||||
|
### Hints & Syntax highlighting
|
||||||
|
- A hint line can be printed below the input line, with any type of information. See utilities for a default one.
|
||||||
|
- The Hint system is now refreshed depending on the cursor position as well, like completions.
|
||||||
|
- A syntax highlighting system. A default one is also available.
|
||||||
|
|
||||||
|
### Command history
|
||||||
|
- Ability to have 2 different history sources (I used this for clients connected to a server, used by a single user).
|
||||||
|
- History is searchable like completions.
|
||||||
|
- Default history is an in-memory list.
|
||||||
|
- Quick history navigation with *Up*/*Down* arrow keys in Emacs mode, and *j*/*k* keys in Vim mode.
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
- Default Tab completer, Hint formatter and Syntax highlighter provided, using [github.com/jessevdk/go-flags](https://github.com/jessevdk/go-flags)
|
||||||
|
command parser to build themselves. These are in the `completers/` directory. Please look at the [Wiki page](https://github.com/maxlandon/readline/wiki)
|
||||||
|
for how to use them. Also feel free to use them as an inspiration source to make your owns.
|
||||||
|
- Colors mercilessly copied from [github.com/evilsocket/islazy/](https://github.com/evilsocket/islazy) `tui/` package.
|
||||||
|
- Also in the `completers` directory, completion functions for environment variables (using Go's std lib for getting them), and dir/file path completions.
|
||||||
|
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
As usual with Go, installation:
|
||||||
|
```
|
||||||
|
go get github.com/maxlandon/readline
|
||||||
|
```
|
||||||
|
Please see either the `examples` directory, or the Wiki for detailed instructions on how to use this library.
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
The complete documentation for this library can be found in the repo's [Wiki](https://github.com/maxlandon/readline/wiki). Below is the Table of Contents:
|
||||||
|
|
||||||
|
**Getting started**
|
||||||
|
* [ Embedding readline in a project ](https://github.com/maxlandon/readline/wiki/Embedding-Readline-In-A-Project)
|
||||||
|
* [ Input Modes ](https://github.com/maxlandon/readline/wiki/Input-Modes)
|
||||||
|
|
||||||
|
**Prompt system**
|
||||||
|
* [ Setting the Prompts](https://github.com/maxlandon/readline/wiki/Prompt-Setup)
|
||||||
|
* [ Prompt Refresh ](https://github.com/maxlandon/readline/wiki/Prompt-Refresh)
|
||||||
|
|
||||||
|
**Completion Engine**
|
||||||
|
* [ Completion Groups ](https://github.com/maxlandon/readline/wiki/Completion-Groups)
|
||||||
|
* [ Completion Search ](https://github.com/maxlandon/readline/wiki/Completion-Search)
|
||||||
|
|
||||||
|
**Hint Formatter & Syntax Highlighter**
|
||||||
|
* [ Live Refresh Demonstration ](https://github.com/maxlandon/readline/wiki/Live-Refresh-Demonstration)
|
||||||
|
|
||||||
|
**Command History**
|
||||||
|
* [ Main & Alternative Sources ](https://github.com/maxlandon/readline/wiki/Main-&-Alternative-Sources)
|
||||||
|
* [ Navigation & Search ](https://github.com/maxlandon/readline/wiki/Navigation-&-Search)
|
||||||
|
|
||||||
|
#### Command & Completion utilities
|
||||||
|
* [ Interfacing with the go-flags library](https://github.com/maxlandon/readline/wiki/Interfacing-With-Go-Flags)
|
||||||
|
* [ Declaring go-flags commands](https://github.com/maxlandon/readline/wiki/Declaring-Commands)
|
||||||
|
* [ Colors/Effects Usage ](https://github.com/maxlandon/readline/wiki/Colors-&-Effects-Usage)
|
||||||
|
|
||||||
|
|
||||||
|
## Project Status & Support
|
||||||
|
|
||||||
|
Being alone working on this project and having only one lifetime (anyone able to solve this please call me), I can engage myself over the following:
|
||||||
|
- Support for any issue opened.
|
||||||
|
- Answering any questions related.
|
||||||
|
- Being available for any blame you'd like to make for my humble but passioned work. I don't mind, I need to go up.
|
||||||
|
|
||||||
|
|
||||||
|
## Version Information
|
||||||
|
|
||||||
|
* The version string will be based on Semantic Versioning. ie version numbers
|
||||||
|
will be formatted `(major).(minor).(patch)` - for example `2.0.1`
|
||||||
|
|
||||||
|
* `major` releases _will_ have breaking changes. Be sure to read CHANGES.md for
|
||||||
|
upgrade instructions
|
||||||
|
|
||||||
|
* `minor` releases will contain new APIs or introduce new user facing features
|
||||||
|
which may affect useability from an end user perspective. However `minor`
|
||||||
|
releases will not break backwards compatibility at the source code level and
|
||||||
|
nor will it break existing expected user-facing behavior. These changes will
|
||||||
|
be documented in CHANGES.md too
|
||||||
|
|
||||||
|
* `patch` releases will be bug fixes and such like. Where the code has changed
|
||||||
|
but both API endpoints and user experience will remain the same (except where
|
||||||
|
expected user experience was broken due to a bug, then that would be bumped
|
||||||
|
to either a `minor` or `major` depending on the significance of the bug and
|
||||||
|
the significance of the change to the user experience)
|
||||||
|
|
||||||
|
* Any updates to documentation, comments within code or the example code will
|
||||||
|
not result in a version bump because they will not affect the output of the
|
||||||
|
go compiler. However if this concerns you then I recommend pinning your
|
||||||
|
project to the git commit hash rather than a `patch` release
|
||||||
|
|
||||||
|
## License Information
|
||||||
|
|
||||||
|
The `readline` library is distributed under the Apache License (Version 2.0, January 2004) (http://www.apache.org/licenses/).
|
||||||
|
All the example code and documentation in `/examples`, `/completers` is public domain.
|
||||||
|
|
||||||
|
|
||||||
|
## Warmest Thanks
|
||||||
|
|
||||||
|
- The [Sliver](https://github.com/BishopFox/sliver) implant framework project, which I used as a basis to make, test and refine this library. as well as all the GIFs and documentation pictures !
|
||||||
|
- [evilsocket](https://github.com/evilsocket) for his TUI library !
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
// Character codes
|
||||||
|
const (
|
||||||
|
charCtrlA = iota + 1
|
||||||
|
charCtrlB
|
||||||
|
charCtrlC
|
||||||
|
charEOF
|
||||||
|
charCtrlE
|
||||||
|
charCtrlF
|
||||||
|
charCtrlG
|
||||||
|
charBackspace // ISO 646
|
||||||
|
charTab
|
||||||
|
charCtrlJ
|
||||||
|
charCtrlK
|
||||||
|
charCtrlL
|
||||||
|
charCtrlM
|
||||||
|
charCtrlN
|
||||||
|
charCtrlO
|
||||||
|
charCtrlP
|
||||||
|
charCtrlQ
|
||||||
|
charCtrlR
|
||||||
|
charCtrlS
|
||||||
|
charCtrlT
|
||||||
|
charCtrlU
|
||||||
|
charCtrlV
|
||||||
|
charCtrlW
|
||||||
|
charCtrlX
|
||||||
|
charCtrlY
|
||||||
|
charCtrlZ
|
||||||
|
charEscape
|
||||||
|
charCtrlSlash // ^\
|
||||||
|
charCtrlCloseSquare // ^]
|
||||||
|
charCtrlHat // ^^
|
||||||
|
charCtrlUnderscore // ^_
|
||||||
|
charBackspace2 = 127 // ASCII 1963
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
// Escape sequences
|
||||||
|
var (
|
||||||
|
seqUp = string([]byte{27, 91, 65})
|
||||||
|
seqDown = string([]byte{27, 91, 66})
|
||||||
|
seqForwards = string([]byte{27, 91, 67})
|
||||||
|
seqBackwards = string([]byte{27, 91, 68})
|
||||||
|
seqHome = string([]byte{27, 91, 72})
|
||||||
|
seqHomeSc = string([]byte{27, 91, 49, 126})
|
||||||
|
seqEnd = string([]byte{27, 91, 70})
|
||||||
|
seqEndSc = string([]byte{27, 91, 52, 126})
|
||||||
|
seqDelete = string([]byte{27, 91, 51, 126})
|
||||||
|
seqShiftTab = string([]byte{27, 91, 90})
|
||||||
|
seqAltQuote = string([]byte{27, 34}) // Added for showing registers ^["
|
||||||
|
seqAltR = string([]byte{27, 114}) // Used for alternative history
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
seqPosSave = "\x1b[s"
|
||||||
|
seqPosRestore = "\x1b[u"
|
||||||
|
|
||||||
|
seqClearLineAfer = "\x1b[0k"
|
||||||
|
seqClearLineBefore = "\x1b[1k"
|
||||||
|
seqClearLine = "\x1b[2k"
|
||||||
|
seqClearScreenBelow = "\x1b[0J"
|
||||||
|
seqClearScreen = "\x1b[2J" // Clears screen fully
|
||||||
|
seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left
|
||||||
|
|
||||||
|
seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R"
|
||||||
|
|
||||||
|
seqCtrlLeftArrow = "\x1b[1;5D"
|
||||||
|
seqCtrlRightArrow = "\x1b[1;5C"
|
||||||
|
|
||||||
|
// seqAltQuote = "\x1b\"" // trigger registers list
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text effects
|
||||||
|
const (
|
||||||
|
seqReset = "\x1b[0m"
|
||||||
|
seqBold = "\x1b[1m"
|
||||||
|
seqUnderscore = "\x1b[4m"
|
||||||
|
seqBlink = "\x1b[5m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text colours
|
||||||
|
const (
|
||||||
|
seqFgBlack = "\x1b[30m"
|
||||||
|
seqFgRed = "\x1b[31m"
|
||||||
|
seqFgGreen = "\x1b[32m"
|
||||||
|
seqFgYellow = "\x1b[33m"
|
||||||
|
seqFgBlue = "\x1b[34m"
|
||||||
|
seqFgMagenta = "\x1b[35m"
|
||||||
|
seqFgCyan = "\x1b[36m"
|
||||||
|
seqFgWhite = "\x1b[37m"
|
||||||
|
|
||||||
|
seqFgBlackBright = "\x1b[1;30m"
|
||||||
|
seqFgRedBright = "\x1b[1;31m"
|
||||||
|
seqFgGreenBright = "\x1b[1;32m"
|
||||||
|
seqFgYellowBright = "\x1b[1;33m"
|
||||||
|
seqFgBlueBright = "\x1b[1;34m"
|
||||||
|
seqFgMagentaBright = "\x1b[1;35m"
|
||||||
|
seqFgCyanBright = "\x1b[1;36m"
|
||||||
|
seqFgWhiteBright = "\x1b[1;37m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background colours
|
||||||
|
const (
|
||||||
|
seqBgBlack = "\x1b[40m"
|
||||||
|
seqBgRed = "\x1b[41m"
|
||||||
|
seqBgGreen = "\x1b[42m"
|
||||||
|
seqBgYellow = "\x1b[43m"
|
||||||
|
seqBgBlue = "\x1b[44m"
|
||||||
|
seqBgMagenta = "\x1b[45m"
|
||||||
|
seqBgCyan = "\x1b[46m"
|
||||||
|
seqBgWhite = "\x1b[47m"
|
||||||
|
|
||||||
|
seqBgBlackBright = "\x1b[1;40m"
|
||||||
|
seqBgRedBright = "\x1b[1;41m"
|
||||||
|
seqBgGreenBright = "\x1b[1;42m"
|
||||||
|
seqBgYellowBright = "\x1b[1;43m"
|
||||||
|
seqBgBlueBright = "\x1b[1;44m"
|
||||||
|
seqBgMagentaBright = "\x1b[1;45m"
|
||||||
|
seqBgCyanBright = "\x1b[1;46m"
|
||||||
|
seqBgWhiteBright = "\x1b[1;47m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Xterm 256 colors
|
||||||
|
const (
|
||||||
|
seqCtermFg255 = "\033[48;5;255m"
|
||||||
|
)
|
|
@ -0,0 +1,144 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initGrid - Grid display details. Called each time we want to be sure to have
|
||||||
|
// a working completion group either immediately, or later on. Generally defered.
|
||||||
|
func (g *CompletionGroup) initGrid(rl *Instance) {
|
||||||
|
|
||||||
|
// Compute size of each completion item box
|
||||||
|
tcMaxLength := 1
|
||||||
|
for i := range g.Suggestions {
|
||||||
|
if len(g.Suggestions[i]) > tcMaxLength {
|
||||||
|
tcMaxLength = len([]rune(g.Suggestions[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset = 0
|
||||||
|
|
||||||
|
// Max number of columns
|
||||||
|
g.tcMaxX = GetTermWidth() / (tcMaxLength + 2)
|
||||||
|
if g.tcMaxX < 1 {
|
||||||
|
g.tcMaxX = 1 // avoid a divide by zero error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximum number of lines
|
||||||
|
maxY := len(g.Suggestions) / g.tcMaxX
|
||||||
|
rest := len(g.Suggestions) % g.tcMaxX
|
||||||
|
if rest != 0 {
|
||||||
|
// if rest != 0 && maxY != 1 {
|
||||||
|
maxY++
|
||||||
|
}
|
||||||
|
if maxY > g.MaxLength {
|
||||||
|
g.tcMaxY = g.MaxLength
|
||||||
|
} else {
|
||||||
|
g.tcMaxY = maxY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveTabGridHighlight - Moves the highlighting for currently selected completion item (grid display)
|
||||||
|
func (g *CompletionGroup) moveTabGridHighlight(rl *Instance, x, y int) (done bool, next bool) {
|
||||||
|
|
||||||
|
g.tcPosX += x
|
||||||
|
g.tcPosY += y
|
||||||
|
|
||||||
|
// Columns
|
||||||
|
if g.tcPosX < 1 {
|
||||||
|
if g.tcPosY == 1 && rl.tabCompletionReverse {
|
||||||
|
g.tcPosX = 1
|
||||||
|
g.tcPosY = 0
|
||||||
|
} else {
|
||||||
|
// This is when multiple ligns, not yet on first one.
|
||||||
|
g.tcPosX = g.tcMaxX
|
||||||
|
g.tcPosY--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.tcPosY > g.tcMaxY {
|
||||||
|
g.tcPosY = 1
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we must move to next line in same group
|
||||||
|
if g.tcPosX > g.tcMaxX {
|
||||||
|
g.tcPosX = 1
|
||||||
|
g.tcPosY++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real max number of suggestions.
|
||||||
|
max := g.tcMaxX * g.tcMaxY
|
||||||
|
if max > len(g.Suggestions) {
|
||||||
|
max = len(g.Suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We arrived at the end of suggestions. This condition can never be triggered
|
||||||
|
// while going in the reverse order, only forward, so no further checks in it.
|
||||||
|
if (g.tcMaxX*(g.tcPosY-1))+g.tcPosX > max {
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case we are reverse cycling and currently selecting the first item,
|
||||||
|
// we adjust the coordinates to point to the last item and return
|
||||||
|
// We set g.tcPosY because the printer needs to get the a candidate nonetheless.
|
||||||
|
if rl.tabCompletionReverse && g.tcPosX == 1 && g.tcPosY == 0 {
|
||||||
|
g.tcPosY = 1
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, come back to this group for next item.
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeGrid - A grid completion string
|
||||||
|
func (g *CompletionGroup) writeGrid(rl *Instance) (comp string) {
|
||||||
|
|
||||||
|
// If group title, print it and adjust offset.
|
||||||
|
if g.Name != "" {
|
||||||
|
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
|
||||||
|
rl.tcUsedY++
|
||||||
|
}
|
||||||
|
|
||||||
|
cellWidth := strconv.Itoa((GetTermWidth() / g.tcMaxX) - 2)
|
||||||
|
x := 0
|
||||||
|
y := 1
|
||||||
|
|
||||||
|
for i := range g.Suggestions {
|
||||||
|
x++
|
||||||
|
if x > g.tcMaxX {
|
||||||
|
x = 1
|
||||||
|
y++
|
||||||
|
if y > g.tcMaxY {
|
||||||
|
y--
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
comp += "\r\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x == g.tcPosX && y == g.tcPosY) && (g.isCurrent) {
|
||||||
|
comp += seqCtermFg255 + seqFgBlackBright
|
||||||
|
}
|
||||||
|
|
||||||
|
comp += fmt.Sprintf("%-"+cellWidth+"s %s", g.Suggestions[i], seqReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always add a newline to the group if the end if not punctuated with one
|
||||||
|
if !strings.HasSuffix(comp, "\n") {
|
||||||
|
comp += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the equivalent of this group's size to final screen clearing.
|
||||||
|
// This is either the max allowed print size for this group, or its actual size if inferior.
|
||||||
|
if g.MaxLength < y {
|
||||||
|
rl.tcUsedY += g.MaxLength
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += y
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,286 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
// CompletionGroup - A group/category of items offered to completion, with its own
|
||||||
|
// name, descriptions and completion display format/type.
|
||||||
|
// The output, if there are multiple groups available for a given completion input,
|
||||||
|
// will look like ZSH's completion system.
|
||||||
|
type CompletionGroup struct {
|
||||||
|
Name string // If not nil, printed on top of the group's completions
|
||||||
|
Description string
|
||||||
|
|
||||||
|
// Candidates & related
|
||||||
|
Suggestions []string
|
||||||
|
Aliases map[string]string // A candidate has an alternative name (ex: --long, -l option flags)
|
||||||
|
Descriptions map[string]string // Items descriptions
|
||||||
|
DisplayType TabDisplayType // Map, list or normal
|
||||||
|
MaxLength int // Each group can be limited in the number of comps offered
|
||||||
|
|
||||||
|
// When this is true, the completion is inserted really (not virtually) without
|
||||||
|
// the trailing slash, if any. This is used when we want to complete paths.
|
||||||
|
TrimSlash bool
|
||||||
|
// PathSeparator - If you intend to write path completions, you can specify the path separator to use, depending on which OS you want completion for. By default, this will be set to the GOOS of the binary. This is also used internally for many things.
|
||||||
|
PathSeparator rune
|
||||||
|
|
||||||
|
// When this is true, we don't add a space after entering the candidate.
|
||||||
|
// Can be used for multi-stage completions, like URLS (scheme:// + host)
|
||||||
|
NoSpace bool
|
||||||
|
|
||||||
|
// For each group, we can define the min and max tab item length
|
||||||
|
MinTabItemLength int
|
||||||
|
MaxTabItemLength int
|
||||||
|
|
||||||
|
// Values used by the shell
|
||||||
|
tcPosX int
|
||||||
|
tcPosY int
|
||||||
|
tcMaxX int
|
||||||
|
tcMaxY int
|
||||||
|
tcOffset int
|
||||||
|
tcMaxLength int // Used when display is map/list, for determining message width
|
||||||
|
tcMaxLengthAlt int // Same as tcMaxLength but for SuggestionsAlt.
|
||||||
|
|
||||||
|
// true if we want to cycle through suggestions because they overflow MaxLength
|
||||||
|
allowCycle bool
|
||||||
|
|
||||||
|
// This is to say we are currently cycling through this group, for highlighting choice
|
||||||
|
isCurrent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// init - The completion group computes and sets all its values, and is then ready to work.
|
||||||
|
func (g *CompletionGroup) init(rl *Instance) {
|
||||||
|
|
||||||
|
// Details common to all displays
|
||||||
|
g.checkCycle(rl) // Based on the number of groups given to the shell, allows cycling or not
|
||||||
|
g.checkMaxLength(rl)
|
||||||
|
|
||||||
|
// Details specific to tab display modes
|
||||||
|
switch g.DisplayType {
|
||||||
|
|
||||||
|
case TabDisplayGrid:
|
||||||
|
g.initGrid(rl)
|
||||||
|
case TabDisplayMap:
|
||||||
|
g.initMap(rl)
|
||||||
|
case TabDisplayList:
|
||||||
|
g.initList(rl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateTabFind - When searching through all completion groups (whether it be command history or not),
|
||||||
|
// we ask each of them to filter its own items and return the results to the shell for aggregating them.
|
||||||
|
// The rx parameter is passed, as the shell already checked that the search pattern is valid.
|
||||||
|
func (g *CompletionGroup) updateTabFind(rl *Instance) {
|
||||||
|
|
||||||
|
suggs := make([]string, 0)
|
||||||
|
|
||||||
|
// We perform filter right here, so we create a new completion group, and populate it with our results.
|
||||||
|
for i := range g.Suggestions {
|
||||||
|
if rl.regexSearch.MatchString(g.Suggestions[i]) {
|
||||||
|
suggs = append(suggs, g.Suggestions[i])
|
||||||
|
} else if g.DisplayType == TabDisplayList && rl.regexSearch.MatchString(g.Descriptions[g.Suggestions[i]]) {
|
||||||
|
// this is a list so lets also check the descriptions
|
||||||
|
suggs = append(suggs, g.Suggestions[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We overwrite the group's items, (will be refreshed as soon as something is typed in the search)
|
||||||
|
g.Suggestions = suggs
|
||||||
|
|
||||||
|
// Finally, the group computes its new printing settings
|
||||||
|
g.init(rl)
|
||||||
|
|
||||||
|
// If we are in history completion, we directly pass to the first candidate
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind && len(g.Suggestions) > 0 {
|
||||||
|
g.tcPosY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkCycle - Based on the number of groups given to the shell, allows cycling or not
|
||||||
|
func (g *CompletionGroup) checkCycle(rl *Instance) {
|
||||||
|
if len(rl.tcGroups) == 1 {
|
||||||
|
g.allowCycle = true
|
||||||
|
}
|
||||||
|
if len(rl.tcGroups) >= 10 {
|
||||||
|
g.allowCycle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkMaxLength - Based on the number of groups given to the shell, check/set MaxLength defaults
|
||||||
|
func (g *CompletionGroup) checkMaxLength(rl *Instance) {
|
||||||
|
|
||||||
|
// This means the user forgot to set it
|
||||||
|
if g.MaxLength == 0 {
|
||||||
|
if len(rl.tcGroups) < 5 {
|
||||||
|
g.MaxLength = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rl.tcGroups) >= 5 {
|
||||||
|
g.MaxLength = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists that have a alternative completions are not allowed to have
|
||||||
|
// MaxLength set, because rolling does not work yet.
|
||||||
|
if g.DisplayType == TabDisplayList {
|
||||||
|
g.MaxLength = 1000 // Should be enough not to trigger anything related.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNilItems - For each completion group we avoid nil maps and possibly other items
|
||||||
|
func checkNilItems(groups []*CompletionGroup) (checked []*CompletionGroup) {
|
||||||
|
|
||||||
|
for _, grp := range groups {
|
||||||
|
if grp.Descriptions == nil || len(grp.Descriptions) == 0 {
|
||||||
|
grp.Descriptions = make(map[string]string)
|
||||||
|
}
|
||||||
|
if grp.Aliases == nil || len(grp.Aliases) == 0 {
|
||||||
|
grp.Aliases = make(map[string]string)
|
||||||
|
}
|
||||||
|
checked = append(checked, grp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeCompletion - This function produces a formatted string containing all appropriate items
|
||||||
|
// and according to display settings. This string is then appended to the main completion string.
|
||||||
|
func (g *CompletionGroup) writeCompletion(rl *Instance) (comp string) {
|
||||||
|
|
||||||
|
// Avoids empty groups in suggestions
|
||||||
|
if len(g.Suggestions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending on display type we produce the approriate string
|
||||||
|
switch g.DisplayType {
|
||||||
|
|
||||||
|
case TabDisplayGrid:
|
||||||
|
comp += g.writeGrid(rl)
|
||||||
|
case TabDisplayMap:
|
||||||
|
comp += g.writeMap(rl)
|
||||||
|
case TabDisplayList:
|
||||||
|
comp += g.writeList(rl)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentCell - The completion groups computes the current cell value,
|
||||||
|
// depending on its display type and its different parameters
|
||||||
|
func (g *CompletionGroup) getCurrentCell(rl *Instance) string {
|
||||||
|
|
||||||
|
switch g.DisplayType {
|
||||||
|
case TabDisplayGrid:
|
||||||
|
// x & y coodinates + safety check
|
||||||
|
cell := (g.tcMaxX * (g.tcPosY - 1)) + g.tcOffset + g.tcPosX - 1
|
||||||
|
if cell < 0 {
|
||||||
|
cell = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if cell < len(g.Suggestions) {
|
||||||
|
return g.Suggestions[cell]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
|
||||||
|
case TabDisplayMap:
|
||||||
|
// x & y coodinates + safety check
|
||||||
|
cell := g.tcOffset + g.tcPosY - 1
|
||||||
|
if cell < 0 {
|
||||||
|
cell = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sugg := g.Suggestions[cell]
|
||||||
|
return sugg
|
||||||
|
|
||||||
|
case TabDisplayList:
|
||||||
|
// x & y coodinates + safety check
|
||||||
|
cell := g.tcOffset + g.tcPosY - 1
|
||||||
|
if cell < 0 {
|
||||||
|
cell = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sugg := g.Suggestions[cell]
|
||||||
|
|
||||||
|
// If we are in the alt suggestions column, check key and return
|
||||||
|
if g.tcPosX == 1 {
|
||||||
|
if alt, ok := g.Aliases[sugg]; ok {
|
||||||
|
return alt
|
||||||
|
}
|
||||||
|
return sugg
|
||||||
|
}
|
||||||
|
return sugg
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should never get here
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *CompletionGroup) goFirstCell() {
|
||||||
|
switch g.DisplayType {
|
||||||
|
case TabDisplayGrid:
|
||||||
|
g.tcPosX = 1
|
||||||
|
g.tcPosY = 1
|
||||||
|
|
||||||
|
case TabDisplayList:
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset = 0
|
||||||
|
|
||||||
|
case TabDisplayMap:
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *CompletionGroup) goLastCell() {
|
||||||
|
switch g.DisplayType {
|
||||||
|
case TabDisplayGrid:
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
|
||||||
|
restX := len(g.Suggestions) % g.tcMaxX
|
||||||
|
if restX != 0 {
|
||||||
|
g.tcPosX = restX
|
||||||
|
} else {
|
||||||
|
g.tcPosX = g.tcMaxX
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to adjust the X position depending
|
||||||
|
// on the interpretation of the remainder with
|
||||||
|
// respect to the group's MaxLength.
|
||||||
|
restY := len(g.Suggestions) % g.tcMaxY
|
||||||
|
maxY := len(g.Suggestions) / g.tcMaxX
|
||||||
|
if restY == 0 && maxY > g.MaxLength {
|
||||||
|
g.tcPosX = g.tcMaxX
|
||||||
|
}
|
||||||
|
if restY != 0 && maxY > g.MaxLength-1 {
|
||||||
|
g.tcPosX = g.tcMaxX
|
||||||
|
}
|
||||||
|
|
||||||
|
case TabDisplayList:
|
||||||
|
// By default, the last item is at maxY
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
|
||||||
|
// If the max length is smaller than the number
|
||||||
|
// of suggestions, we need to adjust the offset.
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
g.tcOffset = len(g.Suggestions) - g.tcMaxY
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not take into account the alternative suggestions
|
||||||
|
g.tcPosX = 0
|
||||||
|
|
||||||
|
case TabDisplayMap:
|
||||||
|
// By default, the last item is at maxY
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
|
||||||
|
// If the max length is smaller than the number
|
||||||
|
// of suggestions, we need to adjust the offset.
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
g.tcOffset = len(g.Suggestions) - g.tcMaxY
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not take into account the alternative suggestions
|
||||||
|
g.tcPosX = 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initList - List display details. Because of the way alternative completions
|
||||||
|
// are handled, MaxLength cannot be set when there are alternative completions.
|
||||||
|
func (g *CompletionGroup) initList(rl *Instance) {
|
||||||
|
|
||||||
|
// We may only ever have two different
|
||||||
|
// columns: (suggestions, and alternatives)
|
||||||
|
g.tcMaxX = 2
|
||||||
|
|
||||||
|
// We make the list anyway, especially if we need to use it later
|
||||||
|
if g.Descriptions == nil {
|
||||||
|
g.Descriptions = make(map[string]string)
|
||||||
|
}
|
||||||
|
if g.Aliases == nil {
|
||||||
|
g.Aliases = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute size of each completion item box. Group independent
|
||||||
|
g.tcMaxLength = rl.getListPad()
|
||||||
|
|
||||||
|
// Same for suggestions alt
|
||||||
|
g.tcMaxLengthAlt = 0
|
||||||
|
for i := range g.Suggestions {
|
||||||
|
if len(g.Suggestions[i]) > g.tcMaxLength {
|
||||||
|
g.tcMaxLength = len([]rune(g.Suggestions[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max values depend on if we have alternative suggestions
|
||||||
|
if len(g.Aliases) == 0 {
|
||||||
|
g.tcMaxX = 1
|
||||||
|
} else {
|
||||||
|
g.tcMaxX = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
g.tcMaxY = g.MaxLength
|
||||||
|
} else {
|
||||||
|
g.tcMaxY = len(g.Suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = 0
|
||||||
|
g.tcOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveTabListHighlight - Moves the highlighting for currently selected completion item (list display)
|
||||||
|
// We don't care about the x, because only can have 2 columns of selectable choices (--long and -s)
|
||||||
|
func (g *CompletionGroup) moveTabListHighlight(rl *Instance, x, y int) (done bool, next bool) {
|
||||||
|
|
||||||
|
// We dont' pass to x, because not managed by callers
|
||||||
|
g.tcPosY += x
|
||||||
|
g.tcPosY += y
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
if g.tcPosY < 1 {
|
||||||
|
if rl.tabCompletionReverse {
|
||||||
|
if g.tcOffset > 0 {
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset--
|
||||||
|
} else {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.tcPosY > g.tcMaxY {
|
||||||
|
g.tcPosY--
|
||||||
|
g.tcOffset++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we get to the end of choices: check which column we were selecting.
|
||||||
|
if g.tcOffset+g.tcPosY > len(g.Suggestions) {
|
||||||
|
// If we have alternative options and that we are not yet
|
||||||
|
// completing them, start on top of their column
|
||||||
|
if g.tcPosX == 0 && len(g.Aliases) > 0 {
|
||||||
|
g.tcPosX++
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset = 0
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else no alternatives, return for next group.
|
||||||
|
// Reset all values, in case we pass on them again.
|
||||||
|
g.tcPosX = 0 // First column
|
||||||
|
g.tcPosY = 1 // first row
|
||||||
|
g.tcOffset = 0
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we must check, in x == 1, that the current choice
|
||||||
|
// is not empty. Handle for both reverse and forward movements.
|
||||||
|
sugg := g.Suggestions[g.tcPosY-1]
|
||||||
|
_, ok := g.Aliases[sugg]
|
||||||
|
if !ok && g.tcPosX == 1 {
|
||||||
|
if rl.tabCompletionReverse {
|
||||||
|
for i := len(g.Suggestions[:g.tcPosY-1]); i > 0; i-- {
|
||||||
|
su := g.Suggestions[i]
|
||||||
|
if _, ok := g.Aliases[su]; ok {
|
||||||
|
g.tcPosY -= (len(g.Suggestions[:g.tcPosY-1])) - i
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for i, su := range g.Suggestions[g.tcPosY-1:] {
|
||||||
|
if _, ok := g.Aliases[su]; ok {
|
||||||
|
g.tcPosY += i
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup offset if needs to be.
|
||||||
|
// TODO: should be rewrited to conditionally process rolling menus with alternatives
|
||||||
|
if g.tcOffset+g.tcPosY < 1 && len(g.Suggestions) > 0 {
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
g.tcOffset = len(g.Suggestions) - g.tcMaxY
|
||||||
|
}
|
||||||
|
if g.tcOffset < 0 {
|
||||||
|
g.tcOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIGHT BE NEEDED IF PROBLEMS WIHT ROLLING COMPLETIONS
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
// Once we get to the end of choices: check which column we were selecting.
|
||||||
|
// We use +1 because we may have a single suggestion, and we just want "a ratio"
|
||||||
|
// if g.tcOffset+g.tcPosY > len(g.Suggestions) {
|
||||||
|
//
|
||||||
|
// // If we have alternative options and that we are not yet
|
||||||
|
// // completing them, start on top of their column
|
||||||
|
// if g.tcPosX == 1 && len(g.SuggestionsAlt) > 0 {
|
||||||
|
// g.tcPosX++
|
||||||
|
// g.tcPosY = 1
|
||||||
|
// g.tcOffset = 0
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Else no alternatives, return for next group.
|
||||||
|
// g.tcPosY = 1
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeList - A list completion string
|
||||||
|
func (g *CompletionGroup) writeList(rl *Instance) (comp string) {
|
||||||
|
|
||||||
|
// Print group title and adjust offset if there is one.
|
||||||
|
if g.Name != "" {
|
||||||
|
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
|
||||||
|
rl.tcUsedY++
|
||||||
|
}
|
||||||
|
|
||||||
|
termWidth := GetTermWidth()
|
||||||
|
if termWidth < 20 {
|
||||||
|
// terminal too small. Probably better we do nothing instead of crash
|
||||||
|
// We are more conservative than lmorg, and push it to 20 instead of 10
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestion cells dimensions
|
||||||
|
maxLength := g.tcMaxLength
|
||||||
|
if maxLength > termWidth-9 {
|
||||||
|
maxLength = termWidth - 9
|
||||||
|
}
|
||||||
|
cellWidth := strconv.Itoa(maxLength)
|
||||||
|
|
||||||
|
// Alternative suggestion cells dimensions
|
||||||
|
maxLengthAlt := g.tcMaxLengthAlt + 2
|
||||||
|
if maxLengthAlt > termWidth-9 {
|
||||||
|
maxLengthAlt = termWidth - 9
|
||||||
|
}
|
||||||
|
cellWidthAlt := strconv.Itoa(maxLengthAlt)
|
||||||
|
|
||||||
|
// Descriptions cells dimensions
|
||||||
|
maxDescWidth := termWidth - maxLength - maxLengthAlt - 4
|
||||||
|
|
||||||
|
// function highlights the cell depending on current selector place.
|
||||||
|
highlight := func(y int, x int) string {
|
||||||
|
if y == g.tcPosY && x == g.tcPosX && g.isCurrent {
|
||||||
|
return seqCtermFg255 + seqFgBlackBright
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each line in completions
|
||||||
|
y := 0
|
||||||
|
for i := g.tcOffset; i < len(g.Suggestions); i++ {
|
||||||
|
y++ // Consider next item
|
||||||
|
if y > g.tcMaxY {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main suggestion
|
||||||
|
item := g.Suggestions[i]
|
||||||
|
if len(item) > maxLength {
|
||||||
|
item = item[:maxLength-3] + "..."
|
||||||
|
}
|
||||||
|
sugg := fmt.Sprintf("\r%s%-"+cellWidth+"s", highlight(y, 0), item)
|
||||||
|
|
||||||
|
// Alt suggestion
|
||||||
|
alt, ok := g.Aliases[item]
|
||||||
|
if ok {
|
||||||
|
alt = fmt.Sprintf(" %s%"+cellWidthAlt+"s", highlight(y, 1), alt)
|
||||||
|
} else {
|
||||||
|
// Else, make an empty cell
|
||||||
|
alt = strings.Repeat(" ", maxLengthAlt+1) // + 2 to keep account of spaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
description := g.Descriptions[g.Suggestions[i]]
|
||||||
|
if len(description) > maxDescWidth {
|
||||||
|
description = description[:maxDescWidth-3] + "..." + RESET + "\n"
|
||||||
|
} else {
|
||||||
|
description += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total completion line
|
||||||
|
comp += sugg + seqReset + alt + " " + seqReset + description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the equivalent of this group's size to final screen clearing
|
||||||
|
// Can be set and used only if no alterative completions have been given.
|
||||||
|
if len(g.Aliases) == 0 {
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
rl.tcUsedY += g.MaxLength
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += len(g.Suggestions)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += len(g.Suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getListPad() (pad int) {
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
if group.DisplayType == TabDisplayList {
|
||||||
|
for i := range group.Suggestions {
|
||||||
|
if len(group.Suggestions[i]) > pad {
|
||||||
|
pad = len([]rune(group.Suggestions[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initMap - Map display details. Called each time we want to be sure to have
|
||||||
|
// a working completion group either immediately, or later on. Generally defered.
|
||||||
|
func (g *CompletionGroup) initMap(rl *Instance) {
|
||||||
|
|
||||||
|
// We make the map anyway, especially if we need to use it later
|
||||||
|
if g.Descriptions == nil {
|
||||||
|
g.Descriptions = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute size of each completion item box. Group independent
|
||||||
|
g.tcMaxLength = 1
|
||||||
|
for i := range g.Suggestions {
|
||||||
|
if len(g.Descriptions[g.Suggestions[i]]) > g.tcMaxLength {
|
||||||
|
g.tcMaxLength = len(g.Descriptions[g.Suggestions[i]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.tcPosX = 0
|
||||||
|
g.tcPosY = 0
|
||||||
|
g.tcOffset = 0
|
||||||
|
|
||||||
|
// Number of lines allowed to be printed for group
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
g.tcMaxY = g.MaxLength
|
||||||
|
} else {
|
||||||
|
g.tcMaxY = len(g.Suggestions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveTabMapHighlight - Moves the highlighting for currently selected completion item (map display)
|
||||||
|
func (g *CompletionGroup) moveTabMapHighlight(rl *Instance, x, y int) (done bool, next bool) {
|
||||||
|
|
||||||
|
g.tcPosY += x
|
||||||
|
g.tcPosY += y
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
if g.tcPosY < 1 {
|
||||||
|
if rl.tabCompletionReverse {
|
||||||
|
if g.tcOffset > 0 {
|
||||||
|
g.tcPosY = 1
|
||||||
|
g.tcOffset--
|
||||||
|
} else {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if g.tcPosY > g.tcMaxY {
|
||||||
|
g.tcPosY--
|
||||||
|
g.tcOffset++
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.tcOffset+g.tcPosY < 1 && len(g.Suggestions) > 0 {
|
||||||
|
g.tcPosY = g.tcMaxY
|
||||||
|
g.tcOffset = len(g.Suggestions) - g.tcMaxY
|
||||||
|
}
|
||||||
|
if g.tcOffset < 0 {
|
||||||
|
g.tcOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.tcOffset+g.tcPosY > len(g.Suggestions) {
|
||||||
|
g.tcOffset--
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMap - A map or list completion string
|
||||||
|
func (g *CompletionGroup) writeMap(rl *Instance) (comp string) {
|
||||||
|
|
||||||
|
if g.Name != "" {
|
||||||
|
// Print group title (changes with line returns depending on type)
|
||||||
|
comp += fmt.Sprintf("%s%s%s %s\n", BOLD, YELLOW, g.Name, RESET)
|
||||||
|
rl.tcUsedY++
|
||||||
|
}
|
||||||
|
|
||||||
|
termWidth := GetTermWidth()
|
||||||
|
if termWidth < 20 {
|
||||||
|
// terminal too small. Probably better we do nothing instead of crash
|
||||||
|
// We are more conservative than lmorg, and push it to 20 instead of 10
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all necessary dimensions
|
||||||
|
maxLength := g.tcMaxLength
|
||||||
|
if maxLength > termWidth-9 {
|
||||||
|
maxLength = termWidth - 9
|
||||||
|
}
|
||||||
|
maxDescWidth := termWidth - maxLength - 4
|
||||||
|
|
||||||
|
cellWidth := strconv.Itoa(maxLength)
|
||||||
|
itemWidth := strconv.Itoa(maxDescWidth)
|
||||||
|
y := 0
|
||||||
|
|
||||||
|
// Highlighting function
|
||||||
|
highlight := func(y int) string {
|
||||||
|
if y == g.tcPosY && g.isCurrent {
|
||||||
|
return seqCtermFg255 + seqFgBlackBright
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String formating
|
||||||
|
var item, description string
|
||||||
|
for i := g.tcOffset; i < len(g.Suggestions); i++ {
|
||||||
|
y++ // Consider new item
|
||||||
|
if y > g.tcMaxY {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
item = g.Suggestions[i]
|
||||||
|
|
||||||
|
if len(item) > maxDescWidth {
|
||||||
|
item = item[:maxDescWidth-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
description = g.Descriptions[g.Suggestions[i]]
|
||||||
|
if len(description) > maxLength {
|
||||||
|
description = description[:maxLength-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
comp += fmt.Sprintf("\r%-"+cellWidth+"s %s %-"+itemWidth+"s %s\n",
|
||||||
|
description, highlight(y), item, seqReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the equivalent of this group's size to final screen clearing
|
||||||
|
if len(g.Suggestions) > g.MaxLength {
|
||||||
|
rl.tcUsedY += g.MaxLength
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += len(g.Suggestions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompleteCommandArguments - Completes all values for arguments to a command.
|
||||||
|
// Arguments here are different from command options (--option).
|
||||||
|
// Many categories, from multiple sources in multiple contexts
|
||||||
|
func completeCommandArguments(cmd *flags.Command, arg string, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// the prefix is the last word, by default
|
||||||
|
prefix = lastWord
|
||||||
|
|
||||||
|
// SEE completeOptionArguments FOR A WAY TO ADD COMPLETIONS TO SPECIFIC ARGUMENTS ------------------------------
|
||||||
|
|
||||||
|
// found := argumentByName(cmd, arg)
|
||||||
|
// var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// completeEnvironmentVariables - Returns all environment variables as suggestions
|
||||||
|
func completeEnvironmentVariables(lastWord string) (last string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// Check if last input is made of several different variables
|
||||||
|
allVars := strings.Split(lastWord, "/")
|
||||||
|
lastVar := allVars[len(allVars)-1]
|
||||||
|
|
||||||
|
var evaluated = map[string]string{}
|
||||||
|
|
||||||
|
grp := &readline.CompletionGroup{
|
||||||
|
Name: "console OS environment",
|
||||||
|
MaxLength: 5, // Should be plenty enough
|
||||||
|
DisplayType: readline.TabDisplayGrid,
|
||||||
|
TrimSlash: true, // Some variables can be paths
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range clientEnv {
|
||||||
|
if strings.HasPrefix("$"+k, lastVar) {
|
||||||
|
grp.Suggestions = append(grp.Suggestions, "$"+k+"/")
|
||||||
|
evaluated[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completions = append(completions, grp)
|
||||||
|
|
||||||
|
return lastVar, completions
|
||||||
|
}
|
||||||
|
|
||||||
|
// clientEnv - Contains all OS environment variables, client-side.
|
||||||
|
// This is used for things like downloading/uploading files from localhost, etc.,
|
||||||
|
// therefore we need completion and parsing stuff, sometimes.
|
||||||
|
var clientEnv = map[string]string{}
|
||||||
|
|
||||||
|
// ParseEnvironmentVariables - Parses a line of input and replace detected environment variables with their values.
|
||||||
|
func ParseEnvironmentVariables(args []string) (processed []string, err error) {
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
|
||||||
|
// Anywhere a $ is assigned means there is an env variable
|
||||||
|
if strings.Contains(arg, "$") || strings.Contains(arg, "~") {
|
||||||
|
|
||||||
|
//Split in case env is embedded in path
|
||||||
|
envArgs := strings.Split(arg, "/")
|
||||||
|
|
||||||
|
// If its not a path
|
||||||
|
if len(envArgs) == 1 {
|
||||||
|
processed = append(processed, handleCuratedVar(arg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If len of the env var split is > 1, its a path
|
||||||
|
if len(envArgs) > 1 {
|
||||||
|
processed = append(processed, handleEmbeddedVar(arg))
|
||||||
|
}
|
||||||
|
} else if arg != "" && arg != " " {
|
||||||
|
// Else, if arg is not an environment variable, return it as is
|
||||||
|
processed = append(processed, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCuratedVar - Replace an environment variable alone and without any undesired characters attached
|
||||||
|
func handleCuratedVar(arg string) (value string) {
|
||||||
|
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||||
|
envVar := strings.TrimPrefix(arg, "$")
|
||||||
|
val, ok := clientEnv[envVar]
|
||||||
|
if !ok {
|
||||||
|
return envVar
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
if arg != "" && arg == "~" {
|
||||||
|
return clientEnv["HOME"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEmbeddedVar - Replace an environment variable that is in the middle of a path, or other one-string combination
|
||||||
|
func handleEmbeddedVar(arg string) (value string) {
|
||||||
|
|
||||||
|
envArgs := strings.Split(arg, "/")
|
||||||
|
var path []string
|
||||||
|
|
||||||
|
for _, arg := range envArgs {
|
||||||
|
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||||
|
envVar := strings.TrimPrefix(arg, "$")
|
||||||
|
val, ok := clientEnv[envVar]
|
||||||
|
if !ok {
|
||||||
|
// Err will be caught when command is ran anyway, or completion will stop...
|
||||||
|
path = append(path, arg)
|
||||||
|
}
|
||||||
|
path = append(path, val)
|
||||||
|
} else if arg != "" && arg == "~" {
|
||||||
|
path = append(path, clientEnv["HOME"])
|
||||||
|
} else if arg != " " && arg != "" {
|
||||||
|
path = append(path, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadClientEnv - Loads all user environment variables
|
||||||
|
func loadClientEnv() error {
|
||||||
|
env := os.Environ()
|
||||||
|
|
||||||
|
for _, kv := range env {
|
||||||
|
key := strings.Split(kv, "=")[0]
|
||||||
|
value := strings.Split(kv, "=")[1]
|
||||||
|
clientEnv[key] = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HintCompleter - Entrypoint to all hints in the Wiregost console
|
||||||
|
func (c *CommandCompleter) HintCompleter(line []rune, pos int) (hint []rune) {
|
||||||
|
|
||||||
|
// Format and sanitize input
|
||||||
|
// @args => All items of the input line
|
||||||
|
// @last => The last word detected in input line as []rune
|
||||||
|
// @lastWord => The last word detected in input as string
|
||||||
|
args, last, lastWord := formatInput(line)
|
||||||
|
|
||||||
|
// Detect base command automatically
|
||||||
|
var command = c.detectedCommand(args)
|
||||||
|
|
||||||
|
// Menu hints (command line is empty, or nothing recognized)
|
||||||
|
if noCommandOrEmpty(args, last, command) {
|
||||||
|
hint = MenuHint(args, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variables
|
||||||
|
if envVarAsked(args, lastWord) {
|
||||||
|
return envVarHint(args, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command Hint
|
||||||
|
if commandFound(command) {
|
||||||
|
|
||||||
|
// Command hint by default (no space between cursor and last command character)
|
||||||
|
hint = CommandHint(command)
|
||||||
|
|
||||||
|
// Check environment variables
|
||||||
|
if envVarAsked(args, lastWord) {
|
||||||
|
return envVarHint(args, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If options are asked for root command, return commpletions.
|
||||||
|
if len(command.Groups()) > 0 {
|
||||||
|
for _, grp := range command.Groups() {
|
||||||
|
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||||
|
hint = OptionArgumentHint(args, last, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If command has args, hint for args
|
||||||
|
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||||
|
hint = []rune(CommandArgumentHints(args, last, command, arg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brief subcommand hint
|
||||||
|
if lastIsSubCommand(lastWord, command) {
|
||||||
|
hint = []rune(commandHint + command.Find(string(last)).ShortDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle subcommand if found
|
||||||
|
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||||
|
return HandleSubcommandHints(args, last, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle system binaries, shell commands, etc...
|
||||||
|
if commandFoundInPath(args[0]) {
|
||||||
|
// hint = []rune(exeHint + util.ParseSummary(util.GetManPages(args[0])))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandHint - Yields the hint of a Wiregost command
|
||||||
|
func CommandHint(command *flags.Command) (hint []rune) {
|
||||||
|
return []rune(commandHint + command.ShortDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSubcommandHints - Handles hints for a subcommand and its arguments, options, etc.
|
||||||
|
func HandleSubcommandHints(args []string, last []rune, command *flags.Command) (hint []rune) {
|
||||||
|
|
||||||
|
// If command has args, hint for args
|
||||||
|
if arg, yes := commandArgumentRequired(string(last), args, command); yes {
|
||||||
|
hint = []rune(CommandArgumentHints(args, last, command, arg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
if envVarAsked(args, string(last)) {
|
||||||
|
hint = envVarHint(args, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last word in input is an option --name, yield argument hint if needed
|
||||||
|
if len(command.Groups()) > 0 {
|
||||||
|
for _, grp := range command.Groups() {
|
||||||
|
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||||
|
hint = OptionArgumentHint(args, last, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user asks for completions with "-" or "--".
|
||||||
|
// (Note: This takes precedence on any argument hints, as it is evaluated after them)
|
||||||
|
if commandOptionsAsked(args, string(last), command) {
|
||||||
|
return OptionHints(args, last, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandArgumentHints - Yields hints for arguments to commands if they have some
|
||||||
|
func CommandArgumentHints(args []string, last []rune, command *flags.Command, arg string) (hint []rune) {
|
||||||
|
|
||||||
|
found := argumentByName(command, arg)
|
||||||
|
// Base Hint is just a description of the command argument
|
||||||
|
hint = []rune(argHint + found.Description)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleOptionHints - If the option being set has a description, show it
|
||||||
|
func ModuleOptionHints(opt string) (hint []rune) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionHints - Yields hints for proposed options lists/groups
|
||||||
|
func OptionHints(args []string, last []rune, command *flags.Command) (hint []rune) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionArgumentHint - Yields hints for arguments to an option (generally the last word in input)
|
||||||
|
func OptionArgumentHint(args []string, last []rune, opt *flags.Option) (hint []rune) {
|
||||||
|
return []rune(valueHint + opt.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuHint - Returns the Hint for a given menu context
|
||||||
|
func MenuHint(args []string, current []rune) (hint []rune) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpecialCommandHint - Shows hints for Wiregost special commands
|
||||||
|
func SpecialCommandHint(args []string, current []rune) (hint []rune) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
// envVarHint - Yields hints for environment variables
|
||||||
|
func envVarHint(args []string, last []rune) (hint []rune) {
|
||||||
|
// Trim last in case its a path with multiple vars
|
||||||
|
allVars := strings.Split(string(last), "/")
|
||||||
|
lastVar := allVars[len(allVars)-1]
|
||||||
|
|
||||||
|
// Base hint
|
||||||
|
hint = []rune(envHint + lastVar)
|
||||||
|
|
||||||
|
envVar := strings.TrimPrefix(lastVar, "$")
|
||||||
|
|
||||||
|
if v, ok := clientEnv[envVar]; ok {
|
||||||
|
if v != "" {
|
||||||
|
hintStr := string(hint) + " => " + clientEnv[envVar]
|
||||||
|
hint = []rune(hintStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Hint signs
|
||||||
|
menuHint = readline.RESET + readline.DIM + readline.BOLD + " menu " + readline.RESET // Dim
|
||||||
|
envHint = readline.RESET + readline.GREEN + readline.BOLD + " env " + readline.RESET + readline.DIM + readline.GREEN // Green
|
||||||
|
commandHint = readline.RESET + readline.DIM + readline.BOLD + " command " + readline.RESET + readline.DIM + "\033[38;5;244m" // Cream
|
||||||
|
exeHint = readline.RESET + readline.DIM + readline.BOLD + " shell " + readline.RESET + readline.DIM // Dim
|
||||||
|
optionHint = "\033[38;5;222m" + readline.BOLD + " options " + readline.RESET + readline.DIM + "\033[38;5;222m" // Cream-Yellow
|
||||||
|
valueHint = readline.RESET + readline.DIM + readline.BOLD + " value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||||
|
// valueHint = "\033[38;5;217m" + readline.BOLD + " Value " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||||
|
argHint = readline.DIM + "\033[38;5;217m" + readline.BOLD + " arg " + readline.RESET + readline.DIM + "\033[38;5;244m" // Pink-Cream
|
||||||
|
)
|
|
@ -0,0 +1,205 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func completeLocalPath(last string) (string, *readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// Completions
|
||||||
|
completion := &readline.CompletionGroup{
|
||||||
|
Name: "(console) local path",
|
||||||
|
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
|
||||||
|
DisplayType: readline.TabDisplayGrid,
|
||||||
|
TrimSlash: true,
|
||||||
|
}
|
||||||
|
var suggestions []string
|
||||||
|
|
||||||
|
// Any parsing error is silently ignored, for not messing the prompt
|
||||||
|
processedPath, _ := ParseEnvironmentVariables([]string{last})
|
||||||
|
|
||||||
|
// Check if processed input is empty
|
||||||
|
var inputPath string
|
||||||
|
if len(processedPath) == 1 {
|
||||||
|
inputPath = processedPath[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a slash if the raw input has one but not the processed input
|
||||||
|
if len(last) > 0 && last[len(last)-1] == '/' {
|
||||||
|
inputPath += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var linePath string // curated version of the inputPath
|
||||||
|
var absPath string // absolute path (excluding suffix) of the inputPath
|
||||||
|
var lastPath string // last directory in the input path
|
||||||
|
|
||||||
|
if strings.HasSuffix(string(inputPath), "/") {
|
||||||
|
linePath = filepath.Dir(string(inputPath))
|
||||||
|
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||||
|
|
||||||
|
} else if string(inputPath) == "" {
|
||||||
|
linePath = "."
|
||||||
|
absPath, _ = expand(string(linePath))
|
||||||
|
} else {
|
||||||
|
linePath = filepath.Dir(string(inputPath))
|
||||||
|
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||||
|
lastPath = filepath.Base(string(inputPath)) // Save filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) We take the absolute path we found, and get all dirs in it.
|
||||||
|
var dirs []string
|
||||||
|
files, _ := ioutil.ReadDir(absPath)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
dirs = append(dirs, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch lastPath {
|
||||||
|
case "":
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if strings.HasPrefix(dir, lastPath) || lastPath == dir {
|
||||||
|
tokenized := addSpaceTokens(dir)
|
||||||
|
suggestions = append(suggestions, tokenized+"/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
filtered := []string{}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if strings.HasPrefix(dir, lastPath) {
|
||||||
|
filtered = append(filtered, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range filtered {
|
||||||
|
if !hasPrefix([]rune(lastPath), []rune(dir)) || lastPath == dir {
|
||||||
|
tokenized := addSpaceTokens(dir)
|
||||||
|
suggestions = append(suggestions, tokenized+"/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
completion.Suggestions = suggestions
|
||||||
|
return string(lastPath), completion
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSpaceTokens(in string) (path string) {
|
||||||
|
items := strings.Split(in, " ")
|
||||||
|
for i := range items {
|
||||||
|
if len(items) == i+1 { // If last one, no char, add and return
|
||||||
|
path += items[i]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path += items[i] + "\\ " // By default add space char and roll
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeLocalPathAndFiles(last string) (string, *readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// Completions
|
||||||
|
completion := &readline.CompletionGroup{
|
||||||
|
Name: "(console) local directory/files",
|
||||||
|
MaxLength: 10, // The grid system is not yet able to roll on comps if > MaxLength
|
||||||
|
DisplayType: readline.TabDisplayGrid,
|
||||||
|
TrimSlash: true,
|
||||||
|
}
|
||||||
|
var suggestions []string
|
||||||
|
|
||||||
|
// Any parsing error is silently ignored, for not messing the prompt
|
||||||
|
processedPath, _ := ParseEnvironmentVariables([]string{last})
|
||||||
|
|
||||||
|
// Check if processed input is empty
|
||||||
|
var inputPath string
|
||||||
|
if len(processedPath) == 1 {
|
||||||
|
inputPath = processedPath[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a slash if the raw input has one but not the processed input
|
||||||
|
if len(last) > 0 && last[len(last)-1] == '/' {
|
||||||
|
inputPath += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var linePath string // curated version of the inputPath
|
||||||
|
var absPath string // absolute path (excluding suffix) of the inputPath
|
||||||
|
var lastPath string // last directory in the input path
|
||||||
|
|
||||||
|
if strings.HasSuffix(string(inputPath), "/") {
|
||||||
|
linePath = filepath.Dir(string(inputPath)) // Trim the non needed slash
|
||||||
|
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||||
|
|
||||||
|
} else if string(inputPath) == "" {
|
||||||
|
linePath = "."
|
||||||
|
absPath, _ = expand(string(linePath))
|
||||||
|
} else {
|
||||||
|
linePath = filepath.Dir(string(inputPath))
|
||||||
|
absPath, _ = expand(string(linePath)) // Get absolute path
|
||||||
|
lastPath = filepath.Base(string(inputPath)) // Save filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) We take the absolute path we found, and get all dirs in it.
|
||||||
|
var dirs []string
|
||||||
|
files, _ := ioutil.ReadDir(absPath)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
dirs = append(dirs, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch lastPath {
|
||||||
|
case "":
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), lastPath) || lastPath == file.Name() {
|
||||||
|
if file.IsDir() {
|
||||||
|
suggestions = append(suggestions, file.Name()+"/")
|
||||||
|
} else {
|
||||||
|
suggestions = append(suggestions, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
filtered := []os.FileInfo{}
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), lastPath) {
|
||||||
|
filtered = append(filtered, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range filtered {
|
||||||
|
if !hasPrefix([]rune(lastPath), []rune(file.Name())) || lastPath == file.Name() {
|
||||||
|
if file.IsDir() {
|
||||||
|
suggestions = append(suggestions, file.Name()+"/")
|
||||||
|
} else {
|
||||||
|
suggestions = append(suggestions, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
completion.Suggestions = suggestions
|
||||||
|
return string(lastPath), completion
|
||||||
|
}
|
||||||
|
|
||||||
|
// expand will expand a path with ~ to the $HOME of the current user.
|
||||||
|
func expand(path string) (string, error) {
|
||||||
|
if path == "" {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
home = usr.HomeDir
|
||||||
|
}
|
||||||
|
return filepath.Abs(strings.Replace(path, "~", home, 1))
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// completeOptionArguments - Completes all values for arguments to a command. Arguments here are different from command options (--option).
|
||||||
|
// Many categories, from multiple sources in multiple contexts
|
||||||
|
func completeOptionArguments(cmd *flags.Command, opt *flags.Option, lastWord string) (prefix string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// By default the last word is the prefix
|
||||||
|
prefix = lastWord
|
||||||
|
|
||||||
|
var comp *readline.CompletionGroup // This group is used as a buffer, to add groups to final completions
|
||||||
|
|
||||||
|
// First of all: some options, no matter their contexts and subject, have default values.
|
||||||
|
// When we have such an option, we don't bother analyzing context, we just build completions and return.
|
||||||
|
if len(opt.Choices) > 0 {
|
||||||
|
comp = &readline.CompletionGroup{
|
||||||
|
Name: opt.ValueName, // Value names are specified in struct metadata fields
|
||||||
|
DisplayType: readline.TabDisplayGrid,
|
||||||
|
}
|
||||||
|
for _, choice := range opt.Choices {
|
||||||
|
if strings.HasPrefix(choice, lastWord) {
|
||||||
|
comp.Suggestions = append(comp.Suggestions, choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completions = append(completions, comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXAMPLE OF COMPLETING ARGUMENTS BASED ON THEIR NAMES -----------------------------------------------------------------------
|
||||||
|
// We have 3 words, potentially different, with which we can filter:
|
||||||
|
//
|
||||||
|
// 1) '--option-name' is the string typed as input.
|
||||||
|
// 2) 'OptionName' is the name of the struct/type for this option.
|
||||||
|
// 3) 'ValueName' is the name of the value we expect.
|
||||||
|
// var match = func(name string) bool {
|
||||||
|
// if strings.Contains(opt.Field().Name, name) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Sessions
|
||||||
|
// if match("ImplantID") || match("SessionID") {
|
||||||
|
// completions = append(completions, sessionIDs(lastWord))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Any arguments with a path name. Often we "save" files that need paths, certificates, etc
|
||||||
|
// if match("Path") || match("Save") || match("Certificate") || match("PrivateKey") {
|
||||||
|
// switch cmd.Name {
|
||||||
|
// case constants.WebContentTypeStr, constants.WebUpdateStr, constants.AddWebContentStr, constants.RmWebContentStr:
|
||||||
|
// // Make an exception for WebPath option in websites commands.
|
||||||
|
// default:
|
||||||
|
// switch opt.ValueName {
|
||||||
|
// case "local-path", "path":
|
||||||
|
// prefix, comp = completeLocalPath(lastWord)
|
||||||
|
// completions = append(completions, comp)
|
||||||
|
// case "local-file", "file":
|
||||||
|
// prefix, comp = completeLocalPathAndFiles(lastWord)
|
||||||
|
// completions = append(completions, comp)
|
||||||
|
// default:
|
||||||
|
// // We always have a default searching for files, locally
|
||||||
|
// prefix, comp = completeLocalPathAndFiles(lastWord)
|
||||||
|
// completions = append(completions, comp)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,548 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These functions are just shorthands for checking various conditions on the input line.
|
||||||
|
// They make the main function more readable, which might be useful, should a logic error pop somewhere.
|
||||||
|
|
||||||
|
// [ Parser Commands & Options ] --------------------------------------------------------------------------
|
||||||
|
// ArgumentByName Get the name of a detected command's argument
|
||||||
|
func argumentByName(command *flags.Command, name string) *flags.Arg {
|
||||||
|
args := command.Args()
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg.Name == name {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionByName - Returns an option for a command or a subcommand, identified by name
|
||||||
|
func optionByName(cmd *flags.Command, option string) *flags.Option {
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Get all (root) option groups.
|
||||||
|
groups := cmd.Groups()
|
||||||
|
|
||||||
|
// For each group, build completions
|
||||||
|
for _, grp := range groups {
|
||||||
|
// Add each option to completion group
|
||||||
|
for _, opt := range grp.Options() {
|
||||||
|
if opt.LongName == option {
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Menus ] --------------------------------------------------------------------------------------------
|
||||||
|
// Is the input line is either empty, or without any detected command ?
|
||||||
|
func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool {
|
||||||
|
if len(args) == 0 || len(args) == 1 && command == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Commands ] -------------------------------------------------------------------------------------
|
||||||
|
// detectedCommand - Returns the base command from parser if detected, depending on context
|
||||||
|
func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) {
|
||||||
|
arg := strings.TrimSpace(args[0])
|
||||||
|
command = c.parser.Find(arg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the command a special command, usually not handled by parser ?
|
||||||
|
func isSpecialCommand(args []string, command *flags.Command) bool {
|
||||||
|
|
||||||
|
// If command is not nil, return
|
||||||
|
if command == nil {
|
||||||
|
// Shell
|
||||||
|
if args[0] == "!" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Exit
|
||||||
|
if args[0] == "exit" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The commmand has been found
|
||||||
|
func commandFound(command *flags.Command) bool {
|
||||||
|
if command != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for input in $PATH
|
||||||
|
func commandFoundInPath(input string) bool {
|
||||||
|
_, err := exec.LookPath(input)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ SubCommands ]-------------------------------------------------------------------------------------
|
||||||
|
// Does the command have subcommands ?
|
||||||
|
func hasSubCommands(command *flags.Command, args []string) bool {
|
||||||
|
if len(args) < 2 || command == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(command.Commands()) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the input has a subcommand in it ?
|
||||||
|
func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) {
|
||||||
|
// First, filter redundant spaces. This does not modify the actual line
|
||||||
|
args := ignoreRedundantSpaces(raw)
|
||||||
|
|
||||||
|
if len(args) <= 1 || command == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = command.Find(args[1])
|
||||||
|
if sub != nil {
|
||||||
|
return sub, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand
|
||||||
|
func lastIsSubCommand(lastWord string, command *flags.Command) bool {
|
||||||
|
if sub := command.Find(lastWord); sub != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Arguments ]-------------------------------------------------------------------------------------
|
||||||
|
// Does the command have arguments ?
|
||||||
|
func hasArgs(command *flags.Command) bool {
|
||||||
|
if len(command.Args()) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for
|
||||||
|
func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) {
|
||||||
|
|
||||||
|
// First, filter redundant spaces. This does not modify the actual line
|
||||||
|
args := ignoreRedundantSpaces(raw)
|
||||||
|
|
||||||
|
// Trim command and subcommand args
|
||||||
|
var remain []string
|
||||||
|
if args[0] == command.Name {
|
||||||
|
remain = args[1:]
|
||||||
|
}
|
||||||
|
if len(args) > 1 && args[1] == command.Name {
|
||||||
|
remain = args[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remain may include a "" as a last element,
|
||||||
|
// which we don't consider as a real remain, so we move it away
|
||||||
|
switch lastWord {
|
||||||
|
case "":
|
||||||
|
case command.Name:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim all --option flags and their arguments if they have
|
||||||
|
remain = filterOptions(remain, command)
|
||||||
|
|
||||||
|
// For each argument, check if needs completion. If not continue, if yes return.
|
||||||
|
// The arguments remainder is popped according to the number of values expected.
|
||||||
|
for i, arg := range command.Args() {
|
||||||
|
|
||||||
|
// If it's required and has one argument, check filled.
|
||||||
|
if arg.Required == 1 && arg.RequiredMaximum == 1 {
|
||||||
|
|
||||||
|
// If last word is the argument, and we are
|
||||||
|
// last arg in: line keep completing.
|
||||||
|
if len(remain) < 1 {
|
||||||
|
return arg.Name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the we are still writing the argument
|
||||||
|
if len(remain) == 1 {
|
||||||
|
if lastWord != "" {
|
||||||
|
return arg.Name, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If filed and we are not last arg, continue
|
||||||
|
if len(remain) > 1 && i < (len(command.Args())-1) {
|
||||||
|
remain = remain[1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need more than one value and we knwo the maximum,
|
||||||
|
// either return or pop the remain.
|
||||||
|
if arg.Required > 0 && arg.RequiredMaximum > 1 {
|
||||||
|
// Pop the corresponding amount of arguments.
|
||||||
|
var found int
|
||||||
|
for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ {
|
||||||
|
remain = remain[1:]
|
||||||
|
found++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still need values:
|
||||||
|
if len(remain) == 0 && found <= arg.RequiredMaximum {
|
||||||
|
if lastWord == "" { // We are done, no more completions.
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
return arg.Name, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Else go on with the next argument
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If has required arguments, with no limit of needs, return true
|
||||||
|
if arg.Required > 0 && arg.RequiredMaximum == -1 {
|
||||||
|
return arg.Name, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, if no requirements and the command has subcommands,
|
||||||
|
// return so that we complete subcommands
|
||||||
|
if arg.Required == -1 && len(command.Commands()) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, return this argument
|
||||||
|
// NOTE: This block is after because we always use []type arguments
|
||||||
|
// AFTER individual argument fields. Thus blocks any args that have
|
||||||
|
// not been processed.
|
||||||
|
if arg.Required == -1 {
|
||||||
|
return arg.Name, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we exited the loop, it means that none of the arguments require completion:
|
||||||
|
// They are all either optional, or fullfiled according to their required numbers.
|
||||||
|
// Thus we return none
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args
|
||||||
|
func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) {
|
||||||
|
|
||||||
|
var input []string
|
||||||
|
// Clean subcommand name
|
||||||
|
if args[0] == command.Name && len(args) >= 2 {
|
||||||
|
input = args[1:]
|
||||||
|
} else if len(args) == 1 {
|
||||||
|
input = args
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each each argument
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
// Check option prefix
|
||||||
|
if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") {
|
||||||
|
// Clean it
|
||||||
|
cur := strings.TrimPrefix(input[i], "--")
|
||||||
|
cur = strings.TrimPrefix(cur, "-")
|
||||||
|
|
||||||
|
// Check if option matches any command option
|
||||||
|
if opt := command.FindOptionByLongName(cur); opt != nil {
|
||||||
|
boolean := true
|
||||||
|
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||||
|
continue // If option is boolean, don't skip an argument
|
||||||
|
}
|
||||||
|
i++ // Else skip next arg in input
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check
|
||||||
|
if input[i] == "" || input[i] == " " {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
remain = append(remain, input[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Options ]-------------------------------------------------------------------------------------
|
||||||
|
// commandOptionsAsked - Does the user asks for options in a root command ?
|
||||||
|
func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
|
||||||
|
if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandOptionsAsked - Does the user asks for options in a subcommand ?
|
||||||
|
func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
|
||||||
|
if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the last input argument is a dash ?
|
||||||
|
func isOptionDash(args []string, last []rune) bool {
|
||||||
|
if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionIsAlreadySet - Detects in input if an option is already set
|
||||||
|
func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if option type allows for repetition
|
||||||
|
func optionNotRepeatable(opt *flags.Option) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Option Values ]-------------------------------------------------------------------------------------
|
||||||
|
// Is the last input word an option name (--option) ?
|
||||||
|
func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) {
|
||||||
|
|
||||||
|
var lastItem string
|
||||||
|
var lastOption string
|
||||||
|
var option *flags.Option
|
||||||
|
|
||||||
|
// If there is argument required we must have 1) command 2) --option inputs at least.
|
||||||
|
if len(args) <= 2 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for last two arguments in input
|
||||||
|
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
|
||||||
|
|
||||||
|
// Long opts
|
||||||
|
if strings.HasPrefix(args[len(args)-2], "--") {
|
||||||
|
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
|
||||||
|
if opt := group.FindOptionByLongName(lastOption); opt != nil {
|
||||||
|
option = opt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short opts
|
||||||
|
} else if strings.HasPrefix(args[len(args)-2], "-") {
|
||||||
|
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
|
||||||
|
if len(lastOption) > 0 {
|
||||||
|
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
|
||||||
|
option = opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// If option is found, and we still are in writing the argument
|
||||||
|
if (lastItem == "" && option != nil) || option != nil {
|
||||||
|
// Check if option is a boolean, if yes return false
|
||||||
|
boolean := true
|
||||||
|
if option.Field().Type == reflect.TypeOf(boolean) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return option, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for previous argument
|
||||||
|
if lastItem != "" && option == nil {
|
||||||
|
if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {
|
||||||
|
|
||||||
|
// Long opts
|
||||||
|
if strings.HasPrefix(args[len(args)-2], "--") {
|
||||||
|
lastOption = strings.TrimPrefix(args[len(args)-2], "--")
|
||||||
|
if opt := group.FindOptionByLongName(lastOption); opt != nil {
|
||||||
|
option = opt
|
||||||
|
return option, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short opts
|
||||||
|
} else if strings.HasPrefix(args[len(args)-2], "-") {
|
||||||
|
lastOption = strings.TrimPrefix(args[len(args)-2], "-")
|
||||||
|
if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
|
||||||
|
option = opt
|
||||||
|
return option, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Other ]-------------------------------------------------------------------------------------
|
||||||
|
// Does the user asks for Environment variables ?
|
||||||
|
func envVarAsked(args []string, lastWord string) bool {
|
||||||
|
|
||||||
|
// Check if the current word is an environment variable, or if the last part of it is a variable
|
||||||
|
if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") {
|
||||||
|
if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if env var is asked in a path or something
|
||||||
|
if len(lastWord) > 1 {
|
||||||
|
// If last is a path, it cannot be an env var anymore
|
||||||
|
if lastWord[len(lastWord)-1] == '/' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastWord[len(lastWord)-1] == '$' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are at the beginning of an env var
|
||||||
|
if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterOptions - Check various elements of an option and return a list
|
||||||
|
func filterOptions(args []string, command *flags.Command) (processed []string) {
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
// --long-name options
|
||||||
|
if strings.HasPrefix(arg, "--") {
|
||||||
|
name := strings.TrimPrefix(arg, "--")
|
||||||
|
if opt := optionByName(command, name); opt != nil {
|
||||||
|
var boolean = true
|
||||||
|
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Else skip the option argument (next item)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// -s short options
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
name := strings.TrimPrefix(arg, "-")
|
||||||
|
if opt := optionByName(command, name); opt != nil {
|
||||||
|
var boolean = true
|
||||||
|
if opt.Field().Type == reflect.TypeOf(boolean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Else skip the option argument (next item)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processed = append(processed, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other Functions -------------------------------------------------------------------------------------------------------------//
|
||||||
|
|
||||||
|
// formatInput - Formats & sanitize the command line input
|
||||||
|
func formatInput(line []rune) (args []string, last []rune, lastWord string) {
|
||||||
|
args = strings.Split(string(line), " ") // The readline input as a []string
|
||||||
|
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
|
||||||
|
lastWord = string(last)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatInput - Formats & sanitize the command line input
|
||||||
|
func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) {
|
||||||
|
args = strings.SplitN(string(line), " ", -1)
|
||||||
|
last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
|
||||||
|
lastWord = string(last)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignoreRedundantSpaces - We might have several spaces between each real arguments.
|
||||||
|
// However these indivual spaces are counted as args themselves.
|
||||||
|
// For each space arg found, verify that no space args follow,
|
||||||
|
// and if some are found, delete them.
|
||||||
|
func ignoreRedundantSpaces(raw []string) (args []string) {
|
||||||
|
|
||||||
|
for i := 0; i < len(raw); i++ {
|
||||||
|
// Catch a space argument.
|
||||||
|
if raw[i] == "" {
|
||||||
|
// The arg evaulated is always kept, because we just adjusted
|
||||||
|
// the indexing to avoid the ones we don't need
|
||||||
|
// args = append(args, raw[i])
|
||||||
|
|
||||||
|
for y, next := range raw[i:] {
|
||||||
|
if next != "" {
|
||||||
|
i += y - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If we come to the end while not breaking
|
||||||
|
// we push the outer loop straight to the end.
|
||||||
|
if y == len(raw[i:])-1 {
|
||||||
|
i += y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The arg evaulated is always kept, because we just adjusted
|
||||||
|
// the indexing to avoid the ones we don't need
|
||||||
|
args = append(args, raw[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpaceLeft(in []rune) []rune {
|
||||||
|
firstIndex := len(in)
|
||||||
|
for i, r := range in {
|
||||||
|
if unicode.IsSpace(r) == false {
|
||||||
|
firstIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in[firstIndex:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func equal(a, b []rune) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPrefix(r, prefix []rune) bool {
|
||||||
|
if len(r) < len(prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return equal(r[:len(prefix)], prefix)
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyntaxHighlighter - Entrypoint to all input syntax highlighting in the Wiregost console
|
||||||
|
func (c *CommandCompleter) SyntaxHighlighter(input []rune) (line string) {
|
||||||
|
|
||||||
|
// Format and sanitize input
|
||||||
|
args, last, lastWord := formatInputHighlighter(input)
|
||||||
|
|
||||||
|
// Remain is all arguments that have not been highlighted, we need it for completing long commands
|
||||||
|
var remain = args
|
||||||
|
|
||||||
|
// Detect base command automatically
|
||||||
|
var command = c.detectedCommand(args)
|
||||||
|
|
||||||
|
// Return input as is
|
||||||
|
if noCommandOrEmpty(remain, last, command) {
|
||||||
|
return string(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base command
|
||||||
|
if commandFound(command) {
|
||||||
|
line, remain = highlightCommand(remain, command)
|
||||||
|
|
||||||
|
// SubCommand
|
||||||
|
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||||
|
line, remain = highlightSubCommand(line, remain, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
line = processRemain(line, remain)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlightCommand(args []string, command *flags.Command) (line string, remain []string) {
|
||||||
|
line = readline.BOLD + args[0] + readline.RESET + " "
|
||||||
|
remain = args[1:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlightSubCommand(input string, args []string, command *flags.Command) (line string, remain []string) {
|
||||||
|
line = input
|
||||||
|
line += readline.BOLD + args[0] + readline.RESET + " "
|
||||||
|
remain = args[1:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func processRemain(input string, remain []string) (line string) {
|
||||||
|
|
||||||
|
// Check the last is not the last space in input
|
||||||
|
if len(remain) == 1 && remain[0] == " " {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
line = input + strings.Join(remain, " ")
|
||||||
|
// line = processEnvVars(input, remain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// processEnvVars - Highlights environment variables. NOTE: Rewrite with logic from console/env.go
|
||||||
|
func processEnvVars(input string, remain []string) (line string) {
|
||||||
|
|
||||||
|
var processed []string
|
||||||
|
|
||||||
|
inputSlice := strings.Split(input, " ")
|
||||||
|
|
||||||
|
// Check already processed input
|
||||||
|
for _, arg := range inputSlice {
|
||||||
|
if arg == "" || arg == " " {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, "$") { // It is an env var.
|
||||||
|
if args := strings.Split(arg, "/"); len(args) > 1 {
|
||||||
|
for _, a := range args {
|
||||||
|
fmt.Println(a)
|
||||||
|
if strings.HasPrefix(a, "$") && a != " " { // It is an env var.
|
||||||
|
processed = append(processed, "\033[38;5;108m"+readline.DIM+a+readline.RESET)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processed = append(processed, "\033[38;5;108m"+readline.DIM+arg+readline.RESET)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processed = append(processed, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check remaining args (non-processed)
|
||||||
|
for _, arg := range remain {
|
||||||
|
if arg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, "$") && arg != "$" { // It is an env var.
|
||||||
|
var full string
|
||||||
|
args := strings.Split(arg, "/")
|
||||||
|
if len(args) == 1 {
|
||||||
|
if strings.HasPrefix(args[0], "$") && args[0] != "" && args[0] != "$" { // It is an env var.
|
||||||
|
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
var counter int
|
||||||
|
for _, arg := range args {
|
||||||
|
// If var is an env var
|
||||||
|
if strings.HasPrefix(arg, "$") && arg != "" && arg != "$" {
|
||||||
|
if counter < len(args)-1 {
|
||||||
|
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET + "/"
|
||||||
|
counter++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if counter == len(args)-1 {
|
||||||
|
full += "\033[38;5;108m" + readline.DIM + args[0] + readline.RESET
|
||||||
|
counter++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, if we are not at the end of array
|
||||||
|
if counter < len(args)-1 && arg != "" {
|
||||||
|
full += arg + "/"
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
if counter == len(args)-1 {
|
||||||
|
full += arg
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Else add first var
|
||||||
|
processed = append(processed, full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Join(processed, " ")
|
||||||
|
|
||||||
|
// Very important, keeps the line clear when erasing
|
||||||
|
// line += " "
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
package completers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order
|
||||||
|
// to build completions for commands, arguments, options and their arguments as well.
|
||||||
|
// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil.
|
||||||
|
type CommandCompleter struct {
|
||||||
|
parser *flags.Parser
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser.
|
||||||
|
func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) {
|
||||||
|
if parser == nil {
|
||||||
|
return nil, errors.New("command completer was instantiated with a nil parser")
|
||||||
|
}
|
||||||
|
return &CommandCompleter{parser: parser}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser.
|
||||||
|
func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
// Format and sanitize input
|
||||||
|
// @args => All items of the input line
|
||||||
|
// @last => The last word detected in input line as []rune
|
||||||
|
// @lastWord => The last word detected in input as string
|
||||||
|
args, last, lastWord := formatInput(line)
|
||||||
|
|
||||||
|
// Detect base command automatically
|
||||||
|
var command = c.detectedCommand(args)
|
||||||
|
|
||||||
|
// Propose commands
|
||||||
|
if noCommandOrEmpty(args, last, command) {
|
||||||
|
return c.completeMenuCommands(lastWord, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variables
|
||||||
|
if envVarAsked(args, lastWord) {
|
||||||
|
completeEnvironmentVariables(lastWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base command has been identified
|
||||||
|
if commandFound(command) {
|
||||||
|
// Check environment variables again
|
||||||
|
if envVarAsked(args, lastWord) {
|
||||||
|
return completeEnvironmentVariables(lastWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If options are asked for root command, return commpletions.
|
||||||
|
if len(command.Groups()) > 0 {
|
||||||
|
for _, grp := range command.Groups() {
|
||||||
|
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||||
|
return completeOptionArguments(command, opt, lastWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then propose subcommands. We don't return from here, otherwise it always skips the next steps.
|
||||||
|
if hasSubCommands(command, args) {
|
||||||
|
completions = completeSubCommands(args, lastWord, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle subcommand if found (maybe we should rewrite this function and use it also for base command)
|
||||||
|
if sub, ok := subCommandFound(lastWord, args, command); ok {
|
||||||
|
return handleSubCommand(line, pos, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user asks for completions with "-" / "--", show command options.
|
||||||
|
// We ask this here, after having ensured there is no subcommand invoked.
|
||||||
|
// This prevails over command arguments, even if they are required.
|
||||||
|
if commandOptionsAsked(args, lastWord, command) {
|
||||||
|
return completeCommandOptions(args, lastWord, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propose argument completion before anything, and if needed
|
||||||
|
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||||
|
return completeCommandArguments(command, arg, lastWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ Main Completion Functions ] -----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions
|
||||||
|
// Many categories, all from command parsers.
|
||||||
|
func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
|
||||||
|
|
||||||
|
// Check their namespace (which should be their "group" (like utils, core, Jobs, etc))
|
||||||
|
for _, cmd := range c.parser.Commands() {
|
||||||
|
// If command matches readline input
|
||||||
|
if strings.HasPrefix(cmd.Name, lastWord) {
|
||||||
|
// Check command group: add to existing group if found
|
||||||
|
var found bool
|
||||||
|
for _, grp := range completions {
|
||||||
|
if grp.Name == cmd.Aliases[0] {
|
||||||
|
found = true
|
||||||
|
grp.Suggestions = append(grp.Suggestions, cmd.Name)
|
||||||
|
grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add a new group if not found
|
||||||
|
if !found {
|
||||||
|
grp := &readline.CompletionGroup{
|
||||||
|
Name: cmd.Aliases[0],
|
||||||
|
Suggestions: []string{cmd.Name},
|
||||||
|
Descriptions: map[string]string{
|
||||||
|
cmd.Name: readline.Dim(cmd.ShortDescription),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions = append(completions, grp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc.
|
||||||
|
for _, grp := range completions {
|
||||||
|
// If the length of suggestions is too long and we have
|
||||||
|
// many groups, use grid display.
|
||||||
|
if len(completions) >= 10 && len(grp.Suggestions) >= 7 {
|
||||||
|
grp.DisplayType = readline.TabDisplayGrid
|
||||||
|
} else {
|
||||||
|
// By default, we use a map of command to descriptions
|
||||||
|
grp.DisplayType = readline.TabDisplayList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeSubCommands - Takes subcommands and gives them as suggestions
|
||||||
|
// One category, from one source (a parent command).
|
||||||
|
func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
group := &readline.CompletionGroup{
|
||||||
|
Name: command.Name,
|
||||||
|
Suggestions: []string{},
|
||||||
|
Descriptions: map[string]string{},
|
||||||
|
DisplayType: readline.TabDisplayList,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range command.Commands() {
|
||||||
|
if strings.HasPrefix(sub.Name, lastWord) {
|
||||||
|
group.Suggestions = append(group.Suggestions, sub.Name)
|
||||||
|
group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completions = append(completions, group)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion
|
||||||
|
// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command.
|
||||||
|
func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
args, last, lastWord := formatInput(line)
|
||||||
|
|
||||||
|
// Check environment variables
|
||||||
|
if envVarAsked(args, lastWord) {
|
||||||
|
completeEnvironmentVariables(lastWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check argument options
|
||||||
|
if len(command.Groups()) > 0 {
|
||||||
|
for _, grp := range command.Groups() {
|
||||||
|
if opt, yes := optionArgRequired(args, last, grp); yes {
|
||||||
|
return completeOptionArguments(command, opt, lastWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user asks for completions with "-" or "--". This must take precedence on arguments.
|
||||||
|
if subCommandOptionsAsked(args, lastWord, command) {
|
||||||
|
return completeCommandOptions(args, lastWord, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If command has non-filled arguments, propose them first
|
||||||
|
if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
|
||||||
|
return completeCommandArguments(command, arg, lastWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeCommandOptions - Yields completion for options of a command, with various decorators
|
||||||
|
// Many categories, from one source (a command)
|
||||||
|
func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) {
|
||||||
|
|
||||||
|
prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.
|
||||||
|
|
||||||
|
// Get all (root) option groups.
|
||||||
|
groups := cmd.Groups()
|
||||||
|
|
||||||
|
// Append command options not gathered in groups
|
||||||
|
groups = append(groups, cmd.Group)
|
||||||
|
|
||||||
|
// For each group, build completions
|
||||||
|
for _, grp := range groups {
|
||||||
|
|
||||||
|
_, comp := completeOptionGroup(lastWord, grp, "")
|
||||||
|
|
||||||
|
// No need to add empty groups, will screw the completion system.
|
||||||
|
if len(comp.Suggestions) > 0 {
|
||||||
|
completions = append(completions, comp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the same for global options, which are not part of any group "per-se"
|
||||||
|
_, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options")
|
||||||
|
if len(gcomp.Suggestions) > 0 {
|
||||||
|
completions = append(completions, gcomp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty.
|
||||||
|
func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) {
|
||||||
|
|
||||||
|
compGrp = &readline.CompletionGroup{
|
||||||
|
Name: grp.ShortDescription,
|
||||||
|
Descriptions: map[string]string{},
|
||||||
|
DisplayType: readline.TabDisplayList,
|
||||||
|
Aliases: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// An optional title for this comp group.
|
||||||
|
// Used by global flag options, added to all commands.
|
||||||
|
if title != "" {
|
||||||
|
compGrp.Name = title
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each option to completion group
|
||||||
|
for _, opt := range grp.Options() {
|
||||||
|
|
||||||
|
// Check if option is already set, next option if yes
|
||||||
|
// if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Depending on the current last word, either build a group with option longs only, or with shorts
|
||||||
|
if strings.HasPrefix("--"+opt.LongName, lastWord) {
|
||||||
|
optName := "--" + opt.LongName
|
||||||
|
compGrp.Suggestions = append(compGrp.Suggestions, optName)
|
||||||
|
|
||||||
|
// Add short if there is, and that the prefix is only one dash
|
||||||
|
if strings.HasPrefix("-", lastWord) {
|
||||||
|
if opt.ShortName != 0 {
|
||||||
|
compGrp.Aliases[optName] = "-" + string(opt.ShortName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option default value if any
|
||||||
|
var def string
|
||||||
|
if len(opt.Default) > 0 {
|
||||||
|
def = " (default:"
|
||||||
|
for _, d := range opt.Default {
|
||||||
|
def += " " + d + ","
|
||||||
|
}
|
||||||
|
def = strings.TrimSuffix(def, ",")
|
||||||
|
def += ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET)
|
||||||
|
compGrp.Descriptions[optName] = desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecursiveGroupCompletion - Handles recursive completion for nested option groups
|
||||||
|
// Many categories, one source (a command's root option group). Called by the function just above.
|
||||||
|
func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) {
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lmorg code
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func leftMost() []byte {
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
w, _, err := GetSize(fd)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{'\r', '\n'}
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, w+1)
|
||||||
|
for i := 0; i < w; i++ {
|
||||||
|
b[i] = ' '
|
||||||
|
}
|
||||||
|
b[w] = '\r'
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var rxRcvCursorPos = regexp.MustCompile("^\x1b([0-9]+);([0-9]+)R$")
|
||||||
|
|
||||||
|
func (rl *Instance) getCursorPos() (x int, y int) {
|
||||||
|
if !rl.EnableGetCursorPos {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
disable := func() (int, int) {
|
||||||
|
os.Stderr.WriteString("\r\ngetCursorPos() not supported by terminal emulator, disabling....\r\n")
|
||||||
|
rl.EnableGetCursorPos = false
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
print(seqGetCursorPos)
|
||||||
|
b := make([]byte, 64)
|
||||||
|
i, err := os.Stdin.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rxRcvCursorPos.Match(b[:i]) {
|
||||||
|
return disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
match := rxRcvCursorPos.FindAllStringSubmatch(string(b[:i]), 1)
|
||||||
|
y, err = strconv.Atoi(match[0][1])
|
||||||
|
if err != nil {
|
||||||
|
return disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
x, err = strconv.Atoi(match[0][2])
|
||||||
|
if err != nil {
|
||||||
|
return disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
return x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// DISPLAY ------------------------------------------------------------
|
||||||
|
// All cursorMoveFunctions move the cursor as it is seen by the user.
|
||||||
|
// This means that they are not used to keep any reference point when
|
||||||
|
// when we internally move around clearning and printing things
|
||||||
|
|
||||||
|
func moveCursorUp(i int) {
|
||||||
|
if i < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\x1b[%dA", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveCursorDown(i int) {
|
||||||
|
if i < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\x1b[%dB", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveCursorForwards(i int) {
|
||||||
|
if i < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\x1b[%dC", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveCursorBackwards(i int) {
|
||||||
|
if i < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\x1b[%dD", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) backspace() {
|
||||||
|
if len(rl.line) == 0 || rl.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.deleteBackspace()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) moveCursorByAdjust(adjust int) {
|
||||||
|
switch {
|
||||||
|
case adjust > 0:
|
||||||
|
rl.pos += adjust
|
||||||
|
case adjust < 0:
|
||||||
|
rl.pos += adjust
|
||||||
|
}
|
||||||
|
|
||||||
|
// The position can never be negative
|
||||||
|
if rl.pos < 0 {
|
||||||
|
rl.pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cursor can never be longer than the line
|
||||||
|
if rl.pos > len(rl.line) {
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are at the end of line, and not in Insert mode, move back one.
|
||||||
|
if rl.modeViMode != VimInsert && (rl.pos == len(rl.line)) && len(rl.line) > 0 {
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
rl.pos--
|
||||||
|
} else if rl.modeViMode == VimInsert && rl.searchMode == HistoryFind && rl.modeAutoFind {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writeTempFile - This function optionally accepts a filename (generally specified with an extension).
|
||||||
|
func (rl *Instance) writeTempFile(content []byte, filename string) (string, error) {
|
||||||
|
// The final path to the buffer on disk
|
||||||
|
var path string
|
||||||
|
|
||||||
|
// If the user has not provided any filename (including an extension)
|
||||||
|
// we generate a random filename with no extension.
|
||||||
|
if filename == "" {
|
||||||
|
fileID := strconv.Itoa(time.Now().Nanosecond()) + ":" + string(rl.line)
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
_, err := h.Write([]byte(fileID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "readline-" + hex.EncodeToString(h.Sum(nil)) + "-" + strconv.Itoa(os.Getpid())
|
||||||
|
path = filepath.Join(rl.TempDirectory, name)
|
||||||
|
} else {
|
||||||
|
// Else, still use the temp/ dir, but with the provided filename
|
||||||
|
path = filepath.Join(rl.TempDirectory, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write(content)
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTempFile(name string) ([]byte, error) {
|
||||||
|
file, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 && b[len(b)-1] == '\n' {
|
||||||
|
b = b[:len(b)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 && b[len(b)-1] == '\r' {
|
||||||
|
b = b[:len(b)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 && b[len(b)-1] == '\n' {
|
||||||
|
b = b[:len(b)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 && b[len(b)-1] == '\r' {
|
||||||
|
b = b[:len(b)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove(name)
|
||||||
|
return b, err
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// +build plan9
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func (rl *Instance) launchEditor(multiline []rune) ([]rune, error) {
|
||||||
|
return rl.line, errors.New("Not currently supported on Plan 9")
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//go:build !windows && !plan9
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultEditor = "vi"
|
||||||
|
|
||||||
|
// StartEditorWithBuffer - Enables a consumer of this console application to
|
||||||
|
// open an arbitrary buffer into the system editor. Currently only implemnted
|
||||||
|
// on *Nix systems. The modified buffer is returned when the editor quits, and
|
||||||
|
// depending on the actions taken by the user within it (eg: x or q! in Vim)
|
||||||
|
// The filename parameter can be used to pass a specific filename.ext pattern,
|
||||||
|
// which might be useful if the editor has builtin filetype plugin functionality.
|
||||||
|
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
|
||||||
|
name, err := rl.writeTempFile([]byte(string(multiline)), filename)
|
||||||
|
if err != nil {
|
||||||
|
return multiline, err
|
||||||
|
}
|
||||||
|
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
// default editor if $EDITOR not set
|
||||||
|
if editor == "" {
|
||||||
|
editor = defaultEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(editor, name)
|
||||||
|
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return multiline, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return multiline, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := readTempFile(name)
|
||||||
|
return []rune(string(b)), err
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// StartEditorWithBuffer - Not implemented on Windows platforms.
|
||||||
|
func (rl *Instance) StartEditorWithBuffer(multiline []rune, filename string) ([]rune, error) {
|
||||||
|
return rl.line, errors.New("Not currently supported on Windows")
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrCtrlC is returned when ctrl+c is pressed.
|
||||||
|
// WARNING: this is being deprecated! Please use CtrlC (type error) instead
|
||||||
|
ErrCtrlC = "Ctrl+C"
|
||||||
|
|
||||||
|
// ErrEOF is returned when ctrl+d is pressed.
|
||||||
|
// WARNING: this is being deprecated! Please use EOF (type error) instead
|
||||||
|
ErrEOF = "EOF"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// CtrlC is returned when ctrl+c is pressed
|
||||||
|
CtrlC = errors.New("Ctrl+C")
|
||||||
|
|
||||||
|
// EOF is returned when ctrl+d is pressed.
|
||||||
|
// (this is actually the same value as io.EOF)
|
||||||
|
EOF = errors.New("EOF")
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
// EventReturn is a structure returned by the callback event function.
|
||||||
|
// This is used by readline to determine what state the API should
|
||||||
|
// return to after the readline event.
|
||||||
|
type EventReturn struct {
|
||||||
|
ForwardKey bool
|
||||||
|
ClearHelpers bool
|
||||||
|
CloseReadline bool
|
||||||
|
HintText []rune
|
||||||
|
NewLine []rune
|
||||||
|
NewPos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEvent registers a new keypress handler
|
||||||
|
func (rl *Instance) AddEvent(keyPress string, callback func(string, []rune, int) *EventReturn) {
|
||||||
|
rl.evtKeyPress[keyPress] = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelEvent deregisters an existing keypress handler
|
||||||
|
func (rl *Instance) DelEvent(keyPress string) {
|
||||||
|
delete(rl.evtKeyPress, keyPress)
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// This file defines a few argument choices for commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command/option argument choices
|
||||||
|
var (
|
||||||
|
// Logs & components
|
||||||
|
logLevels = []string{"trace", "debug", "info", "warning", "error"}
|
||||||
|
loggers = []string{"client", "comm"}
|
||||||
|
|
||||||
|
// Stages / Stagers
|
||||||
|
implantOS = []string{"windows", "linux", "darwin"}
|
||||||
|
implantArch = []string{"amd64", "x86"}
|
||||||
|
implantFmt = []string{"exe", "shared", "service", "shellcode"}
|
||||||
|
|
||||||
|
stageListenerProtocols = []string{"tcp", "http", "https"}
|
||||||
|
|
||||||
|
// MSF
|
||||||
|
msfStagerProtocols = []string{"tcp", "http", "https"}
|
||||||
|
msfTransformFormats = []string{
|
||||||
|
"bash",
|
||||||
|
"c",
|
||||||
|
"csharp",
|
||||||
|
"dw",
|
||||||
|
"dword",
|
||||||
|
"hex",
|
||||||
|
"java",
|
||||||
|
"js_be",
|
||||||
|
"js_le",
|
||||||
|
"num",
|
||||||
|
"perl",
|
||||||
|
"pl",
|
||||||
|
"powershell",
|
||||||
|
"ps1",
|
||||||
|
"py",
|
||||||
|
"python",
|
||||||
|
"raw",
|
||||||
|
"rb",
|
||||||
|
"ruby",
|
||||||
|
"sh",
|
||||||
|
"vbapplication",
|
||||||
|
"vbscript",
|
||||||
|
}
|
||||||
|
|
||||||
|
msfEncoders = []string{
|
||||||
|
"x86/shikata_ga_nai",
|
||||||
|
"x64/xor_dynamic",
|
||||||
|
}
|
||||||
|
|
||||||
|
msfPayloads = map[string][]string{
|
||||||
|
"windows": windowsMsfPayloads,
|
||||||
|
"linux": linuxMsfPayloads,
|
||||||
|
"osx": osxMsfPayloads,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidPayloads - Valid payloads and OS combos
|
||||||
|
windowsMsfPayloads = []string{
|
||||||
|
"meterpreter_reverse_http",
|
||||||
|
"meterpreter_reverse_https",
|
||||||
|
"meterpreter_reverse_tcp",
|
||||||
|
"meterpreter/reverse_tcp",
|
||||||
|
"meterpreter/reverse_http",
|
||||||
|
"meterpreter/reverse_https",
|
||||||
|
}
|
||||||
|
linuxMsfPayloads = []string{
|
||||||
|
"meterpreter_reverse_http",
|
||||||
|
"meterpreter_reverse_https",
|
||||||
|
"meterpreter_reverse_tcp",
|
||||||
|
}
|
||||||
|
osxMsfPayloads = []string{
|
||||||
|
"meterpreter_reverse_http",
|
||||||
|
"meterpreter_reverse_https",
|
||||||
|
"meterpreter_reverse_tcp",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comm network protocols
|
||||||
|
portfwdProtocols = []string{"tcp", "udp"}
|
||||||
|
transportProtocols = []string{"tcp", "udp", "ip"}
|
||||||
|
applicationProtocols = []string{"http", "https", "mtls", "quic", "http3", "dns", "named_pipe"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadArgumentCompletions - Adds a bunch of choices for command arguments (and their completions.)
|
||||||
|
func loadArgumentCompletions(parser *flags.Parser) {
|
||||||
|
if parser == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverCompsAddtional(parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional completion mappings for command in the server context
|
||||||
|
func serverCompsAddtional(parser *flags.Parser) {
|
||||||
|
|
||||||
|
// Stage options
|
||||||
|
g := parser.Find("generate")
|
||||||
|
g.FindOptionByLongName("os").Choices = implantOS
|
||||||
|
g.FindOptionByLongName("arch").Choices = implantArch
|
||||||
|
g.FindOptionByLongName("format").Choices = implantFmt
|
||||||
|
|
||||||
|
// Stager options (mostly MSF)
|
||||||
|
gs := g.Find("stager")
|
||||||
|
gs.FindOptionByLongName("os").Choices = implantOS
|
||||||
|
gs.FindOptionByLongName("arch").Choices = implantArch
|
||||||
|
gs.FindOptionByLongName("protocol").Choices = msfStagerProtocols
|
||||||
|
gs.FindOptionByLongName("msf-format").Choices = msfTransformFormats
|
||||||
|
}
|
|
@ -0,0 +1,315 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file declares a go-flags parser and a few commands.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// commandParser - The command parser used by the example console.
|
||||||
|
commandParser = flags.NewNamedParser("example", flags.IgnoreUnknown)
|
||||||
|
)
|
||||||
|
|
||||||
|
func bindCommands() (err error) {
|
||||||
|
|
||||||
|
// core console
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
ex, err := commandParser.AddCommand("exit", // Command string
|
||||||
|
"Exit from the client/server console", // Description (completions, help usage)
|
||||||
|
"", // Long description
|
||||||
|
&Exit{}) // Command implementation
|
||||||
|
ex.Aliases = []string{"core"}
|
||||||
|
|
||||||
|
cd, err := commandParser.AddCommand("cd",
|
||||||
|
"Change client working directory",
|
||||||
|
"",
|
||||||
|
&ChangeClientDirectory{})
|
||||||
|
cd.Aliases = []string{"core"}
|
||||||
|
|
||||||
|
ls, err := commandParser.AddCommand("ls",
|
||||||
|
"List directory contents",
|
||||||
|
"",
|
||||||
|
&ListClientDirectories{})
|
||||||
|
ls.Aliases = []string{"core"}
|
||||||
|
|
||||||
|
// Log
|
||||||
|
log, err := commandParser.AddCommand("log",
|
||||||
|
"Manage log levels of one or more components",
|
||||||
|
"",
|
||||||
|
&Log{})
|
||||||
|
log.Aliases = []string{"core"}
|
||||||
|
|
||||||
|
// Implant generation
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
g, err := commandParser.AddCommand("generate",
|
||||||
|
"Configure and compile an implant (staged or stager)",
|
||||||
|
"",
|
||||||
|
&Generate{})
|
||||||
|
g.Aliases = []string{"builds"}
|
||||||
|
g.SubcommandsOptional = true
|
||||||
|
|
||||||
|
_, err = g.AddCommand("stager",
|
||||||
|
"Generate a stager shellcode payload using MSFVenom, (to file: --save, to stdout: --format",
|
||||||
|
"",
|
||||||
|
&GenerateStager{})
|
||||||
|
|
||||||
|
r, err := commandParser.AddCommand("regenerate",
|
||||||
|
"Recompile an implant by name, passed as argument (completed)",
|
||||||
|
"",
|
||||||
|
&Regenerate{})
|
||||||
|
r.Aliases = []string{"builds"}
|
||||||
|
|
||||||
|
// Add choices completions (and therefore completions) to some of these commands.
|
||||||
|
loadArgumentCompletions(commandParser)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit - Kill the current client console
|
||||||
|
type Exit struct{}
|
||||||
|
|
||||||
|
// Execute - Run
|
||||||
|
func (e *Exit) Execute(args []string) (err error) {
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Print("Confirm exit (Y/y): ")
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
answer := strings.TrimSpace(text)
|
||||||
|
|
||||||
|
if (answer == "Y") || (answer == "y") {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeClientDirectory - Change the working directory of the client console
|
||||||
|
type ChangeClientDirectory struct {
|
||||||
|
Positional struct {
|
||||||
|
Path string `description:"local path" required:"1-1"`
|
||||||
|
} `positional-args:"yes" required:"yes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Handler for ChangeDirectory
|
||||||
|
func (cd *ChangeClientDirectory) Execute(args []string) (err error) {
|
||||||
|
|
||||||
|
dir, err := expand(cd.Positional.Path)
|
||||||
|
|
||||||
|
err = os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(CommandError+"%s \n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(Info+"Changed directory to %s \n", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListClientDirectories - List directory contents
|
||||||
|
type ListClientDirectories struct {
|
||||||
|
Positional struct {
|
||||||
|
Path []string `description:"local directory/file"`
|
||||||
|
} `positional-args:"yes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Command
|
||||||
|
func (ls *ListClientDirectories) Execute(args []string) error {
|
||||||
|
|
||||||
|
base := []string{"ls", "--color", "-l"}
|
||||||
|
|
||||||
|
if len(ls.Positional.Path) == 0 {
|
||||||
|
ls.Positional.Path = []string{"."}
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPaths := []string{}
|
||||||
|
for _, path := range ls.Positional.Path {
|
||||||
|
full, _ := expand(path)
|
||||||
|
fullPaths = append(fullPaths, full)
|
||||||
|
}
|
||||||
|
base = append(base, fullPaths...)
|
||||||
|
|
||||||
|
// Print output
|
||||||
|
out, err := shellExec(base[0], base[1:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(CommandError+"%s \n", err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print output
|
||||||
|
fmt.Println(out)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellExec - Execute a program
|
||||||
|
func shellExec(executable string, args []string) (string, error) {
|
||||||
|
path, err := exec.LookPath(executable)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(path, args...)
|
||||||
|
|
||||||
|
// Load OS environment
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.Trim(string(out), "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate - Configure and compile an implant
|
||||||
|
type Generate struct {
|
||||||
|
StageOptions // Command makes use of full stage options
|
||||||
|
}
|
||||||
|
|
||||||
|
// StageOptions - All these options, regrouped by area, are used by any command that needs full
|
||||||
|
// configuration information for a stage Sliver implant.
|
||||||
|
type StageOptions struct {
|
||||||
|
// CoreOptions - All options about OS/arch, files to save, debugs, etc.
|
||||||
|
CoreOptions struct {
|
||||||
|
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
|
||||||
|
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
|
||||||
|
Format string `long:"format" short:"f" description:"output formats (exe, shared (DLL), service (see 'psexec' for info), shellcode (Windows only)" default:"exe" value-name:"stage formats"`
|
||||||
|
Profile string `long:"profile-name" description:"implant profile name to use (use with generate-profile)"`
|
||||||
|
Name string `long:"name" short:"N" description:"implant name to use (overrides random name generation)"`
|
||||||
|
Save string `long:"save" short:"s" description:"directory/file where to save binary"`
|
||||||
|
Debug bool `long:"debug" short:"d" description:"enable debug features (incompatible with obfuscation, and prevailing)"`
|
||||||
|
} `group:"core options"`
|
||||||
|
|
||||||
|
// TransportOptions - All options pertaining to transport/RPC matters
|
||||||
|
TransportOptions struct {
|
||||||
|
MTLS []string `long:"mtls" short:"m" description:"mTLS C2 domain(s), comma-separated (ex: mtls://host:port)" env-delim:","`
|
||||||
|
DNS []string `long:"dns" short:"n" description:"DNS C2 domain(s), comma-separated (ex: dns://mydomain.com)" env-delim:","`
|
||||||
|
HTTP []string `long:"http" short:"h" description:"HTTP(S) C2 domain(s)" env-delim:","`
|
||||||
|
NamedPipe []string `long:"named-pipe" short:"p" description:"Named pipe transport strings, comma-separated" env-delim:","`
|
||||||
|
TCPPivot []string `long:"tcp-pivot" short:"i" description:"TCP pivot transport strings, comma-separated" env-delim:","`
|
||||||
|
Reconnect int `long:"reconnect" short:"j" description:"attempt to reconnect every n second(s)" default:"60"`
|
||||||
|
MaxErrors int `long:"max-errors" short:"k" description:"max number of transport errors" default:"10"`
|
||||||
|
} `group:"transport options"`
|
||||||
|
|
||||||
|
// SecurityOptions - All security-oriented options like restrictions.
|
||||||
|
SecurityOptions struct {
|
||||||
|
LimitDatetime string `long:"limit-datetime" short:"w" description:"limit execution to before datetime"`
|
||||||
|
LimitDomain bool `long:"limit-domain-joined" short:"D" description:"limit execution to domain joined machines"`
|
||||||
|
LimitUsername string `long:"limit-username" short:"U" description:"limit execution to specified username"`
|
||||||
|
LimitHosname string `long:"limit-hostname" short:"H" description:"limit execution to specified hostname"`
|
||||||
|
LimitFileExits string `long:"limit-file-exists" short:"F" description:"limit execution to hosts with this file in the filesystem"`
|
||||||
|
} `group:"security options"`
|
||||||
|
|
||||||
|
// EvasionOptions - All proactive security options (obfuscation, evasion, etc)
|
||||||
|
EvasionOptions struct {
|
||||||
|
Canary []string `long:"canary" short:"c" description:"DNS canary domain strings, comma-separated" env-delim:","`
|
||||||
|
SkipSymbols bool `long:"skip-obfuscation" short:"b" description:"skip binary/symbol obfuscation"`
|
||||||
|
Evasion bool `long:"evasion" short:"e" description:"enable evasion features"`
|
||||||
|
} `group:"evasion options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Configure and compile an implant
|
||||||
|
func (g *Generate) Execute(args []string) (err error) {
|
||||||
|
save := g.CoreOptions.Save
|
||||||
|
if save == "" {
|
||||||
|
save, _ = os.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Executed 'generate' command. ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate - Recompile an implant by name, passed as argument (completed)
|
||||||
|
type Regenerate struct {
|
||||||
|
Positional struct {
|
||||||
|
ImplantName string `description:"Name of Sliver implant to recompile" required:"1-1"`
|
||||||
|
} `positional-args:"yes" required:"yes"`
|
||||||
|
Save string `long:"save" short:"s" description:"Directory/file where to save binary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Recompile an implant with a given profile
|
||||||
|
func (r *Regenerate) Execute(args []string) (err error) {
|
||||||
|
fmt.Println("Executed 'regenerate' command. ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateStager - Generate a stager payload using MSFVenom
|
||||||
|
type GenerateStager struct {
|
||||||
|
PayloadOptions struct {
|
||||||
|
OS string `long:"os" short:"o" description:"target host operating system" default:"windows" value-name:"stage OS"`
|
||||||
|
Arch string `long:"arch" short:"a" description:"target host CPU architecture" default:"amd64" value-name:"stage architectures"`
|
||||||
|
Format string `long:"msf-format" short:"f" description:"output format (MSF Venom formats). List is auto-completed" default:"raw" value-name:"MSF Venom transform formats"`
|
||||||
|
BadChars string `long:"badchars" short:"b" description:"bytes to exclude from stage shellcode"`
|
||||||
|
Save string `long:"save" short:"s" description:"directory to save the generated stager to"`
|
||||||
|
} `group:"payload options"`
|
||||||
|
TransportOptions struct {
|
||||||
|
LHost string `long:"lhost" short:"l" description:"listening host address" required:"true"`
|
||||||
|
LPort int `long:"lport" short:"p" description:"listening host port" default:"8443"`
|
||||||
|
Protocol string `long:"protocol" short:"P" description:"staging protocol (tcp/http/https)" default:"tcp" value-name:"stager protocol"`
|
||||||
|
} `group:"transport options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Generate a stager payload using MSFVenom
|
||||||
|
func (g *GenerateStager) Execute(args []string) (err error) {
|
||||||
|
fmt.Println("Executed 'generate stager' subcommand. ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log - Log management commands. Sets log level by default.
|
||||||
|
type Log struct {
|
||||||
|
Positional struct {
|
||||||
|
Level string `description:"log level to filter by" required:"1-1"`
|
||||||
|
Components []string `description:"components on which to apply log filter" required:"1"`
|
||||||
|
} `positional-args:"yes" required:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute - Set the log level of one or more components
|
||||||
|
func (l *Log) Execute(args []string) (err error) {
|
||||||
|
fmt.Println("Executed 'log' command. ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Info = fmt.Sprintf("%s[-]%s ", readline.BLUE, readline.RESET)
|
||||||
|
Warn = fmt.Sprintf("%s[!]%s ", readline.YELLOW, readline.RESET)
|
||||||
|
Error = fmt.Sprintf("%s[!]%s ", readline.RED, readline.RESET)
|
||||||
|
Success = fmt.Sprintf("%s[*]%s ", readline.GREEN, readline.RESET)
|
||||||
|
|
||||||
|
Infof = fmt.Sprintf("%s[-] ", readline.BLUE) // Infof - formatted
|
||||||
|
Warnf = fmt.Sprintf("%s[!] ", readline.YELLOW) // Warnf - formatted
|
||||||
|
Errorf = fmt.Sprintf("%s[!] ", readline.RED) // Errorf - formatted
|
||||||
|
Sucessf = fmt.Sprintf("%s[*] ", readline.GREEN) // Sucessf - formatted
|
||||||
|
|
||||||
|
RPCError = fmt.Sprintf("%s[RPC Error]%s ", readline.RED, readline.RESET)
|
||||||
|
CommandError = fmt.Sprintf("%s[Command Error]%s ", readline.RED, readline.RESET)
|
||||||
|
ParserError = fmt.Sprintf("%s[Parser Error]%s ", readline.RED, readline.RESET)
|
||||||
|
DBError = fmt.Sprintf("%s[DB Error]%s ", readline.RED, readline.RESET)
|
||||||
|
)
|
||||||
|
|
||||||
|
// expand will expand a path with ~ to the $HOME of the current user.
|
||||||
|
func expand(path string) (string, error) {
|
||||||
|
if path == "" {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
home = usr.HomeDir
|
||||||
|
}
|
||||||
|
return filepath.Abs(strings.Replace(path, "~", home, 1))
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
"github.com/maxlandon/readline/completers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file shows a typical way of using readline in a loop.
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Instantiate a console object
|
||||||
|
console := newConsole()
|
||||||
|
|
||||||
|
// Bind commands to the console
|
||||||
|
bindCommands()
|
||||||
|
|
||||||
|
// Setup the console completers, prompts, and input modes
|
||||||
|
console.setup()
|
||||||
|
|
||||||
|
// Start the readline loop (blocking)
|
||||||
|
console.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConsole - Instantiates a new console with some default behavior.
|
||||||
|
// We modify/add elements of behavior later in setup.
|
||||||
|
func newConsole() *console {
|
||||||
|
console := &console{
|
||||||
|
shell: readline.NewInstance(),
|
||||||
|
parser: commandParser,
|
||||||
|
}
|
||||||
|
return console
|
||||||
|
}
|
||||||
|
|
||||||
|
// console - A simple console example.
|
||||||
|
type console struct {
|
||||||
|
shell *readline.Instance
|
||||||
|
parser *flags.Parser
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup - The console sets up various elements such as the completion system, hints,
|
||||||
|
// syntax highlighting, prompt system, commands binding, and client environment loading.
|
||||||
|
func (c *console) setup() (err error) {
|
||||||
|
|
||||||
|
// Input mode & defails
|
||||||
|
c.shell.InputMode = readline.Vim // Could be readline.Emacs for emacs input mode.
|
||||||
|
c.shell.ShowVimMode = true
|
||||||
|
c.shell.VimModeColorize = true
|
||||||
|
|
||||||
|
// Prompt: we want a two-line prompt, with a custom indicator after the Vim status
|
||||||
|
c.shell.SetPrompt("readline ")
|
||||||
|
c.shell.Multiline = true
|
||||||
|
c.shell.MultilinePrompt = " > "
|
||||||
|
|
||||||
|
// Instantiate a default completer associated with the parser
|
||||||
|
// declared in commands.go, and embedded into the console struct.
|
||||||
|
// The error is muted, because we don't pass an nil parser, therefore no problems.
|
||||||
|
defaultCompleter, _ := completers.NewCommandCompleter(c.parser)
|
||||||
|
|
||||||
|
// Register the completer for command/option completions, hints and syntax highlighting.
|
||||||
|
// The completer can handle all of them.
|
||||||
|
c.shell.TabCompleter = defaultCompleter.TabCompleter
|
||||||
|
c.shell.HintText = defaultCompleter.HintCompleter
|
||||||
|
c.shell.SyntaxHighlighter = defaultCompleter.SyntaxHighlighter
|
||||||
|
|
||||||
|
// History: by default the history is in-memory, use it with Ctrl-R
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start - The console has a working RPC connection: we setup all
|
||||||
|
// things pertaining to the console itself, and start the input loop.
|
||||||
|
func (c *console) Start() (err error) {
|
||||||
|
|
||||||
|
// Setup console elements
|
||||||
|
err = c.setup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Console setup failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start input loop
|
||||||
|
for {
|
||||||
|
// Read input line
|
||||||
|
line, _ := c.Readline()
|
||||||
|
|
||||||
|
// Split and sanitize input
|
||||||
|
sanitized, empty := sanitizeInput(line)
|
||||||
|
if empty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process various tokens on input (environment variables, paths, etc.)
|
||||||
|
// These tokens will be expaneded by completers anyway, so this is not absolutely required.
|
||||||
|
envParsed, _ := completers.ParseEnvironmentVariables(sanitized)
|
||||||
|
|
||||||
|
// Other types of tokens, needed by commands who expect a certain type
|
||||||
|
// of arguments, such as paths with spaces.
|
||||||
|
tokenParsed := c.parseTokens(envParsed)
|
||||||
|
|
||||||
|
// Execute the command and print any errors
|
||||||
|
if _, parserErr := c.parser.ParseArgs(tokenParsed); parserErr != nil {
|
||||||
|
fmt.Println(readline.RED + "[Error] " + readline.RESET + parserErr.Error() + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readline - Add an empty line between input line and command output.
|
||||||
|
func (c *console) Readline() (line string, err error) {
|
||||||
|
line, err = c.shell.Readline()
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeInput - Trims spaces and other unwished elements from the input line.
|
||||||
|
func sanitizeInput(line string) (sanitized []string, empty bool) {
|
||||||
|
|
||||||
|
// Assume the input is not empty
|
||||||
|
empty = false
|
||||||
|
|
||||||
|
// Trim border spaces
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if len(line) < 1 {
|
||||||
|
empty = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unfiltered := strings.Split(trimmed, " ")
|
||||||
|
|
||||||
|
// Catch any eventual empty items
|
||||||
|
for _, arg := range unfiltered {
|
||||||
|
if arg != "" {
|
||||||
|
sanitized = append(sanitized, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTokens - Parse and process any special tokens that are not treated by environment-like parsers.
|
||||||
|
func (c *console) parseTokens(sanitized []string) (parsed []string) {
|
||||||
|
|
||||||
|
// PATH SPACE TOKENS
|
||||||
|
// Catch \ tokens, which have been introduced in paths where some directories have spaces in name.
|
||||||
|
// For each of these splits, we concatenate them with the next string.
|
||||||
|
// This will also inspect commands/options/arguments, but there is no reason why a backlash should be present in them.
|
||||||
|
var pathAdjusted []string
|
||||||
|
var roll bool
|
||||||
|
var arg string
|
||||||
|
for i := range sanitized {
|
||||||
|
if strings.HasSuffix(sanitized[i], "\\") {
|
||||||
|
// If we find a suffix, replace with a space. Go on with next input
|
||||||
|
arg += strings.TrimSuffix(sanitized[i], "\\") + " "
|
||||||
|
roll = true
|
||||||
|
} else if roll {
|
||||||
|
// No suffix but part of previous input. Add it and go on.
|
||||||
|
arg += sanitized[i]
|
||||||
|
pathAdjusted = append(pathAdjusted, arg)
|
||||||
|
arg = ""
|
||||||
|
roll = false
|
||||||
|
} else {
|
||||||
|
// Default, we add our path and go on.
|
||||||
|
pathAdjusted = append(pathAdjusted, sanitized[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsed = pathAdjusted
|
||||||
|
|
||||||
|
// Add new function here, act on parsed []string from now on, not sanitized
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
module github.com/maxlandon/readline
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||||
|
github.com/evilsocket/islazy v1.10.6
|
||||||
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
|
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
|
||||||
|
github.com/rivo/uniseg v0.2.0
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4
|
||||||
|
)
|
|
@ -0,0 +1,12 @@
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||||
|
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
|
||||||
|
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw=
|
||||||
|
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Package readline is a pure-Go re-imagining of the UNIX readline API
|
||||||
|
//
|
||||||
|
// This package is designed to be run independently from murex and at some
|
||||||
|
// point it will be separated into it's own git repository (at a stage when I
|
||||||
|
// am confident that murex will no longer be the primary driver for features,
|
||||||
|
// bugs or other code changes)
|
||||||
|
package readline
|
|
@ -0,0 +1,56 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// SetHintText - a nasty function to force writing a new hint text. It does not update helpers, it just renders
|
||||||
|
// them, so the hint will survive until the helpers (thus including the hint) will be updated/recomputed.
|
||||||
|
func (rl *Instance) SetHintText(s string) {
|
||||||
|
rl.hintText = []rune(s)
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getHintText() {
|
||||||
|
|
||||||
|
if !rl.modeAutoFind && !rl.modeTabFind {
|
||||||
|
// Return if no hints provided by the user/engine
|
||||||
|
if rl.HintText == nil {
|
||||||
|
rl.resetHintText()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The hint text also works with the virtual completion line system.
|
||||||
|
// This way, the hint is also refreshed depending on what we are pointing
|
||||||
|
// at with our cursor.
|
||||||
|
rl.hintText = rl.HintText(rl.getCompletionLine())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHintText - only writes the hint text and computes its offsets.
|
||||||
|
func (rl *Instance) writeHintText() {
|
||||||
|
if len(rl.hintText) == 0 {
|
||||||
|
rl.hintY = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
width := GetTermWidth()
|
||||||
|
|
||||||
|
// Wraps the line, and counts the number of newlines in the string,
|
||||||
|
// adjusting the offset as well.
|
||||||
|
re := regexp.MustCompile(`\r?\n`)
|
||||||
|
newlines := re.Split(string(rl.hintText), -1)
|
||||||
|
offset := len(newlines)
|
||||||
|
|
||||||
|
wrapped, hintLen := WrapText(string(rl.hintText), width)
|
||||||
|
offset += hintLen
|
||||||
|
rl.hintY = offset
|
||||||
|
|
||||||
|
hintText := string(wrapped)
|
||||||
|
|
||||||
|
if len(hintText) > 0 {
|
||||||
|
print("\r" + rl.HintFormatting + string(hintText) + seqReset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) resetHintText() {
|
||||||
|
rl.hintY = 0
|
||||||
|
rl.hintText = []rune{}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// History is an interface to allow you to write your own history logging
|
||||||
|
// tools. eg sqlite backend instead of a file system.
|
||||||
|
// By default readline will just use the dummyLineHistory interface which only
|
||||||
|
// logs the history to memory ([]string to be precise).
|
||||||
|
type History interface {
|
||||||
|
// Append takes the line and returns an updated number of lines or an error
|
||||||
|
Write(string) (int, error)
|
||||||
|
|
||||||
|
// GetLine takes the historic line number and returns the line or an error
|
||||||
|
GetLine(int) (string, error)
|
||||||
|
|
||||||
|
// Len returns the number of history lines
|
||||||
|
Len() int
|
||||||
|
|
||||||
|
// Dump returns everything in readline. The return is an interface{} because
|
||||||
|
// not all LineHistory implementations will want to structure the history in
|
||||||
|
// the same way. And since Dump() is not actually used by the readline API
|
||||||
|
// internally, this methods return can be structured in whichever way is most
|
||||||
|
// convenient for your own applications (or even just create an empty
|
||||||
|
//function which returns `nil` if you don't require Dump() either)
|
||||||
|
Dump() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHistoryCtrlR - Set the history source triggered with Ctrl-r combination
|
||||||
|
func (rl *Instance) SetHistoryCtrlR(name string, history History) {
|
||||||
|
rl.mainHistName = name
|
||||||
|
rl.mainHistory = history
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHistoryCtrlR - Returns the history source triggered by Ctrl-r
|
||||||
|
func (rl *Instance) GetHistoryCtrlR() History {
|
||||||
|
return rl.mainHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHistoryAltR - Set the history source triggered with Alt-r combination
|
||||||
|
func (rl *Instance) SetHistoryAltR(name string, history History) {
|
||||||
|
rl.altHistName = name
|
||||||
|
rl.altHistory = history
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHistoryAltR - Returns the history source triggered by Alt-r
|
||||||
|
func (rl *Instance) GetHistoryAltR() History {
|
||||||
|
return rl.altHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleHistory is an example of a LineHistory interface:
|
||||||
|
type ExampleHistory struct {
|
||||||
|
items []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to history
|
||||||
|
func (h *ExampleHistory) Write(s string) (int, error) {
|
||||||
|
h.items = append(h.items, s)
|
||||||
|
return len(h.items), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLine returns a line from history
|
||||||
|
func (h *ExampleHistory) GetLine(i int) (string, error) {
|
||||||
|
return h.items[i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of lines in history
|
||||||
|
func (h *ExampleHistory) Len() int {
|
||||||
|
return len(h.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump returns the entire history
|
||||||
|
func (h *ExampleHistory) Dump() interface{} {
|
||||||
|
return h.items
|
||||||
|
}
|
||||||
|
|
||||||
|
// NullHistory is a null History interface for when you don't want line
|
||||||
|
// entries remembered eg password input.
|
||||||
|
type NullHistory struct{}
|
||||||
|
|
||||||
|
// Write to history
|
||||||
|
func (h *NullHistory) Write(s string) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLine returns a line from history
|
||||||
|
func (h *NullHistory) GetLine(i int) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of lines in history
|
||||||
|
func (h *NullHistory) Len() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump returns the entire history
|
||||||
|
func (h *NullHistory) Dump() interface{} {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browse historic lines:
|
||||||
|
func (rl *Instance) walkHistory(i int) {
|
||||||
|
var (
|
||||||
|
old, new string
|
||||||
|
dedup bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Work with correct history source (depends on CtrlR/CtrlE)
|
||||||
|
var history History
|
||||||
|
if !rl.mainHist {
|
||||||
|
history = rl.altHistory
|
||||||
|
} else {
|
||||||
|
history = rl.mainHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing happens if the history is nil
|
||||||
|
if history == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we are exiting the current line buffer to move around
|
||||||
|
// the history, we make buffer the current line
|
||||||
|
if rl.histPos == 0 && (rl.histPos+i) == 1 {
|
||||||
|
rl.lineBuf = string(rl.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rl.histPos + i {
|
||||||
|
case 0, history.Len() + 1:
|
||||||
|
rl.histPos = 0
|
||||||
|
rl.line = []rune(rl.lineBuf)
|
||||||
|
rl.pos = len(rl.lineBuf)
|
||||||
|
return
|
||||||
|
case -1:
|
||||||
|
rl.histPos = 0
|
||||||
|
rl.lineBuf = string(rl.line)
|
||||||
|
default:
|
||||||
|
dedup = true
|
||||||
|
old = string(rl.line)
|
||||||
|
new, err = history.GetLine(history.Len() - rl.histPos - 1)
|
||||||
|
if err != nil {
|
||||||
|
rl.resetHelpers()
|
||||||
|
print("\r\n" + err.Error() + "\r\n")
|
||||||
|
print(rl.mainPrompt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.clearLine()
|
||||||
|
rl.histPos += i
|
||||||
|
rl.line = []rune(new)
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
if rl.pos > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the line, and any helpers
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
// In order to avoid having to type j/k twice each time for history navigation,
|
||||||
|
// we walk once again. This only ever happens when we aren't out of bounds.
|
||||||
|
if dedup && old == new {
|
||||||
|
rl.walkHistory(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeHistory - Populates a CompletionGroup with history and returns it the shell
|
||||||
|
// we populate only one group, so as to pass it to the main completion engine.
|
||||||
|
func (rl *Instance) completeHistory() (hist []*CompletionGroup) {
|
||||||
|
|
||||||
|
hist = make([]*CompletionGroup, 1)
|
||||||
|
hist[0] = &CompletionGroup{
|
||||||
|
DisplayType: TabDisplayMap,
|
||||||
|
MaxLength: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to completion flux first
|
||||||
|
var history History
|
||||||
|
if !rl.mainHist {
|
||||||
|
if rl.altHistory == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
history = rl.altHistory
|
||||||
|
rl.histHint = []rune(rl.altHistName + ": ")
|
||||||
|
} else {
|
||||||
|
if rl.mainHistory == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
history = rl.mainHistory
|
||||||
|
rl.histHint = []rune(rl.mainHistName + ": ")
|
||||||
|
}
|
||||||
|
|
||||||
|
hist[0].init(rl)
|
||||||
|
|
||||||
|
var (
|
||||||
|
line string
|
||||||
|
num string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
rl.tcPrefix = string(rl.line) // We use the current full line for filtering
|
||||||
|
|
||||||
|
for i := history.Len() - 1; i >= 1; i-- {
|
||||||
|
line, err = history.GetLine(i)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(line, rl.tcPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Replace(line, "\n", ` `, -1)
|
||||||
|
|
||||||
|
if hist[0].Descriptions[line] != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hist[0].Suggestions = append(hist[0].Suggestions, line)
|
||||||
|
num = strconv.Itoa(i)
|
||||||
|
|
||||||
|
hist[0].Descriptions[line] = "\033[38;5;237m" + num + RESET
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instance is used to encapsulate the parameter group and run time of any given
|
||||||
|
// readline instance so that you can reuse the readline API for multiple entry
|
||||||
|
// captures without having to repeatedly unload configuration.
|
||||||
|
type Instance struct {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Input Modes -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// InputMode - The shell can be used in Vim editing mode, or Emacs (classic).
|
||||||
|
InputMode InputMode
|
||||||
|
|
||||||
|
// Vim parameters/functions
|
||||||
|
// ShowVimMode - If set to true, a string '[i]' or '[N]' indicating the
|
||||||
|
// current Vim mode will be appended to the prompt variable, therefore added to
|
||||||
|
// the user's custom prompt is set. Applies for both single and multiline prompts
|
||||||
|
ShowVimMode bool
|
||||||
|
VimModeColorize bool // If set to true, varies colors of the VimModePrompt
|
||||||
|
|
||||||
|
//
|
||||||
|
// Prompt -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Multiline bool // If set to true, the shell will have a two-line prompt.
|
||||||
|
MultilinePrompt string // If multiline is true, this is the content of the 2nd line.
|
||||||
|
|
||||||
|
mainPrompt string // If multiline true, the full prompt string / If false, the 1st line of the prompt
|
||||||
|
realPrompt []rune // The prompt that is actually on the same line as the beginning of the input line.
|
||||||
|
defaultPrompt []rune
|
||||||
|
promptLen int
|
||||||
|
stillOnRefresh bool // True if some logs have printed asynchronously since last loop. Check refresh prompt funcs
|
||||||
|
|
||||||
|
//
|
||||||
|
// Input Line ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// PasswordMask is what character to hide password entry behind.
|
||||||
|
// Once enabled, set to 0 (zero) to disable the mask again.
|
||||||
|
PasswordMask rune
|
||||||
|
|
||||||
|
// readline operating parameters
|
||||||
|
line []rune // This is the input line, with entered text: full line = mlnPrompt + line
|
||||||
|
pos int
|
||||||
|
posX int // Cursor position X
|
||||||
|
fullX int // X coordinate of the full input line, including the prompt if needed.
|
||||||
|
posY int // Cursor position Y (if multiple lines span)
|
||||||
|
fullY int // Y offset to the end of input line.
|
||||||
|
|
||||||
|
// Buffer received from host programms
|
||||||
|
multiline []byte
|
||||||
|
multisplit []string
|
||||||
|
skipStdinRead bool
|
||||||
|
|
||||||
|
// SyntaxHighlight is a helper function to provide syntax highlighting.
|
||||||
|
// Once enabled, set to nil to disable again.
|
||||||
|
SyntaxHighlighter func([]rune) string
|
||||||
|
|
||||||
|
//
|
||||||
|
// Completion ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// TabCompleter is a simple function that offers completion suggestions.
|
||||||
|
// It takes the readline line ([]rune) and cursor pos.
|
||||||
|
// Returns a prefix string, and several completion groups with their items and description
|
||||||
|
// Asynchronously add/refresh completions
|
||||||
|
TabCompleter func([]rune, int, DelayedTabContext) (string, []*CompletionGroup)
|
||||||
|
delayedTabContext DelayedTabContext
|
||||||
|
|
||||||
|
// SyntaxCompletion is used to autocomplete code syntax (like braces and
|
||||||
|
// quotation marks). If you want to complete words or phrases then you might
|
||||||
|
// be better off using the TabCompletion function.
|
||||||
|
// SyntaxCompletion takes the line ([]rune) and cursor position, and returns
|
||||||
|
// the new line and cursor position.
|
||||||
|
SyntaxCompleter func([]rune, int) ([]rune, int)
|
||||||
|
|
||||||
|
// Asynchronously highlight/process the input line
|
||||||
|
DelayedSyntaxWorker func([]rune) []rune
|
||||||
|
delayedSyntaxCount int64
|
||||||
|
|
||||||
|
// MaxTabCompletionRows is the maximum number of rows to display in the tab
|
||||||
|
// completion grid.
|
||||||
|
MaxTabCompleterRows int // = 4
|
||||||
|
|
||||||
|
// tab completion operating parameters
|
||||||
|
tcGroups []*CompletionGroup // All of our suggestions tree is in here
|
||||||
|
tcPrefix string // The current tab completion prefix aggainst which to build candidates
|
||||||
|
|
||||||
|
modeTabCompletion bool
|
||||||
|
compConfirmWait bool // When too many completions, we ask the user to confirm with another Tab keypress.
|
||||||
|
tabCompletionSelect bool // We may have completions printed, but no selected candidate yet
|
||||||
|
tabCompletionReverse bool // Groups sometimes use this indicator to know how they should handle their index
|
||||||
|
tcUsedY int // Comprehensive offset of the currently built completions
|
||||||
|
|
||||||
|
// Candidate / virtual completion string / etc
|
||||||
|
currentComp []rune // The currently selected item, not yet a real part of the input line.
|
||||||
|
lineComp []rune // Same as rl.line, but with the currentComp inserted.
|
||||||
|
lineRemain []rune // When we complete in the middle of a line, we cut and keep the remain.
|
||||||
|
compAddSpace bool // If true, any candidate inserted into the real line is done with an added space.
|
||||||
|
|
||||||
|
//
|
||||||
|
// Completion Search (Normal & History) -----------------------------------------------------
|
||||||
|
|
||||||
|
modeTabFind bool // This does not change, because we will search in all options, no matter the group
|
||||||
|
tfLine []rune // The current search pattern entered
|
||||||
|
modeAutoFind bool // for when invoked via ^R or ^F outside of [tab]
|
||||||
|
searchMode FindMode // Used for varying hints, and underlying functions called
|
||||||
|
regexSearch *regexp.Regexp // Holds the current search regex match
|
||||||
|
mainHist bool // Which history stdin do we want
|
||||||
|
histHint []rune // We store a hist hint, for dual history sources
|
||||||
|
|
||||||
|
//
|
||||||
|
// History -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// mainHistory - current mapped to CtrlR by default, with rl.SetHistoryCtrlR()
|
||||||
|
mainHistory History
|
||||||
|
mainHistName string
|
||||||
|
// altHistory is an alternative history input, in case a console user would
|
||||||
|
// like to have two different history flows. Mapped to CtrlE by default, with rl.SetHistoryCtrlE()
|
||||||
|
altHistory History
|
||||||
|
altHistName string
|
||||||
|
|
||||||
|
// HistoryAutoWrite defines whether items automatically get written to
|
||||||
|
// history.
|
||||||
|
// Enabled by default. Set to false to disable.
|
||||||
|
HistoryAutoWrite bool // = true
|
||||||
|
|
||||||
|
// history operating params
|
||||||
|
lineBuf string
|
||||||
|
histPos int
|
||||||
|
histNavIdx int // Used for quick history navigation.
|
||||||
|
|
||||||
|
//
|
||||||
|
// Hints -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// HintText is a helper function which displays hint text the prompt.
|
||||||
|
// HintText takes the line input from the promt and the cursor position.
|
||||||
|
// It returns the hint text to display.
|
||||||
|
HintText func([]rune, int) []rune
|
||||||
|
|
||||||
|
// HintColor any ANSI escape codes you wish to use for hint formatting. By
|
||||||
|
// default this will just be blue.
|
||||||
|
HintFormatting string
|
||||||
|
|
||||||
|
hintText []rune // The actual hint text
|
||||||
|
hintY int // Offset to hints, if it spans multiple lines
|
||||||
|
|
||||||
|
//
|
||||||
|
// Vim Operatng Parameters -------------------------------------------------------------------
|
||||||
|
|
||||||
|
modeViMode ViMode //= vimInsert
|
||||||
|
viIteration string
|
||||||
|
viUndoHistory []undoItem
|
||||||
|
viUndoSkipAppend bool
|
||||||
|
viIsYanking bool
|
||||||
|
registers *registers // All memory text registers, can be consulted with Alt"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Other -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// TempDirectory is the path to write temporary files when editing a line in
|
||||||
|
// $EDITOR. This will default to os.TempDir()
|
||||||
|
TempDirectory string
|
||||||
|
|
||||||
|
// GetMultiLine is a callback to your host program. Since multiline support
|
||||||
|
// is handled by the application rather than readline itself, this callback
|
||||||
|
// is required when calling $EDITOR. However if this function is not set
|
||||||
|
// then readline will just use the current line.
|
||||||
|
GetMultiLine func([]rune) []rune
|
||||||
|
|
||||||
|
EnableGetCursorPos bool
|
||||||
|
|
||||||
|
// event
|
||||||
|
evtKeyPress map[string]func(string, []rune, int) *EventReturn
|
||||||
|
|
||||||
|
// concurency
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
ViModeCallback func(ViMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstance is used to create a readline instance and initialise it with sane defaults.
|
||||||
|
func NewInstance() *Instance {
|
||||||
|
rl := new(Instance)
|
||||||
|
|
||||||
|
// Prompt
|
||||||
|
rl.Multiline = false
|
||||||
|
rl.mainPrompt = ""
|
||||||
|
rl.defaultPrompt = []rune{}
|
||||||
|
rl.promptLen = len(rl.computePrompt())
|
||||||
|
|
||||||
|
// Input Editing
|
||||||
|
rl.InputMode = Emacs
|
||||||
|
rl.ShowVimMode = true // In case the user sets input mode to Vim, everything is ready.
|
||||||
|
|
||||||
|
// Completion
|
||||||
|
rl.MaxTabCompleterRows = 50
|
||||||
|
|
||||||
|
// History
|
||||||
|
rl.mainHistory = new(ExampleHistory) // In-memory history by default.
|
||||||
|
rl.HistoryAutoWrite = true
|
||||||
|
|
||||||
|
// Others
|
||||||
|
rl.HintFormatting = seqFgBlue
|
||||||
|
rl.evtKeyPress = make(map[string]func(string, []rune, int) *EventReturn)
|
||||||
|
rl.TempDirectory = os.TempDir()
|
||||||
|
|
||||||
|
// Registers
|
||||||
|
rl.initRegisters()
|
||||||
|
|
||||||
|
return rl
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// When the DelayedSyntaxWorker gives us a new line, we need to check if there
|
||||||
|
// is any processing to be made, that all lines match in terms of content.
|
||||||
|
func (rl *Instance) updateLine(line []rune) {
|
||||||
|
if len(rl.currentComp) > 0 {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
rl.line = line
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLine - In many places we need the current line input. We either return the real line,
|
||||||
|
// or the one that includes the current completion candidate, if there is any.
|
||||||
|
func (rl *Instance) getLine() []rune {
|
||||||
|
if len(rl.currentComp) > 0 {
|
||||||
|
return rl.lineComp
|
||||||
|
}
|
||||||
|
return rl.line
|
||||||
|
}
|
||||||
|
|
||||||
|
// echo - refresh the current input line, either virtually completed or not.
|
||||||
|
// also renders the current completions and hints. To be noted, the updateReferences()
|
||||||
|
// function is only ever called once, and after having moved back to prompt position
|
||||||
|
// and having printed the line: this is so that at any moment, everyone has the good
|
||||||
|
// values for moving around, synchronized with the update input line.
|
||||||
|
func (rl *Instance) echo() {
|
||||||
|
|
||||||
|
// Then we print the prompt, and the line,
|
||||||
|
switch {
|
||||||
|
case rl.PasswordMask != 0:
|
||||||
|
case rl.PasswordMask > 0:
|
||||||
|
print(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ")
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Go back to prompt position, and clear everything below
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorUp(rl.posY)
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Print the prompt
|
||||||
|
print(string(rl.realPrompt))
|
||||||
|
|
||||||
|
// Assemble the line, taking virtual completions into account
|
||||||
|
var line []rune
|
||||||
|
if len(rl.currentComp) > 0 {
|
||||||
|
line = rl.lineComp
|
||||||
|
} else {
|
||||||
|
line = rl.line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the input line with optional syntax highlighting
|
||||||
|
if rl.SyntaxHighlighter != nil {
|
||||||
|
print(rl.SyntaxHighlighter(line) + " ")
|
||||||
|
} else {
|
||||||
|
print(string(line) + " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update references with new coordinates only now, because
|
||||||
|
// the new line may be longer/shorter than the previous one.
|
||||||
|
rl.updateReferences()
|
||||||
|
|
||||||
|
// Go back to the current cursor position, with new coordinates
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorUp(rl.fullY)
|
||||||
|
moveCursorDown(rl.posY)
|
||||||
|
moveCursorForwards(rl.posX)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) insert(r []rune) {
|
||||||
|
for {
|
||||||
|
// I don't really understand why `0` is creaping in at the end of the
|
||||||
|
// array but it only happens with unicode characters.
|
||||||
|
if len(r) > 1 && r[len(r)-1] == 0 {
|
||||||
|
r = r[:len(r)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can ONLY have three fondamentally different cases:
|
||||||
|
switch {
|
||||||
|
// The line is empty
|
||||||
|
case len(rl.line) == 0:
|
||||||
|
rl.line = r
|
||||||
|
|
||||||
|
// We are inserting somewhere in the middle
|
||||||
|
case rl.pos < len(rl.line):
|
||||||
|
r := append(r, rl.line[rl.pos:]...)
|
||||||
|
rl.line = append(rl.line[:rl.pos], r...)
|
||||||
|
|
||||||
|
// We are at the end of the input line
|
||||||
|
case rl.pos == len(rl.line):
|
||||||
|
rl.line = append(rl.line, r...)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.pos += len(r)
|
||||||
|
|
||||||
|
// This should also update the rl.pos
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) deleteX() {
|
||||||
|
switch {
|
||||||
|
case len(rl.line) == 0:
|
||||||
|
return
|
||||||
|
case rl.pos == 0:
|
||||||
|
rl.line = rl.line[1:]
|
||||||
|
case rl.pos > len(rl.line):
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
case rl.pos == len(rl.line):
|
||||||
|
rl.pos--
|
||||||
|
rl.line = rl.line[:rl.pos]
|
||||||
|
default:
|
||||||
|
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) deleteBackspace() {
|
||||||
|
switch {
|
||||||
|
case len(rl.line) == 0:
|
||||||
|
return
|
||||||
|
case rl.pos == 0:
|
||||||
|
rl.line = rl.line[1:]
|
||||||
|
case rl.pos > len(rl.line):
|
||||||
|
rl.backspace() // There is an infite loop going on here...
|
||||||
|
case rl.pos == len(rl.line):
|
||||||
|
rl.pos--
|
||||||
|
rl.line = rl.line[:rl.pos]
|
||||||
|
default:
|
||||||
|
rl.pos--
|
||||||
|
rl.line = append(rl.line[:rl.pos], rl.line[rl.pos+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) clearLine() {
|
||||||
|
if len(rl.line) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to go back to prompt
|
||||||
|
moveCursorUp(rl.posY)
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorForwards(rl.promptLen)
|
||||||
|
|
||||||
|
// Clear everything after & below the cursor
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Real input line
|
||||||
|
rl.line = []rune{}
|
||||||
|
rl.lineComp = []rune{}
|
||||||
|
rl.pos = 0
|
||||||
|
rl.posX = 0
|
||||||
|
rl.fullX = 0
|
||||||
|
rl.posY = 0
|
||||||
|
rl.fullY = 0
|
||||||
|
|
||||||
|
// Completions are also reset
|
||||||
|
rl.clearVirtualComp()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) deleteToBeginning() {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
// Keep the line length up until the cursor
|
||||||
|
rl.line = rl.line[rl.pos:]
|
||||||
|
rl.pos = 0
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLineWrap(t *testing.T) {
|
||||||
|
type TestLineWrapT struct {
|
||||||
|
Prompt string
|
||||||
|
Line string
|
||||||
|
TermWidth int
|
||||||
|
Expected []string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []TestLineWrapT{
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 80,
|
||||||
|
Expected: []string{"1234567890 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
TermWidth: 86,
|
||||||
|
Expected: []string{"12345678901234567890123456789012345678901234567890123456789012345678901234567890", " "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
TermWidth: 87,
|
||||||
|
Expected: []string{"12345678901234567890123456789012345678901234567890123456789012345678901234567890 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "123456789012345678901234567890",
|
||||||
|
TermWidth: 20,
|
||||||
|
Expected: []string{"12345678901234", " 56789012345678", " 90 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 4,
|
||||||
|
Expected: []string{"1234", "5678", "90 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 5,
|
||||||
|
Expected: []string{"12345", "67890", " "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 6,
|
||||||
|
Expected: []string{"123456", "7890 "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 7,
|
||||||
|
Expected: []string{"1", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9", " 0", " "},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 8,
|
||||||
|
Expected: []string{"12", " 34", " 56", " 78", " 90", " "},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
rl := NewInstance()
|
||||||
|
rl.SetPrompt(test.Prompt)
|
||||||
|
//rl.lineBuf = test.Line
|
||||||
|
rl.line = []rune(test.Line)
|
||||||
|
|
||||||
|
wrap := lineWrap(rl, test.TermWidth)
|
||||||
|
if len(wrap) != len(test.Expected) {
|
||||||
|
t.Error("Slice lens do not match:")
|
||||||
|
t.Logf(" Test: %d (%s)", i, t.Name())
|
||||||
|
t.Logf(" Prompt: '%s'", test.Prompt)
|
||||||
|
t.Logf(" Line: '%s'", test.Line)
|
||||||
|
t.Logf(" Width: %d", test.TermWidth)
|
||||||
|
t.Logf(" len(exp): %d", len(test.Expected))
|
||||||
|
t.Logf(" len(act): %d", len(wrap))
|
||||||
|
t.Logf(" Slice e: '%s'", fmt.Sprint(test.Expected))
|
||||||
|
t.Logf(" Slice a: '%s'", fmt.Sprint(wrap))
|
||||||
|
t.Logf(" rl.promptLen: %d'", rl.promptLen)
|
||||||
|
t.Logf(" rl.line: '%s'", string(rl.line))
|
||||||
|
t.Logf(" rl.lineBuf: '%s'", rl.lineBuf)
|
||||||
|
t.Logf(" n: %.10f'", float64(len(rl.line))/(float64(test.TermWidth)-float64(rl.promptLen)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for j := range wrap {
|
||||||
|
if wrap[j] != test.Expected[j] {
|
||||||
|
t.Error("Slice element does not match:")
|
||||||
|
t.Logf(" Test: %d (%s)", i, t.Name())
|
||||||
|
t.Logf(" Prompt: '%s'", test.Prompt)
|
||||||
|
t.Logf(" Line: '%s'", test.Line)
|
||||||
|
t.Logf(" Width: %d", test.TermWidth)
|
||||||
|
t.Logf(" Expected: %s", test.Expected[j])
|
||||||
|
t.Logf(" Actual: %s", wrap[j])
|
||||||
|
t.Logf(" len(exp): %d", len(test.Expected))
|
||||||
|
t.Logf(" len(act): %d", len(wrap))
|
||||||
|
t.Logf(" Slice e: '%s'", fmt.Sprint(test.Expected))
|
||||||
|
t.Logf(" Slice a: '%s'", fmt.Sprint(wrap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLineWrapPos(t *testing.T) {
|
||||||
|
type ExpectedT struct {
|
||||||
|
X, Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLineWrapPosT struct {
|
||||||
|
Prompt string
|
||||||
|
Line string
|
||||||
|
TermWidth int
|
||||||
|
Expected ExpectedT
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = []TestLineWrapPosT{
|
||||||
|
// tests := []TestLineWrapPosT{
|
||||||
|
{
|
||||||
|
Prompt: "12345",
|
||||||
|
Line: "123",
|
||||||
|
TermWidth: 10,
|
||||||
|
Expected: ExpectedT{5 + 3, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "12345",
|
||||||
|
Line: "1234",
|
||||||
|
TermWidth: 10,
|
||||||
|
Expected: ExpectedT{5 + 4, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "12345",
|
||||||
|
Line: "12345",
|
||||||
|
TermWidth: 10,
|
||||||
|
Expected: ExpectedT{5 + 0, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "12345",
|
||||||
|
Line: "123456",
|
||||||
|
TermWidth: 10,
|
||||||
|
Expected: ExpectedT{5 + 1, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 80,
|
||||||
|
Expected: ExpectedT{6 + 10, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
TermWidth: 85,
|
||||||
|
Expected: ExpectedT{6 + 1, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
TermWidth: 86,
|
||||||
|
Expected: ExpectedT{6 + 0, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "12345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||||
|
TermWidth: 87,
|
||||||
|
Expected: ExpectedT{6 + 80, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "123456789012345678901234567890",
|
||||||
|
TermWidth: 20,
|
||||||
|
//Expected: []string{"12345678901234", "56789012345678", "90"},
|
||||||
|
Expected: ExpectedT{6 + 2, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 4,
|
||||||
|
//Expected: []string{"1234", "5678", "90"},
|
||||||
|
Expected: ExpectedT{0 + 2, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 5,
|
||||||
|
//Expected: []string{"12345", "67890"},
|
||||||
|
Expected: ExpectedT{0 + 0, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 6,
|
||||||
|
//Expected: []string{"123456", "7890"},
|
||||||
|
Expected: ExpectedT{0 + 4, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 7,
|
||||||
|
//Expected: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"},
|
||||||
|
Expected: ExpectedT{6 + 0, 10},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prompt: "foobar",
|
||||||
|
Line: "1234567890",
|
||||||
|
TermWidth: 8,
|
||||||
|
//Expected: []string{"12", "34", "56", "78", "90"},
|
||||||
|
Expected: ExpectedT{6 + 0, 5},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// for i, test := range tests {
|
||||||
|
//
|
||||||
|
// x, y := lineWrapPos(len(test.Prompt), len(test.Line), test.TermWidth)
|
||||||
|
//
|
||||||
|
// if (test.Expected.X != x) || (test.Expected.Y != y) {
|
||||||
|
// t.Error("X or Y does not matxh:")
|
||||||
|
// t.Logf(" Test: %d (%s)", i, t.Name())
|
||||||
|
// t.Logf(" Prompt: '%s'", test.Prompt)
|
||||||
|
// t.Logf(" Line: '%s'", test.Line)
|
||||||
|
// t.Logf(" Width: %d", test.TermWidth)
|
||||||
|
// t.Logf(" Expected X:%d", test.Expected.X)
|
||||||
|
// t.Logf(" Actual X:%d", x)
|
||||||
|
// t.Logf(" Expected Y:%d", test.Expected.Y)
|
||||||
|
// t.Logf(" Actual Y:%d", y)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
ansi "github.com/acarl005/stripansi"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetPrompt will define the readline prompt string.
|
||||||
|
// It also calculates the runes in the string as well as any non-printable escape codes.
|
||||||
|
func (rl *Instance) SetPrompt(s string) {
|
||||||
|
rl.mainPrompt = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshPromptLog - A simple function to print a string message (a log, or more broadly,
|
||||||
|
// an asynchronous event) without bothering the user, and by "pushing" the prompt below the message.
|
||||||
|
func (rl *Instance) RefreshPromptLog(log string) (err error) {
|
||||||
|
|
||||||
|
// We adjust cursor movement, depending on which mode we're currently in.
|
||||||
|
if !rl.modeTabCompletion {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
// Account for the hint line
|
||||||
|
} else if rl.modeTabCompletion && rl.modeAutoFind {
|
||||||
|
rl.tcUsedY = 0
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt offset
|
||||||
|
if rl.Multiline {
|
||||||
|
rl.tcUsedY += 1
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the current prompt and everything below
|
||||||
|
print(seqClearLine)
|
||||||
|
if rl.stillOnRefresh {
|
||||||
|
moveCursorUp(1)
|
||||||
|
}
|
||||||
|
rl.stillOnRefresh = true
|
||||||
|
moveCursorUp(rl.hintY + rl.tcUsedY)
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
print("\r\n" + seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Print the log
|
||||||
|
fmt.Printf(log)
|
||||||
|
|
||||||
|
// Add a new line between the message and the prompt, so not overloading the UI
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
// Print the prompt
|
||||||
|
if rl.Multiline {
|
||||||
|
rl.tcUsedY += 3
|
||||||
|
fmt.Println(rl.mainPrompt)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY += 2
|
||||||
|
fmt.Print(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the line
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshPromptInPlace - Refreshes the prompt in the very same place he is.
|
||||||
|
func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) {
|
||||||
|
|
||||||
|
// We adjust cursor movement, depending on which mode we're currently in.
|
||||||
|
// Prompt data intependent
|
||||||
|
if !rl.modeTabCompletion {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
// Account for the hint line
|
||||||
|
} else if rl.modeTabCompletion && rl.modeAutoFind {
|
||||||
|
rl.tcUsedY = 0
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the prompt if a special has been passed.
|
||||||
|
if prompt != "" {
|
||||||
|
rl.mainPrompt = prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.Multiline {
|
||||||
|
rl.tcUsedY += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input line and everything below
|
||||||
|
print(seqClearLine)
|
||||||
|
moveCursorUp(rl.hintY + rl.tcUsedY)
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
print("\r\n" + seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Add a new line if needed
|
||||||
|
if rl.Multiline {
|
||||||
|
fmt.Println(rl.mainPrompt)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fmt.Print(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the line
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshPromptCustom - Refresh the console prompt with custom values.
|
||||||
|
// @prompt => If not nil (""), will use this prompt instead of the currently set prompt.
|
||||||
|
// @offset => Used to set the number of lines to go upward, before reprinting. Set to 0 if not used.
|
||||||
|
// @clearLine => If true, will clean the current input line on the next refresh.
|
||||||
|
func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine bool) (err error) {
|
||||||
|
|
||||||
|
// We adjust cursor movement, depending on which mode we're currently in.
|
||||||
|
if !rl.modeTabCompletion {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
} else if rl.modeTabCompletion && rl.modeAutoFind { // Account for the hint line
|
||||||
|
rl.tcUsedY = 0
|
||||||
|
} else {
|
||||||
|
rl.tcUsedY = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user-provided offset
|
||||||
|
rl.tcUsedY += offset
|
||||||
|
|
||||||
|
// Go back to prompt position, then up to the user provided offset.
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorUp(rl.posY)
|
||||||
|
moveCursorUp(offset)
|
||||||
|
|
||||||
|
// Then clear everything below our new position
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Update the prompt if a special has been passed.
|
||||||
|
if prompt != "" {
|
||||||
|
rl.mainPrompt = prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new line if needed
|
||||||
|
if rl.Multiline && prompt == "" {
|
||||||
|
} else if rl.Multiline {
|
||||||
|
fmt.Println(rl.mainPrompt)
|
||||||
|
} else {
|
||||||
|
fmt.Print(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the line
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
// If input line was empty, check that we clear it from detritus
|
||||||
|
// The three lines are borrowed from clearLine(), we don't need more.
|
||||||
|
if clearLine {
|
||||||
|
rl.clearLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// computePrompt - At any moment, returns an (1st or 2nd line) actualized prompt,
|
||||||
|
// considering all input mode parameters and prompt string values.
|
||||||
|
func (rl *Instance) computePrompt() (prompt []rune) {
|
||||||
|
if rl.Multiline {
|
||||||
|
if rl.MultilinePrompt != "" {
|
||||||
|
rl.realPrompt = []rune(rl.MultilinePrompt)
|
||||||
|
} else {
|
||||||
|
rl.realPrompt = []rune{} //rl.defaultPrompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !rl.Multiline {
|
||||||
|
if rl.mainPrompt != "" {
|
||||||
|
rl.realPrompt = []rune(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
// We add the multiline prompt anyway, because it might be empty and thus have
|
||||||
|
// no effect on our user interface, or be specified and thus needed.
|
||||||
|
// if rl.MultilinePrompt != "" {
|
||||||
|
rl.realPrompt = append(rl.realPrompt, []rune(rl.MultilinePrompt)...)
|
||||||
|
// } else {
|
||||||
|
// rl.realPrompt = append(rl.realPrompt, rl.defaultPrompt...)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip color escapes
|
||||||
|
rl.promptLen = getRealLength(string(rl.realPrompt))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) colorizeVimPrompt(p []rune) (cp []rune) {
|
||||||
|
if rl.VimModeColorize {
|
||||||
|
return []rune(fmt.Sprintf("%s%s%s", BOLD, string(p), RESET))
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRealLength - Some strings will have ANSI escape codes, which might be wrongly
|
||||||
|
// interpreted as legitimate parts of the strings. This will bother if some prompt
|
||||||
|
// components depend on other's length, so we always pass the string in this for
|
||||||
|
// getting its real-printed length.
|
||||||
|
func getRealLength(s string) (l int) {
|
||||||
|
stripped := ansi.Strip(s)
|
||||||
|
return uniseg.GraphemeClusterCount(stripped)
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,10 @@
|
||||||
|
# raw
|
||||||
|
|
||||||
|
This command exists here purely as a lazy feature for me to scan key presses
|
||||||
|
for their corresponding escape codes. It is a useful dev tool for rationalizing
|
||||||
|
what is happening in the different terminal emulators (since documentation
|
||||||
|
regarding what escape codes they send can often be non-existent and some of the
|
||||||
|
more exotic key combinations or modern keyboard functions can have multiple
|
||||||
|
published standards.
|
||||||
|
|
||||||
|
This package is not imported by `readline` and is not required as part of `readline`
|
|
@ -0,0 +1,22 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/maxlandon/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
readline.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
|
||||||
|
for {
|
||||||
|
b := make([]byte, 1024)
|
||||||
|
i, err := os.Stdin.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(b[:i])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd netbsd openbsd
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TIOCGETA
|
||||||
|
const ioctlWriteTermios = unix.TIOCSETA
|
||||||
|
|
||||||
|
//const OXTABS = unix.OXTABS
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const ioctlReadTermios = unix.TCGETS
|
||||||
|
const ioctlWriteTermios = unix.TCSETS
|
||||||
|
|
||||||
|
//const OXTABS = unix.XTABS
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct{}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2015 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build solaris
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
state *unix.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw puts the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
// see http://cr.illumos.org/~webrev/andy_js/1060/
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
oldTermios := *oldTermiosPtr
|
||||||
|
|
||||||
|
newTermios := oldTermios
|
||||||
|
newTermios.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
|
||||||
|
//newTermios.Oflag &^= syscall.OPOST
|
||||||
|
newTermios.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
||||||
|
newTermios.Cflag &^= syscall.CSIZE | syscall.PARENB
|
||||||
|
newTermios.Cflag |= syscall.CS8
|
||||||
|
newTermios.Cc[unix.VMIN] = 1
|
||||||
|
newTermios.Cc[unix.VTIME] = 0
|
||||||
|
|
||||||
|
if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newTermios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{
|
||||||
|
state: oldTermiosPtr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, oldState *State) error {
|
||||||
|
return unix.IoctlSetTermios(fd, unix.TCSETS, oldState.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
oldTermiosPtr, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{
|
||||||
|
state: oldTermiosPtr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row), nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
termios unix.Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := State{termios: *termios}
|
||||||
|
|
||||||
|
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||||
|
// the termios(3) manpage.
|
||||||
|
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||||
|
//termios.Oflag &^= unix.OPOST
|
||||||
|
//termios.Oflag &^= OXTABS
|
||||||
|
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||||
|
termios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||||
|
termios.Cflag |= unix.CS8
|
||||||
|
termios.Cc[unix.VMIN] = 1
|
||||||
|
termios.Cc[unix.VTIME] = 0
|
||||||
|
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oldState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{termios: *termios}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
return int(ws.Col), int(ws.Row), nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
var st uint32
|
||||||
|
err := windows.GetConsoleMode(windows.Handle(fd), &st)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
return windows.SetConsoleMode(windows.Handle(fd), state.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
var info windows.ConsoleScreenBufferInfo
|
||||||
|
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(info.Size.X), int(info.Size.Y), nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,838 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rxMultiline = regexp.MustCompile(`[\r\n]+`)
|
||||||
|
|
||||||
|
// Readline displays the readline prompt.
|
||||||
|
// It will return a string (user entered data) or an error.
|
||||||
|
func (rl *Instance) Readline() (string, error) {
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
state, err := MakeRaw(fd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer Restore(fd, state)
|
||||||
|
|
||||||
|
// In Vim mode, we always start in Input mode. The prompt needs this.
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
|
||||||
|
// Prompt Init
|
||||||
|
// Here we have to either print prompt
|
||||||
|
// and return new line (multiline)
|
||||||
|
if rl.Multiline {
|
||||||
|
fmt.Println(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
rl.stillOnRefresh = false
|
||||||
|
rl.computePrompt() // initialise the prompt for first print
|
||||||
|
|
||||||
|
// Line Init & Cursor
|
||||||
|
rl.line = []rune{}
|
||||||
|
rl.currentComp = []rune{} // No virtual completion yet
|
||||||
|
rl.lineComp = []rune{} // So no virtual line either
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.pos = 0
|
||||||
|
rl.posY = 0
|
||||||
|
|
||||||
|
// Completion && hints init
|
||||||
|
rl.resetHintText()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
rl.getHintText()
|
||||||
|
|
||||||
|
// History Init
|
||||||
|
// We need this set to the last command, so that we can access it quickly
|
||||||
|
rl.histPos = 0
|
||||||
|
rl.viUndoHistory = []undoItem{{line: "", pos: 0}}
|
||||||
|
|
||||||
|
// Multisplit
|
||||||
|
if len(rl.multisplit) > 0 {
|
||||||
|
r := []rune(rl.multisplit[0])
|
||||||
|
rl.editorInput(r)
|
||||||
|
rl.carridgeReturn()
|
||||||
|
if len(rl.multisplit) > 1 {
|
||||||
|
rl.multisplit = rl.multisplit[1:]
|
||||||
|
} else {
|
||||||
|
rl.multisplit = []string{}
|
||||||
|
}
|
||||||
|
return string(rl.line), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, print any hints or completions
|
||||||
|
// if the TabCompletion engines so desires
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
// Start handling keystrokes. Classified by subject for most.
|
||||||
|
for {
|
||||||
|
rl.viUndoSkipAppend = false
|
||||||
|
b := make([]byte, 1024)
|
||||||
|
var i int
|
||||||
|
|
||||||
|
if !rl.skipStdinRead {
|
||||||
|
var err error
|
||||||
|
i, err = os.Stdin.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.skipStdinRead = false
|
||||||
|
r := []rune(string(b))
|
||||||
|
|
||||||
|
if isMultiline(r[:i]) || len(rl.multiline) > 0 {
|
||||||
|
rl.multiline = append(rl.multiline, b[:i]...)
|
||||||
|
if i == len(b) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rl.allowMultiline(rl.multiline) {
|
||||||
|
rl.multiline = []byte{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(rl.multiline)
|
||||||
|
rl.multisplit = rxMultiline.Split(s, -1)
|
||||||
|
|
||||||
|
r = []rune(rl.multisplit[0])
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.editorInput(r)
|
||||||
|
rl.carridgeReturn()
|
||||||
|
rl.multiline = []byte{}
|
||||||
|
if len(rl.multisplit) > 1 {
|
||||||
|
rl.multisplit = rl.multisplit[1:]
|
||||||
|
} else {
|
||||||
|
rl.multisplit = []string{}
|
||||||
|
}
|
||||||
|
return string(rl.line), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(r[:i])
|
||||||
|
if rl.evtKeyPress[s] != nil {
|
||||||
|
rl.clearHelpers()
|
||||||
|
|
||||||
|
ret := rl.evtKeyPress[s](s, rl.line, rl.pos)
|
||||||
|
|
||||||
|
rl.clearLine()
|
||||||
|
rl.line = append(ret.NewLine, []rune{}...)
|
||||||
|
rl.updateHelpers() // rl.echo
|
||||||
|
rl.pos = ret.NewPos
|
||||||
|
|
||||||
|
if ret.ClearHelpers {
|
||||||
|
rl.resetHelpers()
|
||||||
|
} else {
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret.HintText) > 0 {
|
||||||
|
rl.hintText = ret.HintText
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
if !ret.ForwardKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ret.CloseReadline {
|
||||||
|
rl.clearHelpers()
|
||||||
|
return string(rl.line), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before anything: we can never be both in modeTabCompletion and compConfirmWait,
|
||||||
|
// because we need to confirm before entering completion. If both are true, there
|
||||||
|
// is a problem (at least, the user has escaped the confirm hint some way).
|
||||||
|
if (rl.modeTabCompletion && rl.searchMode != HistoryFind) && rl.compConfirmWait {
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch b[0] {
|
||||||
|
// Errors & Returns --------------------------------------------------------------------------------
|
||||||
|
case charCtrlC:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(true)
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rl.clearHelpers()
|
||||||
|
return "", CtrlC
|
||||||
|
|
||||||
|
case charEOF:
|
||||||
|
rl.clearHelpers()
|
||||||
|
return "", EOF
|
||||||
|
|
||||||
|
// Clear screen
|
||||||
|
case charCtrlL:
|
||||||
|
print(seqClearScreen)
|
||||||
|
print(seqCursorTopLeft)
|
||||||
|
if rl.Multiline {
|
||||||
|
fmt.Println(rl.mainPrompt)
|
||||||
|
}
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
rl.resetHintText()
|
||||||
|
rl.getHintText()
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
// Line Editing ------------------------------------------------------------------------------------
|
||||||
|
case charCtrlU:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(true)
|
||||||
|
}
|
||||||
|
// Delete everything from the beginning of the line to the cursor position
|
||||||
|
rl.saveBufToRegister(rl.line[:rl.pos])
|
||||||
|
rl.deleteToBeginning()
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
case charBackspace, charBackspace2:
|
||||||
|
// When currently in history completion, we refresh and automatically
|
||||||
|
// insert the first (filtered) candidate, virtually
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind {
|
||||||
|
rl.resetVirtualComp(true)
|
||||||
|
rl.backspaceTabFind()
|
||||||
|
|
||||||
|
// Then update the printing, with the new candidate
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.renderHelpers()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal completion search does only refresh the search pattern and the comps
|
||||||
|
if rl.modeTabFind || rl.modeAutoFind {
|
||||||
|
rl.backspaceTabFind()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
} else {
|
||||||
|
// Always cancel any virtual completion
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
|
||||||
|
// Vim mode has different behaviors
|
||||||
|
if rl.InputMode == Vim {
|
||||||
|
if rl.modeViMode == VimInsert {
|
||||||
|
rl.backspace()
|
||||||
|
} else {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
rl.renderHelpers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else emacs deletes a character
|
||||||
|
rl.backspace()
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emacs Bindings ----------------------------------------------------------------------------------
|
||||||
|
case charCtrlW:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
}
|
||||||
|
// This is only available in Insert mode
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rl.saveToRegister(rl.viJumpB(tokeniseLine))
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine))
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
case charCtrlY:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
}
|
||||||
|
// paste after the cursor position
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
buffer := rl.pasteFromRegister()
|
||||||
|
rl.insert(buffer)
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
case charCtrlE:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
}
|
||||||
|
// This is only available in Insert mode
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(rl.line) > 0 {
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
case charCtrlA:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
}
|
||||||
|
// This is only available in Insert mode
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.pos = 0
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
// Command History ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NOTE: The alternative history source is triggered by Alt+r,
|
||||||
|
// but because this is a sequence, the alternative history code
|
||||||
|
// trigger is in the below rl.escapeSeq(r) function.
|
||||||
|
case charCtrlR:
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
// For some modes only, if we are in vim Keys mode,
|
||||||
|
// we toogle back to insert mode. For others, we return
|
||||||
|
// without getting the completions.
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.computePrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.mainHist = true // false before
|
||||||
|
rl.searchMode = HistoryFind
|
||||||
|
rl.modeAutoFind = true
|
||||||
|
rl.modeTabCompletion = true
|
||||||
|
|
||||||
|
rl.modeTabFind = true
|
||||||
|
rl.updateTabFind([]rune{})
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
// Tab Completion & Completion Search ---------------------------------------------------------------
|
||||||
|
case charTab:
|
||||||
|
// The user cannot show completions if currently in Vim Normal mode
|
||||||
|
if rl.InputMode == Vim && rl.modeViMode != VimInsert {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have asked for completions, already printed, and we want to move selection.
|
||||||
|
if rl.modeTabCompletion && !rl.compConfirmWait {
|
||||||
|
rl.tabCompletionSelect = true
|
||||||
|
rl.moveTabCompletionHighlight(1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.renderHelpers()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
} else {
|
||||||
|
// Else we might be asked to confirm printing (if too many suggestions), or not.
|
||||||
|
rl.getTabCompletion()
|
||||||
|
|
||||||
|
// If too many completions and no yet confirmed, ask user for completion
|
||||||
|
// comps, lines := rl.getCompletionCount()
|
||||||
|
// if ((lines > GetTermLength()) || (lines > rl.MaxTabCompleterRows)) && !rl.compConfirmWait {
|
||||||
|
// sentence := fmt.Sprintf("%s show all %d completions (%d lines) ? tab to confirm",
|
||||||
|
// FOREWHITE, comps, lines)
|
||||||
|
// rl.promptCompletionConfirm(sentence)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
rl.modeTabCompletion = true
|
||||||
|
|
||||||
|
// Also here, if only one candidate is available, automatically
|
||||||
|
// insert it and don't bother printing completions.
|
||||||
|
// Quit the tab completion mode to avoid asking to the user
|
||||||
|
// to press Enter twice to actually run the command.
|
||||||
|
if rl.hasOneCandidate() {
|
||||||
|
rl.insertCandidate()
|
||||||
|
|
||||||
|
// Refresh first, and then quit the completion mode
|
||||||
|
rl.updateHelpers() // REDUNDANT WITH getTabCompletion()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateHelpers() // REDUNDANT WITH getTabCompletion()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
case charCtrlF:
|
||||||
|
rl.resetVirtualComp(true)
|
||||||
|
|
||||||
|
if !rl.modeTabCompletion {
|
||||||
|
rl.modeTabCompletion = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.compConfirmWait {
|
||||||
|
rl.resetHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both these settings apply to when we already
|
||||||
|
// are in completion mode and when we are not.
|
||||||
|
rl.searchMode = CompletionFind
|
||||||
|
rl.modeAutoFind = true
|
||||||
|
|
||||||
|
// Switch from history to completion search
|
||||||
|
if rl.modeTabCompletion && rl.searchMode == HistoryFind {
|
||||||
|
rl.searchMode = CompletionFind
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateTabFind([]rune{})
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case charCtrlG:
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
rl.resetTabFind()
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.modeAutoFind {
|
||||||
|
rl.resetTabFind()
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
case '\r':
|
||||||
|
fallthrough
|
||||||
|
case '\n':
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
cur := rl.getCurrentGroup()
|
||||||
|
|
||||||
|
// Check that there is a group indeed, as we might have no completions.
|
||||||
|
if cur == nil {
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
rl.renderHelpers()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// IF we have a prefix and completions printed, but no candidate
|
||||||
|
// (in which case the completion is ""), we immediately return.
|
||||||
|
completion := cur.getCurrentCell(rl)
|
||||||
|
prefix := len(rl.tcPrefix)
|
||||||
|
if prefix > len(completion) {
|
||||||
|
rl.carridgeReturn()
|
||||||
|
return string(rl.line), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, we insert the completion candidate in the real input line.
|
||||||
|
// By default we add a space, unless completion group asks otherwise.
|
||||||
|
rl.compAddSpace = true
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
|
||||||
|
// If we were in history completion, immediately execute the line.
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind {
|
||||||
|
rl.carridgeReturn()
|
||||||
|
return string(rl.line), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset completions and update input line
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rl.carridgeReturn()
|
||||||
|
return string(rl.line), nil
|
||||||
|
|
||||||
|
// Vim --------------------------------------------------------------------------------------
|
||||||
|
case charEscape:
|
||||||
|
|
||||||
|
// If we were waiting for completion confirm, abort
|
||||||
|
if rl.compConfirmWait {
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always refresh the completion candidates, except if we are currently
|
||||||
|
// cycling through them, because then it would just append the candidate.
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
if string(r[:i]) != seqShiftTab &&
|
||||||
|
string(r[:i]) != seqForwards && string(r[:i]) != seqBackwards &&
|
||||||
|
string(r[:i]) != seqUp && string(r[:i]) != seqDown {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once helpers of all sorts are cleared, we can process
|
||||||
|
// the change of input modes, etc.
|
||||||
|
rl.escapeSeq(r[:i])
|
||||||
|
|
||||||
|
// Dispatch --------------------------------------------------------------------------------------
|
||||||
|
default:
|
||||||
|
|
||||||
|
// If we were waiting for completion confirm, abort
|
||||||
|
if rl.compConfirmWait {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// When currently in history completion, we refresh and automatically
|
||||||
|
// insert the first (filtered) candidate, virtually
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind {
|
||||||
|
rl.resetVirtualComp(true)
|
||||||
|
rl.updateTabFind(r[:i])
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.renderHelpers()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure that CompletionFind is useful, nor one of the other two
|
||||||
|
if rl.modeAutoFind || rl.modeTabFind {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
rl.updateTabFind(r[:i])
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
} else {
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
rl.editorInput(r[:i])
|
||||||
|
if len(rl.multiline) > 0 && rl.modeViMode == VimKeys {
|
||||||
|
rl.skipStdinRead = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.clearHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.undoAppendHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorInput is an unexported function used to determine what mode of text
|
||||||
|
// entry readline is currently configured for and then update the line entries
|
||||||
|
// accordingly.
|
||||||
|
func (rl *Instance) editorInput(r []rune) {
|
||||||
|
switch rl.modeViMode {
|
||||||
|
case VimKeys:
|
||||||
|
rl.vi(r[0])
|
||||||
|
rl.refreshVimStatus()
|
||||||
|
|
||||||
|
case VimDelete:
|
||||||
|
rl.viDelete(r[0])
|
||||||
|
rl.refreshVimStatus()
|
||||||
|
|
||||||
|
case VimReplaceOnce:
|
||||||
|
rl.modeViMode = VimKeys
|
||||||
|
rl.deleteX()
|
||||||
|
rl.insert([]rune{r[0]})
|
||||||
|
rl.refreshVimStatus()
|
||||||
|
|
||||||
|
case VimReplaceMany:
|
||||||
|
for _, char := range r {
|
||||||
|
rl.deleteX()
|
||||||
|
rl.insert([]rune{char})
|
||||||
|
}
|
||||||
|
rl.refreshVimStatus()
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For some reason Ctrl+k messes with the input line, so ignore it.
|
||||||
|
if r[0] == 11 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We reset the history nav counter each time we come here:
|
||||||
|
// We don't need it when inserting text.
|
||||||
|
rl.histNavIdx = 0
|
||||||
|
rl.insert(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rl.multisplit) == 0 {
|
||||||
|
rl.syntaxCompletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// viEscape - In case th user is using Vim input, and the escape sequence has not
|
||||||
|
// been handled by other cases, we dispatch it to Vim and handle a few cases here.
|
||||||
|
func (rl *Instance) viEscape(r []rune) {
|
||||||
|
|
||||||
|
// Sometimes the escape sequence is interleaved with another one,
|
||||||
|
// but key strokes might be in the wrong order, so we double check
|
||||||
|
// and escape the mode only if needed.
|
||||||
|
if rl.modeViMode == VimInsert && len(r) == 1 && r[0] == 27 {
|
||||||
|
if len(rl.line) > 0 && rl.pos > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
rl.modeViMode = VimKeys
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.refreshVimStatus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) escapeSeq(r []rune) {
|
||||||
|
switch string(r) {
|
||||||
|
// Vim escape sequences & dispatching --------------------------------------------------------
|
||||||
|
case string(charEscape):
|
||||||
|
switch {
|
||||||
|
case rl.modeAutoFind:
|
||||||
|
rl.resetTabFind()
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
case rl.modeTabFind:
|
||||||
|
rl.resetTabFind()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
|
||||||
|
case rl.modeTabCompletion:
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
default:
|
||||||
|
// No matter the input mode, we exit
|
||||||
|
// any completion confirm if there's one.
|
||||||
|
if rl.compConfirmWait {
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are in Vim mode, the escape key has its usage.
|
||||||
|
// Otherwise in emacs mode the escape key does nothing.
|
||||||
|
if rl.InputMode == Vim {
|
||||||
|
rl.viEscape(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This refreshed and actually prints the new Vim status
|
||||||
|
// if we have indeed change the Vim mode.
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
// Tab completion movements ------------------------------------------------------------------
|
||||||
|
case seqShiftTab:
|
||||||
|
if rl.modeTabCompletion && !rl.compConfirmWait {
|
||||||
|
rl.tabCompletionReverse = true
|
||||||
|
rl.moveTabCompletionHighlight(-1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.tabCompletionReverse = false
|
||||||
|
rl.renderHelpers()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case seqUp:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.tabCompletionSelect = true
|
||||||
|
rl.tabCompletionReverse = true
|
||||||
|
rl.moveTabCompletionHighlight(-1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.tabCompletionReverse = false
|
||||||
|
rl.renderHelpers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.mainHist = true
|
||||||
|
rl.walkHistory(1)
|
||||||
|
|
||||||
|
case seqDown:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.tabCompletionSelect = true
|
||||||
|
rl.moveTabCompletionHighlight(1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.renderHelpers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.mainHist = true
|
||||||
|
rl.walkHistory(-1)
|
||||||
|
|
||||||
|
case seqForwards:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.tabCompletionSelect = true
|
||||||
|
rl.moveTabCompletionHighlight(1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.renderHelpers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
|
||||||
|
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
|
||||||
|
moveCursorForwards(1)
|
||||||
|
rl.pos++
|
||||||
|
}
|
||||||
|
rl.updateHelpers()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case seqBackwards:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.tabCompletionSelect = true
|
||||||
|
rl.tabCompletionReverse = true
|
||||||
|
rl.moveTabCompletionHighlight(-1, 0)
|
||||||
|
rl.updateVirtualComp()
|
||||||
|
rl.tabCompletionReverse = false
|
||||||
|
rl.renderHelpers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rl.pos > 0 {
|
||||||
|
moveCursorBackwards(1)
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
// Registers -------------------------------------------------------------------------------
|
||||||
|
case seqAltQuote:
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.modeTabCompletion = true
|
||||||
|
rl.modeAutoFind = true
|
||||||
|
rl.searchMode = RegisterFind
|
||||||
|
// Else we might be asked to confirm printing (if too many suggestions), or not.
|
||||||
|
rl.getTabCompletion()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.renderHelpers()
|
||||||
|
|
||||||
|
// Movement -------------------------------------------------------------------------------
|
||||||
|
case seqCtrlLeftArrow:
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpB(tokeniseLine))
|
||||||
|
rl.updateHelpers()
|
||||||
|
return
|
||||||
|
case seqCtrlRightArrow:
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
|
||||||
|
rl.updateHelpers()
|
||||||
|
return
|
||||||
|
|
||||||
|
case seqDelete:
|
||||||
|
if rl.modeTabFind {
|
||||||
|
rl.backspaceTabFind()
|
||||||
|
} else {
|
||||||
|
rl.deleteBackspace()
|
||||||
|
}
|
||||||
|
case seqHome, seqHomeSc:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
moveCursorBackwards(rl.pos)
|
||||||
|
rl.pos = 0
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case seqEnd, seqEndSc:
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
moveCursorForwards(len(rl.line) - rl.pos)
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case seqAltR:
|
||||||
|
rl.resetVirtualComp(false)
|
||||||
|
// For some modes only, if we are in vim Keys mode,
|
||||||
|
// we toogle back to insert mode. For others, we return
|
||||||
|
// without getting the completions.
|
||||||
|
if rl.modeViMode != VimInsert {
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.mainHist = false // true before
|
||||||
|
rl.searchMode = HistoryFind
|
||||||
|
rl.modeAutoFind = true
|
||||||
|
rl.modeTabCompletion = true
|
||||||
|
|
||||||
|
rl.modeTabFind = true
|
||||||
|
rl.updateTabFind([]rune{})
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
if rl.modeTabFind {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// alt+numeric append / delete
|
||||||
|
if len(r) == 2 && '1' <= r[1] && r[1] <= '9' {
|
||||||
|
if rl.modeViMode == VimDelete {
|
||||||
|
rl.viDelete(r[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line, err := rl.mainHistory.GetLine(rl.mainHistory.Len() - 1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !rl.mainHist {
|
||||||
|
line, err = rl.altHistory.GetLine(rl.altHistory.Len() - 1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens, _, _ := tokeniseSplitSpaces([]rune(line), 0)
|
||||||
|
pos := int(r[1]) - 48 // convert ASCII to integer
|
||||||
|
if pos > len(tokens) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.insert([]rune(tokens[pos-1]))
|
||||||
|
} else {
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) carridgeReturn() {
|
||||||
|
rl.clearHelpers()
|
||||||
|
print("\r\n")
|
||||||
|
if rl.HistoryAutoWrite {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Main history
|
||||||
|
if rl.mainHistory != nil {
|
||||||
|
rl.histPos, err = rl.mainHistory.Write(string(rl.line))
|
||||||
|
if err != nil {
|
||||||
|
print(err.Error() + "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Alternative history
|
||||||
|
if rl.altHistory != nil {
|
||||||
|
rl.histPos, err = rl.altHistory.Write(string(rl.line))
|
||||||
|
if err != nil {
|
||||||
|
print(err.Error() + "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMultiline(r []rune) bool {
|
||||||
|
for i := range r {
|
||||||
|
if (r[i] == '\r' || r[i] == '\n') && i != len(r)-1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) allowMultiline(data []byte) bool {
|
||||||
|
rl.clearHelpers()
|
||||||
|
printf("\r\nWARNING: %d bytes of multiline data was dumped into the shell!", len(data))
|
||||||
|
for {
|
||||||
|
print("\r\nDo you wish to proceed (yes|no|preview)? [y/n/p] ")
|
||||||
|
|
||||||
|
b := make([]byte, 1024)
|
||||||
|
|
||||||
|
i, err := os.Stdin.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(b[:i])
|
||||||
|
print(s)
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "y", "Y":
|
||||||
|
print("\r\n" + rl.mainPrompt)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "n", "N":
|
||||||
|
print("\r\n" + rl.mainPrompt)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case "p", "P":
|
||||||
|
preview := string(bytes.Replace(data, []byte{'\r'}, []byte{'\r', '\n'}, -1))
|
||||||
|
if rl.SyntaxHighlighter != nil {
|
||||||
|
preview = rl.SyntaxHighlighter([]rune(preview))
|
||||||
|
}
|
||||||
|
print("\r\n" + preview)
|
||||||
|
|
||||||
|
default:
|
||||||
|
print("\r\nInvalid response. Please answer `y` (yes), `n` (no) or `p` (preview)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,325 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/evilsocket/islazy/tui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registers - Contains all memory registers resulting from delete/paste/search
|
||||||
|
// or other operations in the command line input.
|
||||||
|
type registers struct {
|
||||||
|
unnamed []rune // Unnamed register, used by default
|
||||||
|
num map[int][]rune // numbered registers (0-9)
|
||||||
|
alpha map[string][]rune // lettered registers ( a-z )
|
||||||
|
ro map[string][]rune // read-only registers ( . % : )
|
||||||
|
registerSelectWait bool // The user wants to use a still unidentified register
|
||||||
|
onRegister bool // We have identified the register, and acting on it.
|
||||||
|
currentRegister rune // Any of the read/write registers ("/num/alpha)
|
||||||
|
mutex *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) initRegisters() {
|
||||||
|
rl.registers = ®isters{
|
||||||
|
num: make(map[int][]rune, 10),
|
||||||
|
alpha: make(map[string][]rune, 52),
|
||||||
|
ro: map[string][]rune{},
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveToRegister - Passing a function that will move around the line in the desired way, we get
|
||||||
|
// the number of Vim iterations and we save the resulting string to the appropriate buffer.
|
||||||
|
// It's the same as saveToRegisterTokenize, but without the need to generate tokenized &
|
||||||
|
// cursor-pos-actualized versions of the input line.
|
||||||
|
func (rl *Instance) saveToRegister(adjust int) {
|
||||||
|
|
||||||
|
// Get the current cursor position and go the length specified.
|
||||||
|
var begin = rl.pos
|
||||||
|
var end = rl.pos
|
||||||
|
end += adjust
|
||||||
|
if end > len(rl.line)-1 {
|
||||||
|
end = len(rl.line)
|
||||||
|
} else if end < 0 {
|
||||||
|
end = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer []rune
|
||||||
|
if end < begin {
|
||||||
|
buffer = rl.line[end:begin]
|
||||||
|
} else {
|
||||||
|
buffer = rl.line[begin:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an immutable copy of the buffer before saving it
|
||||||
|
buf := string(buffer)
|
||||||
|
|
||||||
|
// Put the buffer in the appropriate registers.
|
||||||
|
// By default, always in the unnamed one first.
|
||||||
|
rl.saveBufToRegister([]rune(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveToRegisterTokenize - Passing a function that will move around the line in the desired way, we get
|
||||||
|
// the number of Vim iterations and we save the resulting string to the appropriate buffer. Because we
|
||||||
|
// need the cursor position to be really moved around between calls to the jumper, we also need the tokeniser.
|
||||||
|
func (rl *Instance) saveToRegisterTokenize(tokeniser tokeniser, jumper func(tokeniser) int, vii int) {
|
||||||
|
|
||||||
|
// The register is going to have to heavily manipulate the cursor position.
|
||||||
|
// Remember the original one first, for the end.
|
||||||
|
var beginPos = rl.pos
|
||||||
|
|
||||||
|
// Get the current cursor position and go the length specified.
|
||||||
|
var begin = rl.pos
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(jumper(tokeniser))
|
||||||
|
}
|
||||||
|
var end = rl.pos
|
||||||
|
rl.pos = beginPos
|
||||||
|
|
||||||
|
if end > len(rl.line)-1 {
|
||||||
|
end = len(rl.line)
|
||||||
|
} else if end < 0 {
|
||||||
|
end = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer []rune
|
||||||
|
if end < begin {
|
||||||
|
buffer = rl.line[end:begin]
|
||||||
|
} else {
|
||||||
|
buffer = rl.line[begin:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an immutable copy of the buffer before saving it
|
||||||
|
buf := string(buffer)
|
||||||
|
|
||||||
|
// Put the buffer in the appropriate registers.
|
||||||
|
// By default, always in the unnamed one first.
|
||||||
|
rl.saveBufToRegister([]rune(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveBufToRegister - Instead of computing the buffer ourselves based on an adjust,
|
||||||
|
// let the caller pass directly this buffer, yet relying on the register system to
|
||||||
|
// determine which register will store the buffer.
|
||||||
|
func (rl *Instance) saveBufToRegister(buffer []rune) {
|
||||||
|
|
||||||
|
// We must make an immutable version of the buffer first.
|
||||||
|
buf := string(buffer)
|
||||||
|
|
||||||
|
// When exiting this function the currently selected register is dropped,
|
||||||
|
defer rl.registers.resetRegister()
|
||||||
|
|
||||||
|
// If the buffer is empty, just return
|
||||||
|
if len(buffer) == 0 || buf == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the buffer in the appropriate registers.
|
||||||
|
// By default, always in the unnamed one first.
|
||||||
|
rl.registers.unnamed = []rune(buf)
|
||||||
|
|
||||||
|
// If there is an active register, directly give it the buffer.
|
||||||
|
// Check if its a numbered or lettered register, and put it in.
|
||||||
|
if rl.registers.onRegister {
|
||||||
|
num, err := strconv.Atoi(string(rl.registers.currentRegister))
|
||||||
|
if err == nil && num < 10 {
|
||||||
|
rl.registers.writeNumberedRegister(num, []rune(buf), false)
|
||||||
|
} else if err != nil {
|
||||||
|
rl.registers.writeAlphaRegister([]rune(buf))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Or, if no active register and if there is room on the numbered ones,
|
||||||
|
rl.registers.writeNumberedRegister(0, []rune(buf), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user asked to paste a buffer onto the line, so we check from which register
|
||||||
|
// we are supposed to select the buffer, and return it to the caller for insertion.
|
||||||
|
func (rl *Instance) pasteFromRegister() (buffer []rune) {
|
||||||
|
|
||||||
|
// When exiting this function the currently selected register is dropped,
|
||||||
|
defer rl.registers.resetRegister()
|
||||||
|
|
||||||
|
// If no actively selected register, return the unnamed buffer
|
||||||
|
if !rl.registers.registerSelectWait && !rl.registers.onRegister {
|
||||||
|
return rl.registers.unnamed
|
||||||
|
}
|
||||||
|
activeRegister := string(rl.registers.currentRegister)
|
||||||
|
|
||||||
|
// Else find the active register, and return its content.
|
||||||
|
num, err := strconv.Atoi(activeRegister)
|
||||||
|
|
||||||
|
// Either from the numbered ones.
|
||||||
|
if err == nil {
|
||||||
|
buf, found := rl.registers.num[num]
|
||||||
|
if found {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// or the lettered ones
|
||||||
|
buf, found := rl.registers.alpha[activeRegister]
|
||||||
|
if found {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
// Or the read-only ones
|
||||||
|
buf, found = rl.registers.ro[activeRegister]
|
||||||
|
if found {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// setActiveRegister - The user has typed "<regiserID>, and we don't know yet
|
||||||
|
// if we are about to copy to/from it, so we just set as active, so that when
|
||||||
|
// the action to perform on it will be asked, we know which one to use.
|
||||||
|
func (r *registers) setActiveRegister(reg rune) {
|
||||||
|
defer func() {
|
||||||
|
// We now have an active, identified register
|
||||||
|
r.registerSelectWait = false
|
||||||
|
r.onRegister = true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Numbered
|
||||||
|
num, err := strconv.Atoi(string(reg))
|
||||||
|
if err == nil && num < 10 {
|
||||||
|
r.currentRegister = reg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Read-only
|
||||||
|
_, found := r.ro[string(reg)]
|
||||||
|
if found {
|
||||||
|
r.currentRegister = reg
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, lettered
|
||||||
|
r.currentRegister = reg
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeNumberedRegister - Add a buffer to one of the numbered registers
|
||||||
|
// Pass a number above 10 to indicate we just push it on the num stack.
|
||||||
|
func (r *registers) writeNumberedRegister(idx int, buf []rune, push bool) {
|
||||||
|
// No numbered register above 10
|
||||||
|
if len(r.num) > 10 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// No push to the stack if we are already using 9
|
||||||
|
var max int
|
||||||
|
if push {
|
||||||
|
for i := range r.num {
|
||||||
|
if i > max && string(r.num[i]) != string(buf) {
|
||||||
|
max = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if max < 9 {
|
||||||
|
r.num[max+1] = buf
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add to the stack with the specified register
|
||||||
|
r.num[idx] = buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeAlphaRegister - Either adds a buffer to a new/existing letterd register,
|
||||||
|
// or appends to this new/existing register if the currently active register is
|
||||||
|
// the uppercase letter for this register.
|
||||||
|
func (r *registers) writeAlphaRegister(buffer []rune) {
|
||||||
|
appendRegs := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
appended := false
|
||||||
|
for _, char := range appendRegs {
|
||||||
|
if char == r.currentRegister {
|
||||||
|
real := strings.ToLower(string(r.currentRegister))
|
||||||
|
_, exists := r.alpha[real]
|
||||||
|
if exists {
|
||||||
|
r.alpha[real] = append(r.alpha[real], buffer...)
|
||||||
|
} else {
|
||||||
|
r.alpha[real] = buffer
|
||||||
|
}
|
||||||
|
appended = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !appended {
|
||||||
|
r.alpha[string(r.currentRegister)] = buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetRegister - there is no currently active register anymore,
|
||||||
|
// nor we are currently setting one as active.
|
||||||
|
func (r *registers) resetRegister() {
|
||||||
|
r.currentRegister = ' '
|
||||||
|
r.registerSelectWait = false
|
||||||
|
r.onRegister = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user can show registers completions and insert, no matter the cursor position.
|
||||||
|
func (rl *Instance) completeRegisters() (groups []*CompletionGroup) {
|
||||||
|
|
||||||
|
// We set the hint exceptionally
|
||||||
|
hint := BLUE + "-- registers --" + RESET
|
||||||
|
rl.hintText = []rune(hint)
|
||||||
|
|
||||||
|
// Make the groups
|
||||||
|
anonRegs := &CompletionGroup{
|
||||||
|
DisplayType: TabDisplayMap,
|
||||||
|
MaxLength: 20,
|
||||||
|
Descriptions: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unnamed (the added space is because we must have a unique key.
|
||||||
|
// This space is trimmed when the buffer is being passed to users)
|
||||||
|
anonRegs.Suggestions = append(anonRegs.Suggestions, string(rl.registers.unnamed))
|
||||||
|
anonRegs.Descriptions[string(rl.registers.unnamed)] = DIM + "\"" + "\"" + RESET
|
||||||
|
|
||||||
|
groups = append(groups, anonRegs)
|
||||||
|
|
||||||
|
// Numbered registers
|
||||||
|
numRegs := &CompletionGroup{
|
||||||
|
Name: tui.DIM + "num ([0-9])" + tui.RESET,
|
||||||
|
DisplayType: TabDisplayMap,
|
||||||
|
MaxLength: 20,
|
||||||
|
Descriptions: map[string]string{},
|
||||||
|
}
|
||||||
|
var nums []int
|
||||||
|
for reg := range rl.registers.num {
|
||||||
|
nums = append(nums, reg)
|
||||||
|
}
|
||||||
|
sort.Ints(nums)
|
||||||
|
for _, val := range nums {
|
||||||
|
buf := rl.registers.num[val]
|
||||||
|
numRegs.Suggestions = append(numRegs.Suggestions, string(buf))
|
||||||
|
numRegs.Descriptions[string(buf)] = fmt.Sprintf("%s\"%d%s", DIM, val, RESET)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(numRegs.Suggestions) > 0 {
|
||||||
|
groups = append(groups, numRegs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Letter registers
|
||||||
|
alphaRegs := &CompletionGroup{
|
||||||
|
Name: tui.DIM + "alpha ([a-z], [A-Z])" + tui.RESET,
|
||||||
|
DisplayType: TabDisplayMap,
|
||||||
|
MaxLength: 20,
|
||||||
|
Descriptions: map[string]string{},
|
||||||
|
}
|
||||||
|
var lett []string
|
||||||
|
for reg := range rl.registers.alpha {
|
||||||
|
lett = append(lett, reg)
|
||||||
|
}
|
||||||
|
sort.Strings(lett)
|
||||||
|
for _, reg := range lett {
|
||||||
|
buf := rl.registers.alpha[reg]
|
||||||
|
alphaRegs.Suggestions = append(alphaRegs.Suggestions, string(buf))
|
||||||
|
alphaRegs.Descriptions[string(buf)] = DIM + "\"" + reg + RESET
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(alphaRegs.Suggestions) > 0 {
|
||||||
|
groups = append(groups, alphaRegs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
// syntaxCompletion - applies syntax highlighting to the current input line.
|
||||||
|
// nothing special to note here, nor any changes envisioned.
|
||||||
|
func (rl *Instance) syntaxCompletion() {
|
||||||
|
if rl.SyntaxCompleter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newLine, newPos := rl.SyntaxCompleter(rl.line, rl.pos-1)
|
||||||
|
if string(newLine) == string(rl.line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPos++
|
||||||
|
|
||||||
|
rl.line = newLine
|
||||||
|
rl.pos = newPos
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
|
@ -0,0 +1,280 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// insertCandidateVirtual - When a completion candidate is selected, we insert it virtually in the input line:
|
||||||
|
// this will not trigger further firltering against the other candidates. Each time this function
|
||||||
|
// is called, any previous candidate is dropped, after being used for moving the cursor around.
|
||||||
|
func (rl *Instance) insertCandidateVirtual(candidate []rune) {
|
||||||
|
for {
|
||||||
|
// I don't really understand why `0` is creaping in at the end of the
|
||||||
|
// array but it only happens with unicode characters.
|
||||||
|
if len(candidate) > 1 && candidate[len(candidate)-1] == 0 {
|
||||||
|
candidate = candidate[:len(candidate)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// We place the cursor back at the beginning of the previous virtual candidate
|
||||||
|
rl.pos -= len(rl.currentComp)
|
||||||
|
|
||||||
|
// We delete the previous virtual completion, just
|
||||||
|
// like we would delete a word in vim editing mode.
|
||||||
|
if len(rl.currentComp) == 1 {
|
||||||
|
rl.deleteVirtual() // Delete a single character
|
||||||
|
} else if len(rl.currentComp) > 0 {
|
||||||
|
rl.viDeleteByAdjustVirtual(rl.viJumpEVirtual(tokeniseSplitSpaces) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We then keep a reference to the new candidate
|
||||||
|
rl.currentComp = candidate
|
||||||
|
|
||||||
|
// We should not have a remaining virtual completion
|
||||||
|
// line, so it is now identical to the real line.
|
||||||
|
rl.lineComp = rl.line
|
||||||
|
|
||||||
|
// Insert the new candidate in the virtual line.
|
||||||
|
switch {
|
||||||
|
case len(rl.lineComp) == 0:
|
||||||
|
rl.lineComp = candidate
|
||||||
|
case rl.pos == 0:
|
||||||
|
rl.lineComp = append(candidate, rl.lineComp...)
|
||||||
|
case rl.pos < len(rl.lineComp):
|
||||||
|
r := append(candidate, rl.lineComp[rl.pos:]...)
|
||||||
|
rl.lineComp = append(rl.lineComp[:rl.pos], r...)
|
||||||
|
default:
|
||||||
|
rl.lineComp = append(rl.lineComp, candidate...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We place the cursor at the end of our new virtually completed item
|
||||||
|
rl.pos += len(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the current completion candidate into the input line.
|
||||||
|
// This candidate might either be the currently selected one (white frame),
|
||||||
|
// or the only candidate available, if the total number of candidates is 1.
|
||||||
|
func (rl *Instance) insertCandidate() {
|
||||||
|
|
||||||
|
cur := rl.getCurrentGroup()
|
||||||
|
|
||||||
|
if cur != nil {
|
||||||
|
completion := cur.getCurrentCell(rl)
|
||||||
|
prefix := len(rl.tcPrefix)
|
||||||
|
|
||||||
|
// Special case for the only special escape, which
|
||||||
|
// if not handled, will make us insert the first
|
||||||
|
// character of our actual rl.tcPrefix in the candidate.
|
||||||
|
if strings.HasPrefix(string(rl.tcPrefix), "%") {
|
||||||
|
prefix++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no indexing error happens with prefix
|
||||||
|
if len(completion) >= prefix {
|
||||||
|
rl.insert([]rune(completion[prefix:]))
|
||||||
|
if !cur.TrimSlash && !cur.NoSpace {
|
||||||
|
rl.insert([]rune(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateVirtualComp - Either insert the current completion candidate virtually, or on the real line.
|
||||||
|
func (rl *Instance) updateVirtualComp() {
|
||||||
|
cur := rl.getCurrentGroup()
|
||||||
|
if cur != nil {
|
||||||
|
|
||||||
|
completion := cur.getCurrentCell(rl)
|
||||||
|
prefix := len(rl.tcPrefix)
|
||||||
|
|
||||||
|
// If the total number of completions is one, automatically insert it.
|
||||||
|
if rl.hasOneCandidate() {
|
||||||
|
rl.insertCandidate()
|
||||||
|
// Quit the tab completion mode to avoid asking to the user to press
|
||||||
|
// Enter twice to actually run the command
|
||||||
|
// Refresh first, and then quit the completion mode
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Special case for the only special escape, which
|
||||||
|
// if not handled, will make us insert the first
|
||||||
|
// character of our actual rl.tcPrefix in the candidate.
|
||||||
|
if strings.HasPrefix(string(rl.tcPrefix), "%") {
|
||||||
|
prefix++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or insert it virtually.
|
||||||
|
if len(completion) >= prefix {
|
||||||
|
rl.insertCandidateVirtual([]rune(completion[prefix:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetVirtualComp - This function is called before most of our readline key handlers,
|
||||||
|
// and makes sure that the current completion (virtually inserted) is either inserted or dropped,
|
||||||
|
// and that all related parameters are reinitialized.
|
||||||
|
func (rl *Instance) resetVirtualComp(drop bool) {
|
||||||
|
|
||||||
|
// If we don't have a current virtual completion, there's nothing to do.
|
||||||
|
// IMPORTANT: this MUST be first, to avoid nil problems with empty comps.
|
||||||
|
if len(rl.currentComp) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current candidate and its group.
|
||||||
|
//It contains info on how we must process it
|
||||||
|
cur := rl.getCurrentGroup()
|
||||||
|
if cur == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion := cur.getCurrentCell(rl)
|
||||||
|
// Avoid problems with empty completions
|
||||||
|
if completion == "" {
|
||||||
|
rl.clearVirtualComp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will only insert the net difference between prefix and completion.
|
||||||
|
prefix := len(rl.tcPrefix)
|
||||||
|
// Special case for the only special escape, which
|
||||||
|
// if not handled, will make us insert the first
|
||||||
|
// character of our actual rl.tcPrefix in the candidate.
|
||||||
|
if strings.HasPrefix(string(rl.tcPrefix), "%") {
|
||||||
|
prefix++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are asked to drop the completion, move it away from the line and return.
|
||||||
|
if drop {
|
||||||
|
rl.pos -= len([]rune(completion[prefix:]))
|
||||||
|
rl.lineComp = rl.line
|
||||||
|
rl.clearVirtualComp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the current candidate. A bit of processing happens:
|
||||||
|
// - We trim the trailing slash if needed
|
||||||
|
// - We add a space only if the group has not explicitely specified not to add one.
|
||||||
|
if cur.TrimSlash {
|
||||||
|
trimmed, hadSlash := trimTrailing(completion)
|
||||||
|
if !hadSlash && rl.compAddSpace {
|
||||||
|
if !cur.NoSpace {
|
||||||
|
trimmed = trimmed + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.insertCandidateVirtual([]rune(trimmed[prefix:]))
|
||||||
|
} else {
|
||||||
|
if rl.compAddSpace {
|
||||||
|
if !cur.NoSpace {
|
||||||
|
completion = completion + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.insertCandidateVirtual([]rune(completion[prefix:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset virtual
|
||||||
|
rl.clearVirtualComp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimTrailing - When the group to which the current candidate
|
||||||
|
// belongs has TrimSlash enabled, we process the candidate.
|
||||||
|
// This is used when the completions are directory/file paths.
|
||||||
|
func trimTrailing(comp string) (trimmed string, hadSlash bool) {
|
||||||
|
// Unix paths
|
||||||
|
if strings.HasSuffix(comp, "/") {
|
||||||
|
return strings.TrimSuffix(comp, "/"), true
|
||||||
|
}
|
||||||
|
// Windows paths
|
||||||
|
if strings.HasSuffix(comp, "\\") {
|
||||||
|
return strings.TrimSuffix(comp, "\\"), true
|
||||||
|
}
|
||||||
|
return comp, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// viDeleteByAdjustVirtual - Same as viDeleteByAdjust, but for our virtually completed input line.
|
||||||
|
func (rl *Instance) viDeleteByAdjustVirtual(adjust int) {
|
||||||
|
var (
|
||||||
|
newLine []rune
|
||||||
|
backOne bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Avoid doing anything if input line is empty.
|
||||||
|
if len(rl.lineComp) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case adjust == 0:
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
return
|
||||||
|
case rl.pos+adjust == len(rl.lineComp)-1:
|
||||||
|
newLine = rl.lineComp[:rl.pos]
|
||||||
|
// backOne = true // Deleted, otherwise the completion moves back when we don't want to.
|
||||||
|
case rl.pos+adjust == 0:
|
||||||
|
newLine = rl.lineComp[rl.pos:]
|
||||||
|
case adjust < 0:
|
||||||
|
newLine = append(rl.lineComp[:rl.pos+adjust], rl.lineComp[rl.pos:]...)
|
||||||
|
default:
|
||||||
|
newLine = append(rl.lineComp[:rl.pos], rl.lineComp[rl.pos+adjust:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have our new line completed
|
||||||
|
rl.lineComp = newLine
|
||||||
|
|
||||||
|
if adjust < 0 {
|
||||||
|
rl.moveCursorByAdjust(adjust)
|
||||||
|
}
|
||||||
|
|
||||||
|
if backOne {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// viJumpEVirtual - Same as viJumpE, but for our virtually completed input line.
|
||||||
|
func (rl *Instance) viJumpEVirtual(tokeniser func([]rune, int) ([]string, int, int)) (adjust int) {
|
||||||
|
split, index, pos := tokeniser(rl.lineComp, rl.pos)
|
||||||
|
if len(split) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
word := rTrimWhiteSpace(split[index])
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(split) == 0:
|
||||||
|
return
|
||||||
|
case index == len(split)-1 && pos >= len(word)-1:
|
||||||
|
return
|
||||||
|
case pos >= len(word)-1:
|
||||||
|
word = rTrimWhiteSpace(split[index+1])
|
||||||
|
adjust = len(split[index]) - pos
|
||||||
|
adjust += len(word) - 1
|
||||||
|
default:
|
||||||
|
adjust = len(word) - pos - 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) deleteVirtual() {
|
||||||
|
switch {
|
||||||
|
case len(rl.lineComp) == 0:
|
||||||
|
return
|
||||||
|
case rl.pos == 0:
|
||||||
|
rl.lineComp = rl.lineComp[1:]
|
||||||
|
case rl.pos > len(rl.lineComp):
|
||||||
|
case rl.pos == len(rl.lineComp):
|
||||||
|
rl.lineComp = rl.lineComp[:rl.pos]
|
||||||
|
default:
|
||||||
|
rl.lineComp = append(rl.lineComp[:rl.pos], rl.lineComp[rl.pos+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are done with the current virtual completion candidate.
|
||||||
|
// Get ready for the next one
|
||||||
|
func (rl *Instance) clearVirtualComp() {
|
||||||
|
rl.line = rl.lineComp
|
||||||
|
rl.currentComp = []rune{}
|
||||||
|
rl.compAddSpace = false
|
||||||
|
}
|
|
@ -0,0 +1,557 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TabDisplayType defines how the autocomplete suggestions display
|
||||||
|
type TabDisplayType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TabDisplayGrid is the default. It's where the screen below the prompt is
|
||||||
|
// divided into a grid with each suggestion occupying an individual cell.
|
||||||
|
TabDisplayGrid = iota
|
||||||
|
|
||||||
|
// TabDisplayList is where suggestions are displayed as a list with a
|
||||||
|
// description. The suggestion gets highlighted but both are searchable (ctrl+f)
|
||||||
|
TabDisplayList
|
||||||
|
|
||||||
|
// TabDisplayMap is where suggestions are displayed as a list with a
|
||||||
|
// description however the description is what gets highlighted and only
|
||||||
|
// that is searchable (ctrl+f). The benefit of TabDisplayMap is when your
|
||||||
|
// autocomplete suggestions are IDs rather than human terms.
|
||||||
|
TabDisplayMap
|
||||||
|
)
|
||||||
|
|
||||||
|
// getTabCompletion - This root function sets up all completion items and engines,
|
||||||
|
// dealing with all search and completion modes. But it does not perform printing.
|
||||||
|
func (rl *Instance) getTabCompletion() {
|
||||||
|
|
||||||
|
// Populate registers if requested.
|
||||||
|
if rl.modeAutoFind && rl.searchMode == RegisterFind {
|
||||||
|
rl.getRegisterCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate for completion search if in this mode
|
||||||
|
if rl.modeAutoFind && rl.searchMode == CompletionFind {
|
||||||
|
rl.getTabSearchCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate for History search if in this mode
|
||||||
|
if rl.modeAutoFind && rl.searchMode == HistoryFind {
|
||||||
|
rl.getHistorySearchCompletion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, yield normal completions
|
||||||
|
rl.getNormalCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRegisterCompletion - Populates and sets up completion for Vim registers.
|
||||||
|
func (rl *Instance) getRegisterCompletion() {
|
||||||
|
|
||||||
|
rl.tcGroups = rl.completeRegisters()
|
||||||
|
if len(rl.tcGroups) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups
|
||||||
|
|
||||||
|
// Adjust the index for each group after the first:
|
||||||
|
// this ensures no latency when we will move around them.
|
||||||
|
for i, group := range rl.tcGroups {
|
||||||
|
group.init(rl)
|
||||||
|
if i != 0 {
|
||||||
|
group.tcPosY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there aren't ANY completion candidates, we
|
||||||
|
// escape the completion mode from here directly.
|
||||||
|
var items bool
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
if len(group.Suggestions) > 0 {
|
||||||
|
items = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !items {
|
||||||
|
rl.modeTabCompletion = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTabSearchCompletion - Populates and sets up completion for completion search.
|
||||||
|
func (rl *Instance) getTabSearchCompletion() {
|
||||||
|
|
||||||
|
// Get completions from the engine, and make sure there is a current group.
|
||||||
|
rl.getCompletions()
|
||||||
|
if len(rl.tcGroups) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.getCurrentGroup()
|
||||||
|
|
||||||
|
// Set the hint for this completion mode
|
||||||
|
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
|
||||||
|
|
||||||
|
// Set the hint for this completion mode
|
||||||
|
rl.hintText = append([]rune("Completion search: "), rl.tfLine...)
|
||||||
|
|
||||||
|
for _, g := range rl.tcGroups {
|
||||||
|
g.updateTabFind(rl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If total number of matches is zero, we directly change the hint, and return
|
||||||
|
if comps, _, _ := rl.getCompletionCount(); comps == 0 {
|
||||||
|
rl.hintText = append(rl.hintText, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHistorySearchCompletion - Populates and sets up completion for command history search
|
||||||
|
func (rl *Instance) getHistorySearchCompletion() {
|
||||||
|
|
||||||
|
// Refresh full list each time
|
||||||
|
rl.tcGroups = rl.completeHistory()
|
||||||
|
if len(rl.tcGroups) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.tcGroups = checkNilItems(rl.tcGroups) // Avoid nil maps in groups
|
||||||
|
rl.getCurrentGroup() // Make sure there is a current group
|
||||||
|
|
||||||
|
// The history hint is already set, but overwrite it if we don't have completions
|
||||||
|
if len(rl.tcGroups[0].Suggestions) == 0 {
|
||||||
|
rl.histHint = []rune(fmt.Sprintf("%s%s%s %s", DIM, RED,
|
||||||
|
"No command history source, or empty (Ctrl-G/Esc to cancel)", RESET))
|
||||||
|
rl.hintText = rl.histHint
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the hint line with everything
|
||||||
|
rl.histHint = append([]rune("\033[38;5;183m"+string(rl.histHint)+RESET), rl.tfLine...)
|
||||||
|
rl.histHint = append(rl.histHint, []rune(RESET)...)
|
||||||
|
rl.hintText = rl.histHint
|
||||||
|
|
||||||
|
// Refresh filtered candidates
|
||||||
|
rl.tcGroups[0].updateTabFind(rl)
|
||||||
|
|
||||||
|
// If no items matched history, add hint text that we failed to search
|
||||||
|
if len(rl.tcGroups[0].Suggestions) == 0 {
|
||||||
|
rl.hintText = append(rl.histHint, []rune(DIM+RED+" ! no matches (Ctrl-G/Esc to cancel)"+RESET)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNormalCompletion - Populates and sets up completion for normal comp mode.
|
||||||
|
// Will automatically cancel the completion mode if there are no candidates.
|
||||||
|
func (rl *Instance) getNormalCompletion() {
|
||||||
|
|
||||||
|
// Get completions groups, pass delayedTabContext and check nils
|
||||||
|
rl.getCompletions()
|
||||||
|
|
||||||
|
// Adjust the index for each group after the first:
|
||||||
|
// this ensures no latency when we will move around them.
|
||||||
|
for i, group := range rl.tcGroups {
|
||||||
|
group.init(rl)
|
||||||
|
if i != 0 {
|
||||||
|
group.tcPosY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there aren't ANY completion candidates, we
|
||||||
|
// escape the completion mode from here directly.
|
||||||
|
var items bool
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
if len(group.Suggestions) > 0 {
|
||||||
|
items = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !items {
|
||||||
|
rl.modeTabCompletion = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCompletions - Calls the completion engine/function to yield a list of 0 or more completion groups,
|
||||||
|
// sets up a delayed tab context and passes it on to the tab completion engine function, and ensure no
|
||||||
|
// nil groups/items will pass through. This function is called by different comp search/nav modes.
|
||||||
|
func (rl *Instance) getCompletions() {
|
||||||
|
|
||||||
|
// If there is no wired tab completion engine, nothing we can do.
|
||||||
|
if rl.TabCompleter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any existing tab context first.
|
||||||
|
if rl.delayedTabContext.cancel != nil {
|
||||||
|
rl.delayedTabContext.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate a new context
|
||||||
|
rl.delayedTabContext = DelayedTabContext{rl: rl}
|
||||||
|
rl.delayedTabContext.Context, rl.delayedTabContext.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// Get the correct line to be completed, and the current cursor position
|
||||||
|
compLine, compPos := rl.getCompletionLine()
|
||||||
|
|
||||||
|
// Call up the completion engine/function to yield completion groups
|
||||||
|
rl.tcPrefix, rl.tcGroups = rl.TabCompleter(compLine, compPos, rl.delayedTabContext)
|
||||||
|
|
||||||
|
// Avoid nil maps in groups. Maybe we could also pop any empty group.
|
||||||
|
rl.tcGroups = checkNilItems(rl.tcGroups)
|
||||||
|
|
||||||
|
// We have been loading fresh completion sin this function,
|
||||||
|
// so adjust the positions for each group, so that cycling
|
||||||
|
// correctly occurs in both directions (tab/shift+tab)
|
||||||
|
for i, group := range rl.tcGroups {
|
||||||
|
if i > 0 {
|
||||||
|
switch group.DisplayType {
|
||||||
|
case TabDisplayGrid:
|
||||||
|
group.tcPosX = 1
|
||||||
|
case TabDisplayList, TabDisplayMap:
|
||||||
|
group.tcPosY = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveTabCompletionHighlight - This function is in charge of
|
||||||
|
// computing the new position in the current completions liste.
|
||||||
|
func (rl *Instance) moveTabCompletionHighlight(x, y int) {
|
||||||
|
|
||||||
|
g := rl.getCurrentGroup()
|
||||||
|
|
||||||
|
// If there is no current group, we leave any current completion mode.
|
||||||
|
if g == nil || g.Suggestions == nil {
|
||||||
|
rl.modeTabCompletion = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// done means we need to find the next/previous group.
|
||||||
|
// next determines if we need to get the next OR previous group.
|
||||||
|
var done, next bool
|
||||||
|
|
||||||
|
// Depending on the display, we only keep track of x or (x and y)
|
||||||
|
switch g.DisplayType {
|
||||||
|
case TabDisplayGrid:
|
||||||
|
done, next = g.moveTabGridHighlight(rl, x, y)
|
||||||
|
case TabDisplayList:
|
||||||
|
done, next = g.moveTabListHighlight(rl, x, y)
|
||||||
|
case TabDisplayMap:
|
||||||
|
done, next = g.moveTabMapHighlight(rl, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle to next/previous group, if done with current one.
|
||||||
|
if done {
|
||||||
|
if next {
|
||||||
|
rl.cycleNextGroup()
|
||||||
|
nextGroup := rl.getCurrentGroup()
|
||||||
|
nextGroup.goFirstCell()
|
||||||
|
} else {
|
||||||
|
rl.cyclePreviousGroup()
|
||||||
|
prevGroup := rl.getCurrentGroup()
|
||||||
|
prevGroup.goLastCell()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTabCompletion - Prints all completion groups and their items
|
||||||
|
func (rl *Instance) writeTabCompletion() {
|
||||||
|
|
||||||
|
// The final completions string to print.
|
||||||
|
var completions string
|
||||||
|
|
||||||
|
// This stablizes the completion printing just beyond the input line
|
||||||
|
rl.tcUsedY = 0
|
||||||
|
|
||||||
|
// Safecheck
|
||||||
|
if !rl.modeTabCompletion {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any case, we write the completions strings, trimmed for redundant
|
||||||
|
// newline occurences that have been put at the end of each group.
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
completions += group.writeCompletion(rl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because some completion groups might have more suggestions
|
||||||
|
// than what their MaxLength allows them to, cycling sometimes occur,
|
||||||
|
// but does not fully clears itself: some descriptions are messed up with.
|
||||||
|
// We always clear the screen as a result, between writings.
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Crop the completions so that it fits within our MaxTabCompleterRows
|
||||||
|
completions, rl.tcUsedY = rl.cropCompletions(completions)
|
||||||
|
|
||||||
|
// Then we print all of them.
|
||||||
|
fmt.Printf(completions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cropCompletions - When the user cycles through a completion list longer
|
||||||
|
// than the console MaxTabCompleterRows value, we crop the completions string
|
||||||
|
// so that "global" cycling (across all groups) is printed correctly.
|
||||||
|
func (rl *Instance) cropCompletions(comps string) (cropped string, usedY int) {
|
||||||
|
|
||||||
|
// If we actually fit into the MaxTabCompleterRows, return the comps
|
||||||
|
if rl.tcUsedY < rl.MaxTabCompleterRows {
|
||||||
|
return comps, rl.tcUsedY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else we go on, but we have more comps than what allowed:
|
||||||
|
// we will add a line to the end of the comps, giving the actualized
|
||||||
|
// number of completions remaining and not printed
|
||||||
|
var moreComps = func(cropped string, offset int) (hinted string, noHint bool) {
|
||||||
|
_, _, adjusted := rl.getCompletionCount()
|
||||||
|
remain := adjusted - offset
|
||||||
|
if remain == 0 {
|
||||||
|
return cropped, true
|
||||||
|
}
|
||||||
|
hint := fmt.Sprintf(DIM+YELLOW+" %d more completions... (scroll down to show)"+RESET+"\n", remain)
|
||||||
|
hinted = cropped + hint
|
||||||
|
return hinted, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current absolute candidate position (prev groups x suggestions + curGroup.tcPosY)
|
||||||
|
var absPos = rl.getAbsPos()
|
||||||
|
|
||||||
|
// Get absPos - MaxTabCompleterRows for having the number of lines to cut at the top
|
||||||
|
// If the number is negative, that means we don't need to cut anything at the top yet.
|
||||||
|
var maxLines = absPos - rl.MaxTabCompleterRows
|
||||||
|
if maxLines < 0 {
|
||||||
|
maxLines = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan the completions for cutting them at newlines
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(comps))
|
||||||
|
|
||||||
|
// If absPos < MaxTabCompleterRows, cut below MaxTabCompleterRows and return
|
||||||
|
if absPos <= rl.MaxTabCompleterRows {
|
||||||
|
var count int
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if count < rl.MaxTabCompleterRows {
|
||||||
|
cropped += line + "\n"
|
||||||
|
count++
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cropped, _ = moreComps(cropped, count)
|
||||||
|
return cropped, count
|
||||||
|
}
|
||||||
|
|
||||||
|
// If absolute > MaxTabCompleterRows, cut above and below and return
|
||||||
|
// -> This includes de facto when we tabCompletionReverse
|
||||||
|
if absPos > rl.MaxTabCompleterRows {
|
||||||
|
cutAbove := absPos - rl.MaxTabCompleterRows
|
||||||
|
var count int
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if count < cutAbove {
|
||||||
|
count++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if count >= cutAbove && count < absPos {
|
||||||
|
cropped += line + "\n"
|
||||||
|
count++
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cropped, _ := moreComps(cropped, rl.MaxTabCompleterRows+cutAbove)
|
||||||
|
return cropped, count - cutAbove
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getAbsPos() int {
|
||||||
|
var prev int
|
||||||
|
var foundCurrent bool
|
||||||
|
for _, grp := range rl.tcGroups {
|
||||||
|
if grp.isCurrent {
|
||||||
|
prev += grp.tcPosY + 1 // + 1 for title
|
||||||
|
foundCurrent = true
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
prev += grp.tcMaxY + 1 // + 1 for title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no current group, it means
|
||||||
|
// we showed completions but there is no
|
||||||
|
// candidate selected yet, return 0
|
||||||
|
if !foundCurrent {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pass a special subset of the current input line, so that
|
||||||
|
// completions are available no matter where the cursor is.
|
||||||
|
func (rl *Instance) getCompletionLine() (line []rune, pos int) {
|
||||||
|
|
||||||
|
pos = rl.pos - len(rl.currentComp)
|
||||||
|
if pos < 0 {
|
||||||
|
pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case rl.pos == len(rl.line):
|
||||||
|
line = rl.line
|
||||||
|
case rl.pos < len(rl.line):
|
||||||
|
line = rl.line[:pos]
|
||||||
|
default:
|
||||||
|
line = rl.line
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getCurrentGroup() (group *CompletionGroup) {
|
||||||
|
for _, g := range rl.tcGroups {
|
||||||
|
if g.isCurrent && len(g.Suggestions) > 0 {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We might, for whatever reason, not find one.
|
||||||
|
// If there are groups but no current, make first one the king.
|
||||||
|
if len(rl.tcGroups) > 0 {
|
||||||
|
// Find first group that has list > 0, as another checkup
|
||||||
|
for _, g := range rl.tcGroups {
|
||||||
|
if len(g.Suggestions) > 0 {
|
||||||
|
g.isCurrent = true
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cycleNextGroup - Finds either the first non-empty group,
|
||||||
|
// or the next non-empty group after the current one.
|
||||||
|
func (rl *Instance) cycleNextGroup() {
|
||||||
|
for i, g := range rl.tcGroups {
|
||||||
|
if g.isCurrent {
|
||||||
|
g.isCurrent = false
|
||||||
|
if i == len(rl.tcGroups)-1 {
|
||||||
|
rl.tcGroups[0].isCurrent = true
|
||||||
|
} else {
|
||||||
|
rl.tcGroups[i+1].isCurrent = true
|
||||||
|
// Here, we check if the cycled group is not empty.
|
||||||
|
// If yes, cycle to next one now.
|
||||||
|
new := rl.getCurrentGroup()
|
||||||
|
if len(new.Suggestions) == 0 {
|
||||||
|
rl.cycleNextGroup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cyclePreviousGroup - Same as cycleNextGroup but reverse
|
||||||
|
func (rl *Instance) cyclePreviousGroup() {
|
||||||
|
for i, g := range rl.tcGroups {
|
||||||
|
if g.isCurrent {
|
||||||
|
g.isCurrent = false
|
||||||
|
if i == 0 {
|
||||||
|
rl.tcGroups[len(rl.tcGroups)-1].isCurrent = true
|
||||||
|
} else {
|
||||||
|
rl.tcGroups[i-1].isCurrent = true
|
||||||
|
new := rl.getCurrentGroup()
|
||||||
|
if len(new.Suggestions) == 0 {
|
||||||
|
rl.cyclePreviousGroup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a single completion candidate
|
||||||
|
func (rl *Instance) hasOneCandidate() bool {
|
||||||
|
if len(rl.tcGroups) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If one group and one option, obvious
|
||||||
|
if len(rl.tcGroups) == 1 {
|
||||||
|
cur := rl.getCurrentGroup()
|
||||||
|
if cur == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(cur.Suggestions) == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If many groups but only one option overall
|
||||||
|
if len(rl.tcGroups) > 1 {
|
||||||
|
var count int
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
for range group.Suggestions {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the completions are either longer than:
|
||||||
|
// - The user-specified max completion length
|
||||||
|
// - The terminal lengh
|
||||||
|
// we use this function to prompt for confirmation before printing comps.
|
||||||
|
func (rl *Instance) promptCompletionConfirm(sentence string) {
|
||||||
|
rl.hintText = []rune(sentence)
|
||||||
|
|
||||||
|
rl.compConfirmWait = true
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getCompletionCount() (comps int, lines int, adjusted int) {
|
||||||
|
for _, group := range rl.tcGroups {
|
||||||
|
comps += len(group.Suggestions)
|
||||||
|
// if group.Name != "" {
|
||||||
|
adjusted++ // Title
|
||||||
|
// }
|
||||||
|
if group.tcMaxY > len(group.Suggestions) {
|
||||||
|
lines += len(group.Suggestions)
|
||||||
|
adjusted += len(group.Suggestions)
|
||||||
|
} else {
|
||||||
|
lines += group.tcMaxY
|
||||||
|
adjusted += group.tcMaxY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) resetTabCompletion() {
|
||||||
|
rl.modeTabCompletion = false
|
||||||
|
rl.tabCompletionSelect = false
|
||||||
|
rl.compConfirmWait = false
|
||||||
|
|
||||||
|
rl.tcUsedY = 0
|
||||||
|
rl.modeTabFind = false
|
||||||
|
rl.modeAutoFind = false
|
||||||
|
rl.tfLine = []rune{}
|
||||||
|
|
||||||
|
// Reset tab highlighting
|
||||||
|
if len(rl.tcGroups) > 0 {
|
||||||
|
for _, g := range rl.tcGroups {
|
||||||
|
g.isCurrent = false
|
||||||
|
}
|
||||||
|
rl.tcGroups[0].isCurrent = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindMode defines how the autocomplete suggestions display
|
||||||
|
type FindMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HistoryFind - Searching through history
|
||||||
|
HistoryFind = iota
|
||||||
|
// CompletionFind - Searching through completion items
|
||||||
|
CompletionFind
|
||||||
|
// RegisterFind - The user can complete/search registers
|
||||||
|
RegisterFind
|
||||||
|
)
|
||||||
|
|
||||||
|
func (rl *Instance) backspaceTabFind() {
|
||||||
|
if len(rl.tfLine) > 0 {
|
||||||
|
rl.tfLine = rl.tfLine[:len(rl.tfLine)-1]
|
||||||
|
}
|
||||||
|
rl.updateTabFind([]rune{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and refresh (print) a list of completions. The caller should have reset
|
||||||
|
// the virtual completion system before, so that should not clash with this.
|
||||||
|
func (rl *Instance) updateTabFind(r []rune) {
|
||||||
|
|
||||||
|
rl.tfLine = append(rl.tfLine, r...)
|
||||||
|
|
||||||
|
// The search regex is common to all search modes
|
||||||
|
var err error
|
||||||
|
rl.regexSearch, err = regexp.Compile("(?i)" + string(rl.tfLine))
|
||||||
|
if err != nil {
|
||||||
|
rl.hintText = []rune(Red("Failed to match search regexp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We update and print
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.getTabCompletion()
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) resetTabFind() {
|
||||||
|
rl.modeTabFind = false
|
||||||
|
// rl.modeAutoFind = false // Added, because otherwise it gets stuck on search completions
|
||||||
|
|
||||||
|
rl.mainHist = false
|
||||||
|
rl.tfLine = []rune{}
|
||||||
|
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
|
||||||
|
// If we were browsing history, we don't load the completions again
|
||||||
|
// if rl.searchMode != HistoryFind {
|
||||||
|
rl.getTabCompletion()
|
||||||
|
// }
|
||||||
|
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/olekukonko/ts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTermWidth returns the width of Stdout or 80 if the width cannot be established
|
||||||
|
func GetTermWidth() (termWidth int) {
|
||||||
|
var err error
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
termWidth, _, err = GetSize(fd)
|
||||||
|
if err != nil {
|
||||||
|
termWidth = 80 // The defacto standard on older terms
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTermLength returns the length of the terminal
|
||||||
|
// (Y length), or 80 if it cannot be established
|
||||||
|
func GetTermLength() (termLength int) {
|
||||||
|
size, err := ts.GetSize()
|
||||||
|
if err != nil || size.Row() == 0 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
termLength = size.Row()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func printf(format string, a ...interface{}) {
|
||||||
|
s := fmt.Sprintf(format, a...)
|
||||||
|
print(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(s string) {
|
||||||
|
os.Stdout.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rxAnsiSgr = regexp.MustCompile("\x1b\\[[:;0-9]+m")
|
||||||
|
|
||||||
|
// Gets the number of runes in a string
|
||||||
|
func strLen(s string) int {
|
||||||
|
s = rxAnsiSgr.ReplaceAllString(s, "")
|
||||||
|
return utf8.RuneCountInString(s)
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DelayedTabContext is a custom context interface for async updates to the tab completions
|
||||||
|
type DelayedTabContext struct {
|
||||||
|
rl *Instance
|
||||||
|
Context context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func delayedSyntaxTimer(rl *Instance, i int64) {
|
||||||
|
if rl.PasswordMask != 0 || rl.DelayedSyntaxWorker == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if len(rl.line)+rl.promptLen > GetTermWidth() {
|
||||||
|
// // line wraps, which is hard to do with random ANSI escape sequences
|
||||||
|
// // so better we don't bother trying.
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// We pass either the current line or the one with the current completion.
|
||||||
|
newLine := rl.DelayedSyntaxWorker(rl.getLine())
|
||||||
|
var sLine string
|
||||||
|
count := atomic.LoadInt64(&rl.delayedSyntaxCount)
|
||||||
|
if count != i {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the line again
|
||||||
|
if rl.SyntaxHighlighter != nil {
|
||||||
|
sLine = rl.SyntaxHighlighter(newLine)
|
||||||
|
} else {
|
||||||
|
sLine = string(newLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the line as current, and refresh
|
||||||
|
rl.updateLine([]rune(sLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendGroupSuggestions - Given a group name, append a list of completion candidates.
|
||||||
|
// Any item in the list already existing in the current group's will be ignored.
|
||||||
|
// If the group is not found with the given name, all suggestions will be dropped, no new group is created.
|
||||||
|
func (dtc DelayedTabContext) AppendGroupSuggestions(groupName string, suggestions []string) {
|
||||||
|
dtc.rl.mutex.Lock()
|
||||||
|
defer dtc.rl.mutex.Unlock()
|
||||||
|
|
||||||
|
// Get the group, and return if not found
|
||||||
|
var grp *CompletionGroup
|
||||||
|
for _, g := range dtc.rl.tcGroups {
|
||||||
|
if g.Name == groupName {
|
||||||
|
grp = g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if grp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add candidate items
|
||||||
|
for i := range suggestions {
|
||||||
|
select {
|
||||||
|
case <-dtc.Context.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Drop duplicates
|
||||||
|
for _, actual := range grp.Suggestions {
|
||||||
|
if actual == suggestions[i] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptions might be used by tabdisplay maps or grids, but not lists.
|
||||||
|
if grp.DisplayType != TabDisplayList {
|
||||||
|
grp.Descriptions[suggestions[i]] = suggestions[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions are used by all groups no matter their display type
|
||||||
|
grp.Suggestions = append(grp.Suggestions, suggestions[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dtc.rl.clearHelpers()
|
||||||
|
dtc.rl.renderHelpers()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendGroupAliases - Given a group name, append a list of completion candidates' ALIASES, that is, a second candidate item.
|
||||||
|
// If any candidate (map index) already exists, its corresponding alias will be overwritten with the new one.
|
||||||
|
// If any candidate (map index) does not exists yet, it will be added along with its alias.
|
||||||
|
// If the group is not found with the given name, all suggestions will be dropped, no new group is created.
|
||||||
|
func (dtc DelayedTabContext) AppendGroupAliases(groupName string, aliases map[string]string) {
|
||||||
|
dtc.rl.mutex.Lock()
|
||||||
|
defer dtc.rl.mutex.Unlock()
|
||||||
|
|
||||||
|
// Get the group, and return if not found
|
||||||
|
var grp *CompletionGroup
|
||||||
|
for _, g := range dtc.rl.tcGroups {
|
||||||
|
if g.Name == groupName {
|
||||||
|
grp = g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if grp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add candidate aliases
|
||||||
|
for sugg, alias := range aliases {
|
||||||
|
select {
|
||||||
|
case <-dtc.Context.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Add to suggestions list if not existing yet
|
||||||
|
var found bool
|
||||||
|
for _, actual := range grp.Suggestions {
|
||||||
|
if actual == sugg {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
grp.Suggestions = append(grp.Suggestions, sugg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the new description anyway
|
||||||
|
grp.Aliases[sugg] = alias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinit all completion groups (recomputes sizes)
|
||||||
|
for _, grp := range dtc.rl.tcGroups {
|
||||||
|
grp.init(dtc.rl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dtc.rl.clearHelpers()
|
||||||
|
// dtc.rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendGroupDescriptions - Given a group name, append a list of descriptions to a list of candidates.
|
||||||
|
// If any candidate (map index) already exists, its corresponding description will be overwritten with the new one.
|
||||||
|
// If any candidate (map index) does not exists yet, it will be added along with its description.
|
||||||
|
func (dtc DelayedTabContext) AppendGroupDescriptions(groupName string, descriptions map[string]string) {
|
||||||
|
dtc.rl.mutex.Lock()
|
||||||
|
defer dtc.rl.mutex.Unlock()
|
||||||
|
|
||||||
|
// Get the group, and return if not found
|
||||||
|
var grp *CompletionGroup
|
||||||
|
for _, g := range dtc.rl.tcGroups {
|
||||||
|
if g.Name == groupName {
|
||||||
|
grp = g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if grp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add candidate descriptions
|
||||||
|
for sugg, desc := range descriptions {
|
||||||
|
select {
|
||||||
|
case <-dtc.Context.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Add to suggestions list if not existing yet
|
||||||
|
var found bool
|
||||||
|
for _, actual := range grp.Suggestions {
|
||||||
|
if actual == sugg {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
grp.Suggestions = append(grp.Suggestions, sugg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the new description anyway
|
||||||
|
grp.Descriptions[sugg] = desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinit all completion groups (recomputes sizes)
|
||||||
|
for _, grp := range dtc.rl.tcGroups {
|
||||||
|
grp.init(dtc.rl)
|
||||||
|
}
|
||||||
|
dtc.rl.clearHelpers()
|
||||||
|
dtc.rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendGroup - Asynchronously add an entire group of completions to the current list
|
||||||
|
func (dtc DelayedTabContext) AppendGroup(group *CompletionGroup) {
|
||||||
|
dtc.rl.mutex.Lock()
|
||||||
|
defer dtc.rl.mutex.Unlock()
|
||||||
|
|
||||||
|
// Simply append group to the list
|
||||||
|
dtc.rl.tcGroups = append(dtc.rl.tcGroups, group)
|
||||||
|
|
||||||
|
dtc.rl.clearHelpers()
|
||||||
|
dtc.rl.renderHelpers()
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// tokeniser - The input line must be splitted according to different rules (split between spaces, brackets, etc ?).
|
||||||
|
type tokeniser func(line []rune, cursorPos int) (split []string, index int, newPos int)
|
||||||
|
|
||||||
|
func tokeniseLine(line []rune, linePos int) ([]string, int, int) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var index, pos int
|
||||||
|
var punc bool
|
||||||
|
|
||||||
|
split := make([]string, 1)
|
||||||
|
|
||||||
|
for i, r := range line {
|
||||||
|
switch {
|
||||||
|
case (r >= 33 && 47 >= r) ||
|
||||||
|
(r >= 58 && 64 >= r) ||
|
||||||
|
(r >= 91 && 94 >= r) ||
|
||||||
|
r == 96 ||
|
||||||
|
(r >= 123 && 126 >= r):
|
||||||
|
|
||||||
|
if i > 0 && line[i-1] != r {
|
||||||
|
split = append(split, "")
|
||||||
|
}
|
||||||
|
split[len(split)-1] += string(r)
|
||||||
|
punc = true
|
||||||
|
|
||||||
|
case r == ' ' || r == '\t':
|
||||||
|
split[len(split)-1] += string(r)
|
||||||
|
punc = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
if punc {
|
||||||
|
split = append(split, "")
|
||||||
|
}
|
||||||
|
split[len(split)-1] += string(r)
|
||||||
|
punc = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == linePos {
|
||||||
|
index = len(split) - 1
|
||||||
|
pos = len(split[index]) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hackish: if we are at the end of the line,
|
||||||
|
// currently appending to it, we return the pos
|
||||||
|
// as we would do when matching linePos
|
||||||
|
if linePos == len(line) {
|
||||||
|
if index == 0 {
|
||||||
|
index = len(split) - 1
|
||||||
|
}
|
||||||
|
if pos == 0 {
|
||||||
|
pos = len(split[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return split, index, pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokeniseSplitSpaces(line []rune, linePos int) ([]string, int, int) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var index, pos int
|
||||||
|
split := make([]string, 1)
|
||||||
|
|
||||||
|
for i, r := range line {
|
||||||
|
switch {
|
||||||
|
case r == ' ' || r == '\t':
|
||||||
|
split[len(split)-1] += string(r)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if i > 0 && (line[i-1] == ' ' || line[i-1] == '\t') {
|
||||||
|
split = append(split, "")
|
||||||
|
}
|
||||||
|
split[len(split)-1] += string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == linePos {
|
||||||
|
index = len(split) - 1
|
||||||
|
pos = len(split[index]) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return split, index, pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokeniseBrackets(line []rune, linePos int) ([]string, int, int) {
|
||||||
|
var (
|
||||||
|
open, close rune
|
||||||
|
split []string
|
||||||
|
count int
|
||||||
|
pos = make(map[int]int)
|
||||||
|
match int
|
||||||
|
single, double bool
|
||||||
|
)
|
||||||
|
|
||||||
|
switch line[linePos] {
|
||||||
|
case '(', ')':
|
||||||
|
open = '('
|
||||||
|
close = ')'
|
||||||
|
|
||||||
|
case '{', '[':
|
||||||
|
open = line[linePos]
|
||||||
|
close = line[linePos] + 2
|
||||||
|
|
||||||
|
case '}', ']':
|
||||||
|
open = line[linePos] - 2
|
||||||
|
close = line[linePos]
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range line {
|
||||||
|
switch line[i] {
|
||||||
|
case '\'':
|
||||||
|
if !single {
|
||||||
|
double = !double
|
||||||
|
}
|
||||||
|
|
||||||
|
case '"':
|
||||||
|
if !double {
|
||||||
|
single = !single
|
||||||
|
}
|
||||||
|
|
||||||
|
case open:
|
||||||
|
if !single && !double {
|
||||||
|
count++
|
||||||
|
pos[count] = i
|
||||||
|
if i == linePos {
|
||||||
|
match = count
|
||||||
|
split = []string{string(line[:i-1])}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if i == linePos {
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case close:
|
||||||
|
if !single && !double {
|
||||||
|
if match == count {
|
||||||
|
split = append(split, string(line[pos[count]:i]))
|
||||||
|
return split, 1, 0
|
||||||
|
}
|
||||||
|
if i == linePos {
|
||||||
|
split = []string{
|
||||||
|
string(line[:pos[count]-1]),
|
||||||
|
string(line[pos[count]:i]),
|
||||||
|
}
|
||||||
|
return split, 1, len(split[1])
|
||||||
|
}
|
||||||
|
count--
|
||||||
|
|
||||||
|
} else if i == linePos {
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func rTrimWhiteSpace(oldString string) (newString string) {
|
||||||
|
return strings.TrimRight(oldString, " ")
|
||||||
|
// TODO: support tab chars
|
||||||
|
/*defer fmt.Println(">" + oldString + "<" + newString + ">")
|
||||||
|
newString = oldString
|
||||||
|
for len(oldString) > 0 {
|
||||||
|
if newString[len(newString)-1] == ' ' || newString[len(newString)-1] == '\t' {
|
||||||
|
newString = newString[:len(newString)-1]
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return*/
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TUI colors & effects, from evilsocket's github.com/evilsocket/islazy/tui package
|
||||||
|
// These are exported as an easy-to-use quick effect/color library, and also will
|
||||||
|
// integrate nicely with the readline shell itself (ex: transport no_color mode with Disable())
|
||||||
|
|
||||||
|
// https://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||||
|
var (
|
||||||
|
// effects
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
RESET = "\033[0m"
|
||||||
|
// colors
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
BLUE = "\033[34m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
// foreground colors
|
||||||
|
FOREBLACK = "\033[30m"
|
||||||
|
FOREWHITE = "\033[97m"
|
||||||
|
// background colors
|
||||||
|
BACKDARKGRAY = "\033[100m"
|
||||||
|
BACKRED = "\033[41m"
|
||||||
|
BACKGREEN = "\033[42m"
|
||||||
|
BACKYELLOW = "\033[43m"
|
||||||
|
BACKLIGHTBLUE = "\033[104m"
|
||||||
|
|
||||||
|
ctrl = []string{"\x033", "\\e", "\x1b"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Effects returns true if colors and effects are supported
|
||||||
|
// on the current terminal.
|
||||||
|
func Effects() bool {
|
||||||
|
if term := os.Getenv("TERM"); term == "" {
|
||||||
|
return false
|
||||||
|
} else if term == "dumb" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable will disable all colors and effects.
|
||||||
|
func Disable() {
|
||||||
|
BOLD = ""
|
||||||
|
DIM = ""
|
||||||
|
RESET = ""
|
||||||
|
RED = ""
|
||||||
|
GREEN = ""
|
||||||
|
BLUE = ""
|
||||||
|
YELLOW = ""
|
||||||
|
FOREBLACK = ""
|
||||||
|
FOREWHITE = ""
|
||||||
|
BACKDARKGRAY = ""
|
||||||
|
BACKRED = ""
|
||||||
|
BACKGREEN = ""
|
||||||
|
BACKYELLOW = ""
|
||||||
|
BACKLIGHTBLUE = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasEffect returns true if the string has any shell control codes in it.
|
||||||
|
func HasEffect(s string) bool {
|
||||||
|
for _, ch := range ctrl {
|
||||||
|
if strings.Contains(s, ch) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap wraps a string with an effect or color and appends a reset control code.
|
||||||
|
func Wrap(e, s string) string {
|
||||||
|
return e + s + RESET
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bold makes the string Bold.
|
||||||
|
func Bold(s string) string {
|
||||||
|
return Wrap(BOLD, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dim makes the string Diminished.
|
||||||
|
func Dim(s string) string {
|
||||||
|
return Wrap(DIM, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Red makes the string Red.
|
||||||
|
func Red(s string) string {
|
||||||
|
return Wrap(RED, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Green makes the string Green.
|
||||||
|
func Green(s string) string {
|
||||||
|
return Wrap(GREEN, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue makes the string Green.
|
||||||
|
func Blue(s string) string {
|
||||||
|
return Wrap(BLUE, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yellow makes the string Green.
|
||||||
|
func Yellow(s string) string {
|
||||||
|
return Wrap(YELLOW, s)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
type undoItem struct {
|
||||||
|
line string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) undoAppendHistory() {
|
||||||
|
defer func() { rl.viUndoSkipAppend = false }()
|
||||||
|
|
||||||
|
if rl.viUndoSkipAppend {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.viUndoHistory = append(rl.viUndoHistory, undoItem{
|
||||||
|
line: string(rl.line),
|
||||||
|
pos: rl.pos,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) undoLast() {
|
||||||
|
var undo undoItem
|
||||||
|
for {
|
||||||
|
if len(rl.viUndoHistory) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
undo = rl.viUndoHistory[len(rl.viUndoHistory)-1]
|
||||||
|
rl.viUndoHistory = rl.viUndoHistory[:len(rl.viUndoHistory)-1]
|
||||||
|
if string(undo.line) != string(rl.line) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.line = []rune(undo.line)
|
||||||
|
rl.pos = undo.pos
|
||||||
|
|
||||||
|
if rl.modeViMode != VimInsert && len(rl.line) > 0 && rl.pos == len(rl.line) {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
// updateHelpers is a key part of the whole refresh process:
|
||||||
|
// it should coordinate reprinting the input line, any hints and completions
|
||||||
|
// and manage to get back to the current (computed) cursor coordinates
|
||||||
|
func (rl *Instance) updateHelpers() {
|
||||||
|
|
||||||
|
// Load all hints & completions before anything.
|
||||||
|
// Thus overwrites anything having been dirtily added/forced/modified, like rl.SetHintText()
|
||||||
|
rl.getHintText()
|
||||||
|
if rl.modeTabCompletion {
|
||||||
|
rl.getTabCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We clear everything
|
||||||
|
rl.clearHelpers()
|
||||||
|
|
||||||
|
// We are at the prompt line (with the latter
|
||||||
|
// not printed yet), then reprint everything
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update reference should be called only once in a "loop" (not Readline(), but key control loop)
|
||||||
|
func (rl *Instance) updateReferences() {
|
||||||
|
|
||||||
|
// We always need to work with clean data,
|
||||||
|
// since we will have incrementers all around
|
||||||
|
rl.posX = 0
|
||||||
|
rl.fullX = 0
|
||||||
|
rl.posY = 0
|
||||||
|
rl.fullY = 0
|
||||||
|
|
||||||
|
var fullLine, cPosLine int
|
||||||
|
if len(rl.currentComp) > 0 {
|
||||||
|
fullLine = len(rl.lineComp)
|
||||||
|
cPosLine = len(rl.lineComp[:rl.pos])
|
||||||
|
} else {
|
||||||
|
fullLine = len(rl.line)
|
||||||
|
cPosLine = len(rl.line[:rl.pos])
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the X offset of the whole line
|
||||||
|
toEndLine := rl.promptLen + fullLine
|
||||||
|
fullOffset := toEndLine / GetTermWidth()
|
||||||
|
rl.fullY = fullOffset
|
||||||
|
fullRest := toEndLine % GetTermWidth()
|
||||||
|
rl.fullX = fullRest
|
||||||
|
|
||||||
|
// Use rl.pos value to get the offset to go TO/FROM the CURRENT POSITION
|
||||||
|
lineToCursorPos := rl.promptLen + cPosLine
|
||||||
|
offsetToCursor := lineToCursorPos / GetTermWidth()
|
||||||
|
cPosRest := lineToCursorPos % GetTermWidth()
|
||||||
|
|
||||||
|
// If we are at the end of line
|
||||||
|
if fullLine == rl.pos {
|
||||||
|
rl.posY = fullOffset
|
||||||
|
|
||||||
|
if fullRest == 0 {
|
||||||
|
rl.posX = 0
|
||||||
|
} else if fullRest > 0 {
|
||||||
|
rl.posX = fullRest
|
||||||
|
}
|
||||||
|
} else if rl.pos < fullLine {
|
||||||
|
// If we are somewhere in the middle of the line
|
||||||
|
rl.posY = offsetToCursor
|
||||||
|
|
||||||
|
if cPosRest == 0 {
|
||||||
|
} else if cPosRest > 0 {
|
||||||
|
rl.posX = cPosRest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) resetHelpers() {
|
||||||
|
rl.modeAutoFind = false
|
||||||
|
|
||||||
|
// Now reset all below-input helpers
|
||||||
|
rl.resetHintText()
|
||||||
|
rl.resetTabCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearHelpers - Clears everything: prompt, input, hints & comps,
|
||||||
|
// and comes back at the prompt.
|
||||||
|
func (rl *Instance) clearHelpers() {
|
||||||
|
|
||||||
|
// Now go down to the last line of input
|
||||||
|
moveCursorDown(rl.fullY - rl.posY)
|
||||||
|
moveCursorBackwards(rl.posX)
|
||||||
|
moveCursorForwards(rl.fullX)
|
||||||
|
|
||||||
|
// Clear everything below
|
||||||
|
print(seqClearScreenBelow)
|
||||||
|
|
||||||
|
// Go back to current cursor position
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorUp(rl.fullY - rl.posY)
|
||||||
|
moveCursorForwards(rl.posX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderHelpers - pritns all components (prompt, line, hints & comps)
|
||||||
|
// and replaces the cursor to its current position. This function never
|
||||||
|
// computes or refreshes any value, except from inside the echo function.
|
||||||
|
func (rl *Instance) renderHelpers() {
|
||||||
|
|
||||||
|
// Optional, because neutral on placement
|
||||||
|
rl.echo()
|
||||||
|
|
||||||
|
// Go at beginning of first line after input remainder
|
||||||
|
moveCursorDown(rl.fullY - rl.posY)
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
|
||||||
|
// Print hints, check for any confirmation hint current.
|
||||||
|
// (do not overwrite the confirmation question hint)
|
||||||
|
if !rl.compConfirmWait {
|
||||||
|
if len(rl.hintText) > 0 {
|
||||||
|
print("\n")
|
||||||
|
}
|
||||||
|
rl.writeHintText()
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
|
||||||
|
// Print completions and go back to beginning of this line
|
||||||
|
print("\n")
|
||||||
|
rl.writeTabCompletion()
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
moveCursorUp(rl.tcUsedY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are still waiting for the user to confirm too long completions
|
||||||
|
// Immediately refresh the hints
|
||||||
|
if rl.compConfirmWait {
|
||||||
|
print("\n")
|
||||||
|
rl.writeHintText()
|
||||||
|
rl.getHintText()
|
||||||
|
moveCursorBackwards(GetTermWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anyway, compensate for hint printout
|
||||||
|
if len(rl.hintText) > 0 {
|
||||||
|
moveCursorUp(rl.hintY)
|
||||||
|
} else if !rl.compConfirmWait {
|
||||||
|
moveCursorUp(1)
|
||||||
|
} else if rl.compConfirmWait {
|
||||||
|
moveCursorUp(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go back to current cursor position
|
||||||
|
moveCursorUp(rl.fullY - rl.posY)
|
||||||
|
moveCursorForwards(rl.posX)
|
||||||
|
}
|
|
@ -0,0 +1,503 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputMode - The shell input mode
|
||||||
|
type InputMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Vim - Vim editing mode
|
||||||
|
Vim = iota
|
||||||
|
// Emacs - Emacs (classic) editing mode
|
||||||
|
Emacs
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
VimInsert ViMode = iota
|
||||||
|
VimReplaceOnce
|
||||||
|
VimReplaceMany
|
||||||
|
VimDelete
|
||||||
|
VimKeys
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
VimInsertStr = "[I]"
|
||||||
|
VimReplaceOnceStr = "[V]"
|
||||||
|
VimReplaceManyStr = "[R]"
|
||||||
|
VimDeleteStr = "[D]"
|
||||||
|
VimKeysStr = "[N]"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// registerFreeKeys - Some Vim keys don't act on/ aren't affected by registers,
|
||||||
|
// and using these keys will automatically cancel any active register.
|
||||||
|
// NOTE: Don't forget to update if you add Vim bindings !!
|
||||||
|
registerFreeKeys = []rune{'a', 'A', 'h', 'i', 'I', 'j', 'k', 'l', 'r', 'R', 'u', 'v', '$', '%', '[', ']'}
|
||||||
|
|
||||||
|
// validRegisterKeys - All valid register IDs (keys) for read/write Vim registers
|
||||||
|
validRegisterKeys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-\""
|
||||||
|
)
|
||||||
|
|
||||||
|
// vi - Apply a key to a Vi action. Note that as in the rest of the code, all cursor movements
|
||||||
|
// have been moved away, and only the rl.pos is adjusted: when echoing the input line, the shell
|
||||||
|
// will compute the new cursor pos accordingly.
|
||||||
|
func (rl *Instance) vi(r rune) {
|
||||||
|
|
||||||
|
// Check if we are in register mode. If yes, and for some characters,
|
||||||
|
// we select the register and exit this func immediately.
|
||||||
|
if rl.registers.registerSelectWait {
|
||||||
|
for _, char := range validRegisterKeys {
|
||||||
|
if r == char {
|
||||||
|
rl.registers.setActiveRegister(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are on register mode and one is already selected,
|
||||||
|
// check if the key stroke to be evaluated is acting on it
|
||||||
|
// or not: if not, we cancel the active register now.
|
||||||
|
if rl.registers.onRegister {
|
||||||
|
for _, char := range registerFreeKeys {
|
||||||
|
if char == r {
|
||||||
|
rl.registers.resetRegister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then evaluate the key.
|
||||||
|
switch r {
|
||||||
|
case 'a':
|
||||||
|
if len(rl.line) > 0 {
|
||||||
|
rl.pos++
|
||||||
|
}
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'A':
|
||||||
|
if len(rl.line) > 0 {
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
}
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'b':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpB, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpB(tokeniseLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'B':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpB, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpB(tokeniseSplitSpaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
rl.modeViMode = VimDelete
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'D':
|
||||||
|
rl.saveBufToRegister(rl.line[rl.pos-1:])
|
||||||
|
rl.line = rl.line[:rl.pos]
|
||||||
|
// Only go back if there is an input
|
||||||
|
if len(rl.line) > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.updateHelpers()
|
||||||
|
rl.viIteration = ""
|
||||||
|
|
||||||
|
case 'e':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpE, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpE(tokeniseLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'E':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpE, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpE(tokeniseSplitSpaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
if rl.pos > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'i':
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.registers.resetRegister()
|
||||||
|
|
||||||
|
case 'I':
|
||||||
|
rl.modeViMode = VimInsert
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.pos = 0
|
||||||
|
|
||||||
|
case 'j':
|
||||||
|
// Set the main history as the one we navigate, by default
|
||||||
|
rl.mainHist = true
|
||||||
|
rl.walkHistory(-1)
|
||||||
|
case 'k':
|
||||||
|
// Set the main history as the one we navigate, by default
|
||||||
|
rl.mainHist = true
|
||||||
|
rl.walkHistory(1)
|
||||||
|
|
||||||
|
case 'l':
|
||||||
|
if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) ||
|
||||||
|
(rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) {
|
||||||
|
rl.pos++
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'p':
|
||||||
|
// paste after the cursor position
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.pos += 2
|
||||||
|
|
||||||
|
buffer := rl.pasteFromRegister()
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.insert(buffer)
|
||||||
|
}
|
||||||
|
rl.pos--
|
||||||
|
|
||||||
|
case 'P':
|
||||||
|
// paste before
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
buffer := rl.pasteFromRegister()
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.insert(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'r':
|
||||||
|
rl.modeViMode = VimReplaceOnce
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'R':
|
||||||
|
rl.modeViMode = VimReplaceMany
|
||||||
|
rl.viIteration = ""
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'u':
|
||||||
|
rl.undoLast()
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'v':
|
||||||
|
rl.clearHelpers()
|
||||||
|
var multiline []rune
|
||||||
|
if rl.GetMultiLine == nil {
|
||||||
|
multiline = rl.line
|
||||||
|
} else {
|
||||||
|
multiline = rl.GetMultiLine(rl.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the previous cursor position
|
||||||
|
prev := rl.pos
|
||||||
|
|
||||||
|
new, err := rl.StartEditorWithBuffer(multiline, "")
|
||||||
|
if err != nil || len(new) == 0 || string(new) == string(multiline) {
|
||||||
|
fmt.Println(err)
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the shell and put the new buffer, with adjusted pos if needed.
|
||||||
|
rl.clearLine()
|
||||||
|
rl.line = new
|
||||||
|
if prev > len(rl.line) {
|
||||||
|
rl.pos = len(rl.line) - 1
|
||||||
|
} else {
|
||||||
|
rl.pos = prev
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'w':
|
||||||
|
// If we were not yanking
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
// If the input line is empty, we don't do anything
|
||||||
|
if rl.pos == 0 && len(rl.line) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were yanking, we forge the new yank buffer
|
||||||
|
// and return without moving the cursor.
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpW, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else get iterations and move
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'W':
|
||||||
|
// If the input line is empty, we don't do anything
|
||||||
|
if rl.pos == 0 && len(rl.line) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
if rl.viIsYanking {
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpW, vii)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpW(tokeniseSplitSpaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'x':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
|
||||||
|
// We might be on an active register, but not yanking...
|
||||||
|
rl.saveToRegister(vii)
|
||||||
|
|
||||||
|
// Delete the chars in the line anyway
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.deleteX()
|
||||||
|
}
|
||||||
|
if rl.pos == len(rl.line) && len(rl.line) > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'y':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
rl.saveBufToRegister(rl.line)
|
||||||
|
rl.viIsYanking = false
|
||||||
|
}
|
||||||
|
rl.viIsYanking = true
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case 'Y':
|
||||||
|
rl.saveBufToRegister(rl.line)
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
rl.saveToRegister(rl.viJumpPreviousBrace())
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpPreviousBrace())
|
||||||
|
|
||||||
|
case ']':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
rl.saveToRegister(rl.viJumpNextBrace())
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpNextBrace())
|
||||||
|
|
||||||
|
case '$':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
rl.saveBufToRegister(rl.line[rl.pos:])
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.pos = len(rl.line)
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
|
||||||
|
case '%':
|
||||||
|
if rl.viIsYanking {
|
||||||
|
rl.saveToRegister(rl.viJumpBracket())
|
||||||
|
rl.viIsYanking = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
rl.moveCursorByAdjust(rl.viJumpBracket())
|
||||||
|
|
||||||
|
case '"':
|
||||||
|
// We might be on a register already, so reset it,
|
||||||
|
// and then wait again for a new register ID.
|
||||||
|
if rl.registers.onRegister {
|
||||||
|
rl.registers.resetRegister()
|
||||||
|
}
|
||||||
|
rl.registers.registerSelectWait = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
if r <= '9' && '0' <= r {
|
||||||
|
rl.viIteration += string(r)
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) getViIterations() int {
|
||||||
|
i, _ := strconv.Atoi(rl.viIteration)
|
||||||
|
if i < 1 {
|
||||||
|
i = 1
|
||||||
|
}
|
||||||
|
rl.viIteration = ""
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) refreshVimStatus() {
|
||||||
|
rl.ViModeCallback(rl.modeViMode)
|
||||||
|
rl.computePrompt()
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// viHintMessage - lmorg's way of showing Vim status is to overwrite the hint.
|
||||||
|
// Currently not used, as there is a possibility to show the current Vim mode in the prompt.
|
||||||
|
func (rl *Instance) viHintMessage() {
|
||||||
|
switch rl.modeViMode {
|
||||||
|
case VimKeys:
|
||||||
|
rl.hintText = []rune("-- VIM KEYS -- (press `i` to return to normal editing mode)")
|
||||||
|
case VimInsert:
|
||||||
|
rl.hintText = []rune("-- INSERT --")
|
||||||
|
case VimReplaceOnce:
|
||||||
|
rl.hintText = []rune("-- REPLACE CHARACTER --")
|
||||||
|
case VimReplaceMany:
|
||||||
|
rl.hintText = []rune("-- REPLACE --")
|
||||||
|
case VimDelete:
|
||||||
|
rl.hintText = []rune("-- DELETE --")
|
||||||
|
default:
|
||||||
|
rl.getHintText()
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.clearHelpers()
|
||||||
|
rl.renderHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpB(tokeniser tokeniser) (adjust int) {
|
||||||
|
split, index, pos := tokeniser(rl.line, rl.pos)
|
||||||
|
switch {
|
||||||
|
case len(split) == 0:
|
||||||
|
return
|
||||||
|
case index == 0 && pos == 0:
|
||||||
|
return
|
||||||
|
case pos == 0:
|
||||||
|
adjust = len(split[index-1])
|
||||||
|
default:
|
||||||
|
adjust = pos
|
||||||
|
}
|
||||||
|
return adjust * -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpE(tokeniser tokeniser) (adjust int) {
|
||||||
|
split, index, pos := tokeniser(rl.line, rl.pos)
|
||||||
|
if len(split) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
word := rTrimWhiteSpace(split[index])
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(split) == 0:
|
||||||
|
return
|
||||||
|
case index == len(split)-1 && pos >= len(word)-1:
|
||||||
|
return
|
||||||
|
case pos >= len(word)-1:
|
||||||
|
word = rTrimWhiteSpace(split[index+1])
|
||||||
|
adjust = len(split[index]) - pos
|
||||||
|
adjust += len(word) - 1
|
||||||
|
default:
|
||||||
|
adjust = len(word) - pos - 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpW(tokeniser tokeniser) (adjust int) {
|
||||||
|
split, index, pos := tokeniser(rl.line, rl.pos)
|
||||||
|
switch {
|
||||||
|
case len(split) == 0:
|
||||||
|
return
|
||||||
|
case index+1 == len(split):
|
||||||
|
adjust = len(rl.line) - rl.pos
|
||||||
|
default:
|
||||||
|
adjust = len(split[index]) - pos
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpPreviousBrace() (adjust int) {
|
||||||
|
if rl.pos == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := rl.pos - 1; i != 0; i-- {
|
||||||
|
if rl.line[i] == '{' {
|
||||||
|
return i - rl.pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpNextBrace() (adjust int) {
|
||||||
|
if rl.pos >= len(rl.line)-1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := rl.pos + 1; i < len(rl.line); i++ {
|
||||||
|
if rl.line[i] == '{' {
|
||||||
|
return i - rl.pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viJumpBracket() (adjust int) {
|
||||||
|
split, index, pos := tokeniseBrackets(rl.line, rl.pos)
|
||||||
|
switch {
|
||||||
|
case len(split) == 0:
|
||||||
|
return
|
||||||
|
case pos == 0:
|
||||||
|
adjust = len(split[index])
|
||||||
|
default:
|
||||||
|
adjust = pos * -1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// vimDelete -
|
||||||
|
func (rl *Instance) viDelete(r rune) {
|
||||||
|
|
||||||
|
// We are allowed to type iterations after a delete ('d') command.
|
||||||
|
// in which case we don't exit the delete mode. The next thing typed
|
||||||
|
// will thus be dispatched back here (like "2d4 then w).
|
||||||
|
if !(r <= '9' && '0' <= r) {
|
||||||
|
defer func() { rl.modeViMode = VimKeys }()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case 'b':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpB, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'B':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpB, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpB(tokeniseSplitSpaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
rl.saveBufToRegister(rl.line)
|
||||||
|
rl.clearLine()
|
||||||
|
rl.resetHelpers()
|
||||||
|
rl.getHintText()
|
||||||
|
|
||||||
|
case 'e':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpE, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpE(tokeniseLine) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'E':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpE, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpE(tokeniseSplitSpaces) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'w':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseLine, rl.viJumpW, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpW(tokeniseLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'W':
|
||||||
|
vii := rl.getViIterations()
|
||||||
|
rl.saveToRegisterTokenize(tokeniseSplitSpaces, rl.viJumpW, vii)
|
||||||
|
for i := 1; i <= vii; i++ {
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpW(tokeniseSplitSpaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
case '%':
|
||||||
|
rl.saveToRegister(rl.viJumpBracket())
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpBracket())
|
||||||
|
|
||||||
|
case '$':
|
||||||
|
rl.saveBufToRegister(rl.line[rl.pos:])
|
||||||
|
rl.viDeleteByAdjust(len(rl.line) - rl.pos)
|
||||||
|
// Only go back if there is an input
|
||||||
|
if len(rl.line) > 0 {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
case '[':
|
||||||
|
rl.saveToRegister(rl.viJumpPreviousBrace())
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpPreviousBrace())
|
||||||
|
|
||||||
|
case ']':
|
||||||
|
rl.saveToRegister(rl.viJumpNextBrace())
|
||||||
|
rl.viDeleteByAdjust(rl.viJumpNextBrace())
|
||||||
|
|
||||||
|
default:
|
||||||
|
if r <= '9' && '0' <= r {
|
||||||
|
rl.viIteration += string(r)
|
||||||
|
}
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) viDeleteByAdjust(adjust int) {
|
||||||
|
var (
|
||||||
|
newLine []rune
|
||||||
|
backOne bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Avoid doing anything if input line is empty.
|
||||||
|
if len(rl.line) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case adjust == 0:
|
||||||
|
rl.viUndoSkipAppend = true
|
||||||
|
return
|
||||||
|
case rl.pos+adjust == len(rl.line)-1:
|
||||||
|
// This case should normally happen only when we met ALL THOSE CONDITIONS:
|
||||||
|
// - We are currently in Insert Mode
|
||||||
|
// - Appending to the end of the line (the cusor pos is len(line) + 1)
|
||||||
|
// - We just deleted a single-lettered word from the input line.
|
||||||
|
//
|
||||||
|
// We must therefore ake a little adjustment (the -1), otherwise this
|
||||||
|
// single letter is kept in the input line while it should be deleted.
|
||||||
|
newLine = rl.line[:rl.pos-1]
|
||||||
|
if adjust != -1 {
|
||||||
|
backOne = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case rl.pos+adjust == 0:
|
||||||
|
newLine = rl.line[rl.pos:]
|
||||||
|
case adjust < 0:
|
||||||
|
newLine = append(rl.line[:rl.pos+adjust], rl.line[rl.pos:]...)
|
||||||
|
default:
|
||||||
|
newLine = append(rl.line[:rl.pos], rl.line[rl.pos+adjust:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.line = newLine
|
||||||
|
|
||||||
|
// rl.updateHelpers()
|
||||||
|
|
||||||
|
if adjust < 0 {
|
||||||
|
rl.moveCursorByAdjust(adjust)
|
||||||
|
}
|
||||||
|
|
||||||
|
if backOne {
|
||||||
|
rl.pos--
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.updateHelpers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Instance) vimDeleteToken(r rune) bool {
|
||||||
|
tokens, _, _ := tokeniseSplitSpaces(rl.line, 0)
|
||||||
|
pos := int(r) - 48 // convert ASCII to integer
|
||||||
|
if pos > len(tokens) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(rl.line)
|
||||||
|
newLine := strings.Replace(s, tokens[pos-1], "", -1)
|
||||||
|
if newLine == s {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.line = []rune(newLine)
|
||||||
|
|
||||||
|
rl.updateHelpers()
|
||||||
|
|
||||||
|
if rl.pos > len(rl.line) {
|
||||||
|
rl.pos = len(rl.line) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// WrapText - Wraps a text given a specified width, and returns the formatted
|
||||||
|
// string as well the number of lines it will occupy
|
||||||
|
func WrapText(text string, lineWidth int) (wrapped string, lines int) {
|
||||||
|
words := strings.Fields(text)
|
||||||
|
if len(words) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wrapped = words[0]
|
||||||
|
spaceLeft := lineWidth - len(wrapped)
|
||||||
|
// There must be at least a line
|
||||||
|
if text != "" {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
for _, word := range words[1:] {
|
||||||
|
if len(word)+1 > spaceLeft {
|
||||||
|
lines++
|
||||||
|
wrapped += "\n" + word
|
||||||
|
spaceLeft = lineWidth - len(word)
|
||||||
|
} else {
|
||||||
|
wrapped += " " + word
|
||||||
|
spaceLeft -= 1 + len(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue