chore: bring readline in repo for easier maintenance

windows-fixes
TorchedSammy 2022-03-13 13:48:49 -04:00
parent b05cb30ed7
commit 5a2e3e4700
Signed by: sammyette
GPG Key ID: 904FC49417B44DCD
60 changed files with 9178 additions and 1 deletions

2
go.mod
View File

@ -16,6 +16,6 @@ require (
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

122
readline/CHANGES.md 100644
View File

@ -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).

201
readline/LICENSE 100644
View File

@ -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.

165
readline/README.md 100644
View File

@ -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 !

128
readline/codes.go 100644
View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

137
readline/cursor.go 100644
View File

@ -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--
}
}
}

79
readline/editor.go 100644
View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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")
}

24
readline/errors.go 100644
View File

@ -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")
)

23
readline/events.go 100644
View File

@ -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)
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

12
readline/go.mod 100644
View File

@ -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
)

12
readline/go.sum 100644
View File

@ -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=

View File

@ -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

56
readline/hint.go 100644
View File

@ -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{}
}

228
readline/history.go 100644
View File

@ -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
}

View File

@ -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
}

178
readline/line.go 100644
View File

@ -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
}

View File

@ -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)
// }
//
// }
}

207
readline/prompt.go 100644
View File

@ -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)
}

View File

@ -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.

View File

@ -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`

View File

@ -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])
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 Insert 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)")
}
}
}

View File

@ -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 = &registers{
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
}

20
readline/syntax.go 100644
View File

@ -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()
}

View File

@ -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
}

557
readline/tab.go 100644
View File

@ -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
}
}

View File

@ -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()
}

51
readline/term.go 100644
View File

@ -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)
}

202
readline/timer.go 100644
View File

@ -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()
}

View File

@ -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*/
}

View File

@ -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)
}

42
readline/undo.go 100644
View File

@ -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()
}

149
readline/update.go 100644
View File

@ -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)
}

503
readline/vim.go 100644
View File

@ -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
}

View File

@ -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
}

29
readline/wrap.go 100644
View File

@ -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
}