1
1
Fork 0

Compare commits

...

2 Commits

Author SHA1 Message Date
sloum cf6f54aa94 Adds readme and license info 2021-04-03 20:15:57 -07:00
sloum 4863be6bba Converts to working code to a module 2021-04-03 19:56:32 -07:00
3 changed files with 108 additions and 100 deletions

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# qline
qline provides one function: provide an editable input field to the user along with a prompt and initial data.
Unlike readline, editline, linenoise, etc. there is no history, no word jumping, or other builtin features. Just a simple, navigable, editable line of text with an optional prompt.
## Usage
``` go
var prompt string = "> "
var default string = "Edit Me"
var in string = qline.GetInput(prompt, default)
```
Arrow keys, backspace, delete, home, and end all work as one might expect. There are no CTRL keys, no vi mode, etc.
## License
qline is available under the MIT license. A copy of which is included as a comment in the sole source code file for this package.

View File

@ -1,4 +1,33 @@
package main
package qline
// qline is a line input library for terminals that utilize
// vt100 compatible escape sequences
//
// Copyright © 2021 Brian Evans
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the “Software”), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
import (
"bufio"
@ -17,7 +46,6 @@ const (
Delete
Home
End
PrintScreen
PageUp
PageDown
Escape rune = 27
@ -30,7 +58,7 @@ var (
width int
)
type Buffer struct {
type buffer struct {
buf []rune
cursor int
maxWidth int
@ -39,11 +67,56 @@ type Buffer struct {
prompt string
}
func (lb Buffer) String() string {
func GetInput(prompt string, content string) string {
termios.SetCharMode()
cols, _ := termios.GetWindowSize()
b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
seeded := false
fmt.Print(prompt)
fmt.Print("\033[6n")
var ch rune
var err error
reader := bufio.NewReader(os.Stdin)
for {
ch, err = readKey(reader)
if err != nil && err.Error() == "response" {
b.cursorStart = int(ch)
b.maxWidth = cols - b.cursorStart
} else if err != nil {
continue
}
if ch == CarriageReturn || ch == NewLine {
break
}
if isControl(ch) {
b.controlInput(ch)
} else {
b.addChar(ch, true)
}
// Seeding the content needs to happen
// _after_ reading in the width from the [6n
// function call.
if !seeded {
b.seedContent(content)
b.printBuf()
seeded = true
}
}
return b.string()
}
func (lb buffer) string() string {
return string(lb.buf)
}
func (lb *Buffer) deleteChar() {
func (lb *buffer) deleteChar() {
if lb.cursor == len(lb.buf) {
return
} else if lb.cursor == len(lb.buf)-1 {
@ -56,10 +129,7 @@ func (lb *Buffer) deleteChar() {
}
}
// TODO rework this so that the changes are made but all drawing to the screen comes
// from one source and always draws the whole input area. This will make dealing with
// offsets much easier.
func (lb *Buffer) addChar(c rune, echo bool) {
func (lb *buffer) addChar(c rune, echo bool) {
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) {
return
}
@ -93,12 +163,12 @@ func max(a, b int) int {
return b
}
func (lb Buffer) printBuf() {
func (lb buffer) printBuf() {
out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)]
fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt))
}
func (lb *Buffer) controlInput(c rune) {
func (lb *buffer) controlInput(c rune) {
switch c {
case Delete:
lb.deleteChar()
@ -115,7 +185,7 @@ func (lb *Buffer) controlInput(c rune) {
lb.cursor--
}
case RightArrow:
// FIXME: This still isnt working quite right...
// This is still mildly funky, but works enough
for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); {
lb.offset++
}
@ -123,18 +193,16 @@ func (lb *Buffer) controlInput(c rune) {
lb.cursor++
}
case Home:
lb.offset = 0
lb.cursor = 0
lb.offset = 0
lb.cursor = 0
case End:
if lb.cursor < len(lb.buf) {
lb.cursor = len(lb.buf)
lb.offset = max(lb.cursor - lb.maxWidth, 0)
}
lb.cursor = len(lb.buf)
lb.offset = max(lb.cursor - lb.maxWidth, 0)
}
lb.printBuf()
}
func (lb *Buffer) seedContent(s string) {
func (lb *buffer) seedContent(s string) {
for _, r := range s {
lb.addChar(r, false)
}
@ -147,41 +215,6 @@ func parseCursorPosition(esc string) (int, int, error) {
return row, col, err
}
func GetText(prompt string, content string) string {
cols, _ := termios.GetWindowSize()
b := Buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
b.seedContent(content)
fmt.Print(prompt)
fmt.Print("\033[6n")
b.printBuf()
var ch rune
var err error
reader := bufio.NewReader(os.Stdin)
for {
ch, err = readKey(reader)
if err != nil && err.Error() == "response" {
b.cursorStart = int(ch)
b.maxWidth = cols - b.cursorStart
} else if err != nil {
continue
}
if ch == CarriageReturn || ch == NewLine {
break
}
if isControl(ch) {
b.controlInput(ch)
} else {
b.addChar(ch, true)
}
}
return b.String()
}
func readKey(reader *bufio.Reader) (rune, error) {
char, _, err := reader.ReadRune()
@ -220,13 +253,12 @@ func readKey(reader *bufio.Reader) (rune, error) {
case isDeleteKey(escSeq):
char = Delete
case escSeq[len(escSeq)-1] == 'R':
// This is a request for cursor position
_, cols, err := parseCursorPosition(escSeq)
if err == nil {
err = fmt.Errorf("response")
}
return rune(cols), err
default:
fmt.Printf("Odd sequence: %s\n", escSeq[1:])
}
}
return char, nil
@ -268,46 +300,3 @@ func isEndKey(seq string) bool {
}
}
func PrintKey(k rune) {
switch true {
case k == UpArrow:
fmt.Println("UpArrow")
case k == DownArrow:
fmt.Println("DownArrow")
case k == LeftArrow:
fmt.Println("LeftArrow")
case k == RightArrow:
fmt.Println("RightArrow")
case k == Escape:
fmt.Println("Escape")
case k == NewLine:
fmt.Println("NewLine")
case k == CarriageReturn:
fmt.Println("CarriageReturn")
case k == Delete:
fmt.Println("Delete")
case k == Home:
fmt.Println("Home")
case k == End:
fmt.Println("End")
case k == BackSpace:
fmt.Println("BackSpace")
default:
fmt.Printf("%c (%d)\n", k, k)
}
}
func main() {
// This main func is just for testing and will
// be removed with the intention that this just
// gets used as a lib
termios.SetCharMode()
defer termios.Restore()
str := ""
for {
fmt.Print("\n")
str = GetText("> ", str)
}
}

View File

@ -1 +0,0 @@
{"BoardTitle":"","Lanes":[{"LaneTitle":"Backlog","Stories":[{"StoryTitle":"Figure out a way to solve the linewidth issue","StoryBody":"Right now, line editing is working, but without knowing where the cursor starts out we cant know where the end of the line is.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[{"TaskBody":"Try the escape code method.","TaskComplete":false}],"StoryComments":[],"StoryCreated":"2021-04-01T21:57:43.386761058-07:00","StoryUpdated":"2021-04-01T21:59:21.689817136-07:00","StoryPoints":3},{"StoryTitle":"Build out line offsetting for display","StoryBody":"Once widths are figured out there needs to be horizontal scrolling of input for display soas to not jump down a line.","StoryUsers":["sloum"],"StoryTag":-1,"StoryTasks":[],"StoryComments":[],"StoryCreated":"2021-04-01T21:59:09.929902095-07:00","StoryUpdated":"2021-04-01T22:00:00.004446556-07:00","StoryPoints":2}],"CurrentStory":1},{"LaneTitle":"Active","Stories":[],"CurrentStory":-1},{"LaneTitle":"Complete","Stories":[],"CurrentStory":-1}],"CurrentLane":0,"Zoom":3,"StoryOpen":true}