From 71d140f624b87461a4ed362e55fc2d1c13c0c7a4 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Tue, 28 Feb 2017 16:45:39 -0800 Subject: [PATCH 1/6] First commit of basic input component --- input.go | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++ input_test.go | 84 +++++++++++ 2 files changed, 464 insertions(+) create mode 100644 input.go create mode 100644 input_test.go diff --git a/input.go b/input.go new file mode 100644 index 0000000..79329ef --- /dev/null +++ b/input.go @@ -0,0 +1,380 @@ +package termui + +import ( + "strings" + "github.com/nsf/termbox-go" + //"fmt" +) + +// default mappings between /sys/kbd events and multi-line inputs +var multiLineCharMap = map[string]string{ + "": " ", + "": "\t", + "": "\n", + "": "", +} + +// default mappings between /sys/kbd events and single line inputs +var singleLineCharMap = map[string]string{ + "": " ", + "": "\t", + "": "", + "": "", +} + +const NEW_LINE = "\n" + +// EvtInput defines the structure for the /input/* events. The event contains the last keystroke, the full text +// for the current line, and the position of the cursor in the current line as well as the index of the current +// line in the full text of the input +type EvtInput struct { + KeyStr string + LineText string + CursorPosition int + LineIndex int +} + +// Input is the main object for a text input. The object exposes the following public properties +// TextFgColor: color for the text +// TextBgColor: background color for the text box +// IsCapturing: true if the input is currently capturing keyboard events, this is controlled by the StartCapture and +// StopCapture methods +// IsMultiline: Whether we should accept multiple lines of input or this is a singe line form field +// TextBuilder: An implementation of the TextBuilder interface to customize the look of the text on the screen +// SpecialChars: a map[string]string of characters from the /sys/kbd events to actual strings in the content +type Input struct { + Block + TextFgColor Attribute + TextBgColor Attribute + IsCapturing bool + IsMultiLine bool + TextBuilder TextBuilder + SpecialChars map[string]string + + //DebugMode bool + //debugMessage string + + // internal vars + lines []string + cursorLineIndex int + cursorLinePos int +} + +// NewInput returns a new, initialized Input object. The method receives the initial content for the input (if any) +// and whether it should be initialized as a multi-line innput field or not +func NewInput(s string, isMultiLine bool) *Input { + textArea := &Input{ + Block: *NewBlock(), + TextFgColor: ThemeAttr("par.text.fg"), + TextBgColor: ThemeAttr("par.text.bg"), + TextBuilder: NewMarkdownTxBuilder(), + IsMultiLine: isMultiLine, + + cursorLineIndex: 0, + cursorLinePos: 0, + } + + if s != "" { + textArea.lines = strings.Split(s, NEW_LINE) + } + + if isMultiLine { + textArea.SpecialChars = multiLineCharMap + } else { + textArea.SpecialChars = singleLineCharMap + } + + return textArea +} + +// StartCapture begins catching events from the /sys/kbd stream and updates the content of the Input field. While +// capturing events, the Input field also publishes its own event stream under the /input/kbd path. +func (i *Input) StartCapture() { + i.IsCapturing = true + Handle("/sys/kbd", func(e Event) { + if i.IsCapturing { + key := e.Data.(EvtKbd).KeyStr + + switch key { + case "": + i.moveUp() + case "": + i.moveDown() + case "": + i.moveLeft() + case "": + i.moveRight() + case "C-8": + i.backspace() + default: + // If it's a CTRL something we don't handle then just ignore it + if strings.HasPrefix(key, "C-") { + break + } + newString := i.getCharString(key) + /*i.bufferXPos += len(newString) + i.CurStringOffset += len(newString) + i.Text += newString*/ + i.addString(newString) + } + SendCustomEvt("/input/kbd", i.getInputEvt(key)) + //print(i.Text) + i.Buffer() + Render(i) + } + }) +} + +// StopCapture tells the Input field to stop accepting events from the /sys/kbd stream +func (i *Input) StopCapture() { + i.IsCapturing = false +} + +// Text returns the text of the input field as a string +func (i *Input) Text() string { + if len(i.lines) == 0 { + return "" + } + + if len(i.lines) == 1 { + return i.lines[0] + } + + if i.IsMultiLine { + return strings.Join(i.lines, NEW_LINE) + } else { + // we should never get here! + return i.lines[0] + } +} + +// Lines returns the slice of strings with the content of the input field. By default lines are separated by \n +func (i *Input) Lines() []string { + return i.lines +} + + +// Private methods for the input field +// TODO: handle delete key + +func (i *Input) backspace() { + curLine := i.lines[i.cursorLineIndex] + // at the beginning of the buffer, nothing to do + if len(curLine) == 0 && i.cursorLineIndex == 0{ + return + } + + // at the beginning of a line somewhere in the buffer + if i.cursorLinePos == 0 { + prevLine := i.lines[i.cursorLineIndex - 1] + // remove the newline character from the prevline + prevLine = prevLine[:len(curLine) - 1] + curLine + i.lines = append(i.lines[:i.cursorLineIndex], i.lines[i.cursorLineIndex+1:]...) + i.cursorLineIndex-- + i.cursorLinePos = len(prevLine) - 1 + return + } + + // I'm at the end of a line + if i.cursorLinePos == len(curLine) - 1 { + i.lines[i.cursorLineIndex] = curLine[:len(curLine) - 1] + i.cursorLinePos-- + return + } + + // I'm in the middle of a line + i.lines[i.cursorLineIndex] = curLine[:i.cursorLinePos - 1] + curLine[i.cursorLinePos:] + i.cursorLinePos-- +} + +func (i *Input) addString(key string) { + if len(i.lines) > 0 { + if key == NEW_LINE { + // special case when we go back to the beginning of a buffer with multiple lines, prepend a new line + if i.cursorLineIndex == 0 && len(i.lines) > 1 { + i.lines = append([]string { "" }, i.lines...) + + // this case handles newlines at the end of the file or in the middle of the file + } else { + newString := "" + + // if we are inserting a newline in a populated line then set what goes into the new line + // and what stays in the current line + if i.cursorLinePos < len(i.lines[i.cursorLineIndex]) { + newString = i.lines[i.cursorLineIndex][i.cursorLinePos:] + i.lines[i.cursorLineIndex] = i.lines[i.cursorLineIndex][:i.cursorLinePos] + } + + // append a newline in the current position with the content we computed in the previous if statement + i.lines = append( + i.lines[:i.cursorLineIndex+1], + append( + []string{ newString }, + i.lines[i.cursorLineIndex+1:]... + )... + ) + } + // increment the line index, reset the cursor to the beginning and return to skip the next step + i.cursorLineIndex++ + i.cursorLinePos = 0 + return + } + + // cursor is at the end of the line + if i.cursorLinePos == len(i.lines[i.cursorLineIndex]) { + //i.debugMessage ="end" + i.lines[i.cursorLineIndex] += key + + // cursor at the beginning of the line + } else if i.cursorLinePos == 0 { + //i.debugMessage = "beginning" + i.lines[i.cursorLineIndex] = key + i.lines[i.cursorLineIndex] + + // cursor in the middle of the line + } else { + //i.debugMessage = "middle" + before := i.lines[i.cursorLineIndex][:i.cursorLinePos] + after := i.lines[i.cursorLineIndex][i.cursorLinePos:] + i.lines[i.cursorLineIndex] = before + key + after + + } + i.cursorLinePos += len(key) + + } else { + //i.debugMessage = "newline" + i.lines = append(i.lines, key) + i.cursorLinePos += len(key) + } +} + +func (i *Input) moveUp() { + // if we are already on the first line then just move the cursor to the beginning + if i.cursorLineIndex == 0 { + i.cursorLinePos = 0 + return + } + + // The previous line is just as long, we can move to the same position in the line + prevLine := i.lines[i.cursorLineIndex - 1] + if len(prevLine) >= i.cursorLinePos { + i.cursorLineIndex-- + } else { + // otherwise we move the cursor to the end of the previous line + i.cursorLineIndex-- + i.cursorLinePos = len(prevLine) - 1 + } +} + +func (i *Input) moveDown() { + // we are already on the last line, we just need to move the position to the end of the line + if i.cursorLineIndex == len(i.lines) - 1 { + i.cursorLinePos = len(i.lines[i.cursorLineIndex]) - 1 + return + } + + // check if the cursor can move to the same position in the next line, otherwise move it to the end + nextLine := i.lines[i.cursorLineIndex + 1] + if len(nextLine) >= i.cursorLinePos { + i.cursorLineIndex++ + } else { + i.cursorLineIndex++ + i.cursorLinePos = len(nextLine) - 1 + } +} + +func (i *Input) moveLeft() { + // if we are at the beginning of the line move the cursor to the previous line + if i.cursorLinePos == 0 { + i.moveUp() + i.cursorLinePos = len(i.lines[i.cursorLineIndex]) + return + } + + i.cursorLinePos-- +} + +func (i *Input) moveRight() { + // if we are at the end of the line move to the next + if i.cursorLinePos >= len(i.lines[i.cursorLineIndex]) { + i.moveDown() + i.cursorLinePos = 0 + return + } + + i.cursorLinePos++ +} + + +// Buffer implements Bufferer interface. +func (i *Input) Buffer() Buffer { + buf := i.Block.Buffer() + + bufferLines := i.lines[:] + if i.IsMultiLine { + firstLine := 0 + lastLine := firstLine + i.innerArea.Dy() + + if i.cursorLineIndex > lastLine { + firstLine += i.cursorLineIndex - lastLine + lastLine += i.cursorLineIndex - lastLine + } + + if len(i.lines) < lastLine { + bufferLines = i.lines[firstLine:] + } else { + bufferLines = i.lines[firstLine:lastLine] + } + } + + text := strings.Join(bufferLines, NEW_LINE) + + fg, bg := i.TextFgColor, i.TextBgColor + cs := i.TextBuilder.Build(text, fg, bg) + y, x, n := 0, 0, 0 + + for n < len(cs) { + w := cs[n].Width() + + if cs[n].Ch == '\n' { + y++ + x = 0 // set x = 0 + } + buf.Set(i.innerArea.Min.X+x, i.innerArea.Min.Y+y, cs[n]) + + n++ + x += w + } + + /* + if i.DebugMode { + position := fmt.Sprintf("%s li: %d lp: %d n: %d", i.debugMessage, i.cursorLineIndex, i.cursorLinePos, len(i.lines)) + + for idx, char := range position { + buf.Set(i.innerArea.Min.X+i.innerArea.Dx()-len(position) + idx, + i.innerArea.Min.Y+i.innerArea.Dy()-1, + Cell{Ch: char, Fg: i.TextFgColor, Bg: i.TextBgColor}) + } + } + */ + + termbox.SetCursor(i.cursorLinePos + 1, i.cursorLineIndex + 1) + + return buf +} + +func (i *Input) getCharString(s string) string { + if val, ok := i.SpecialChars[s]; ok { + return val + } else { + return s + } +} + +func (i *Input) getInputEvt(key string) EvtInput { + return EvtInput{ + KeyStr: key, + LineText: i.lines[i.cursorLineIndex], + CursorPosition: i.cursorLinePos, + LineIndex: i.cursorLineIndex, + } +} diff --git a/input_test.go b/input_test.go new file mode 100644 index 0000000..b48edc3 --- /dev/null +++ b/input_test.go @@ -0,0 +1,84 @@ +package termui + +import ( + "testing" + "strings" +) + +// TODO: More tests! + +const TESTING_LINE = "testing" + +func TestInput_SingleLine_NoNewLines(t *testing.T) { + input := NewInput("", false) + input.addString(TESTING_LINE) + input.addString(NEW_LINE) + + if input.Text() != TESTING_LINE { + t.Errorf("Expected test to only contains %s (%d) but found %s (%d)", + TESTING_LINE, len(TESTING_LINE), input.Text(), len(input.Text())) + } + + if strings.HasSuffix(input.Text(), NEW_LINE) { + t.Error("Unexpected newline at the end of TEXT") + } +} + +func TestInput_MultiLine_SingleEntry(t *testing.T) { + input := NewInput("", true) + input.addString(TESTING_LINE) + + if len(input.Lines()) != 1 { + t.Errorf("Invalid number of lines in input, expected 1 but found %d", len(input.Lines())) + } + if input.Text() != TESTING_LINE { + t.Errorf("Expected test to only contains %s (%d) but found %s (%d)", + TESTING_LINE, len(TESTING_LINE), input.Text(), len(input.Text())) + } +} + +func TestInput_MultiLine_ShiftLineDown(t *testing.T) { + input := NewInput("", true) + input.addString(TESTING_LINE) + input.addString(NEW_LINE) + input.addString(TESTING_LINE) + + if len(input.Lines()) != 2 { + t.Errorf("Expected 2 lines in input but found %d", len(input.Lines())) + } + if input.cursorLineIndex != 1 { + t.Errorf("Expected line cursor to be at index 1, found it at %d", input.cursorLineIndex) + } + + input.moveUp() + input.addString(NEW_LINE) + + if len(input.Lines()) != 3 { + t.Errorf("Expected 3 lines in input but found %d", len(input.Lines())) + } + if input.Lines()[0] != "" { + t.Errorf("Expected first line to be blank but found %s", input.Lines()[0]) + } +} + +func TestInput_MultiLine_MoveLeftToPreviousLine(t *testing.T) { + input := NewInput("", true) + input.addString(TESTING_LINE) + input.addString(NEW_LINE) + input.addString(TESTING_LINE) + + if input.cursorLinePos != len(TESTING_LINE) { + t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) + } + + // reset to 0 and move left + input.cursorLinePos = 0 + input.moveLeft() + + if input.cursorLineIndex != 0 { + t.Errorf("Expcted line cursor to be at index 0, found it at %d", input.cursorLineIndex) + } + if input.cursorLinePos != len(TESTING_LINE) { + t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) + } +} \ No newline at end of file From 265941feea4f6280822b1ed595354bfd8c5a580e Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Tue, 28 Feb 2017 17:16:36 -0800 Subject: [PATCH 2/6] Fixed formatting (gofmt) --- input.go | 100 +++++++++++++++++++++++++------------------------- input_test.go | 4 +- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/input.go b/input.go index 79329ef..defd028 100644 --- a/input.go +++ b/input.go @@ -1,24 +1,24 @@ package termui import ( - "strings" "github.com/nsf/termbox-go" + "strings" //"fmt" ) // default mappings between /sys/kbd events and multi-line inputs var multiLineCharMap = map[string]string{ - "": " ", - "": "\t", - "": "\n", + "": " ", + "": "\t", + "": "\n", "": "", } // default mappings between /sys/kbd events and single line inputs var singleLineCharMap = map[string]string{ - "": " ", - "": "\t", - "": "", + "": " ", + "": "\t", + "": "", "": "", } @@ -28,10 +28,10 @@ const NEW_LINE = "\n" // for the current line, and the position of the cursor in the current line as well as the index of the current // line in the full text of the input type EvtInput struct { - KeyStr string - LineText string - CursorPosition int - LineIndex int + KeyStr string + LineText string + CursorPosition int + LineIndex int } // Input is the main object for a text input. The object exposes the following public properties @@ -44,19 +44,19 @@ type EvtInput struct { // SpecialChars: a map[string]string of characters from the /sys/kbd events to actual strings in the content type Input struct { Block - TextFgColor Attribute - TextBgColor Attribute - IsCapturing bool - IsMultiLine bool - TextBuilder TextBuilder - SpecialChars map[string]string + TextFgColor Attribute + TextBgColor Attribute + IsCapturing bool + IsMultiLine bool + TextBuilder TextBuilder + SpecialChars map[string]string //DebugMode bool //debugMessage string // internal vars - lines []string - cursorLineIndex int + lines []string + cursorLineIndex int cursorLinePos int } @@ -64,11 +64,11 @@ type Input struct { // and whether it should be initialized as a multi-line innput field or not func NewInput(s string, isMultiLine bool) *Input { textArea := &Input{ - Block: *NewBlock(), - TextFgColor: ThemeAttr("par.text.fg"), - TextBgColor: ThemeAttr("par.text.bg"), - TextBuilder: NewMarkdownTxBuilder(), - IsMultiLine: isMultiLine, + Block: *NewBlock(), + TextFgColor: ThemeAttr("par.text.fg"), + TextBgColor: ThemeAttr("par.text.bg"), + TextBuilder: NewMarkdownTxBuilder(), + IsMultiLine: isMultiLine, cursorLineIndex: 0, cursorLinePos: 0, @@ -153,22 +153,21 @@ func (i *Input) Lines() []string { return i.lines } - // Private methods for the input field // TODO: handle delete key func (i *Input) backspace() { curLine := i.lines[i.cursorLineIndex] // at the beginning of the buffer, nothing to do - if len(curLine) == 0 && i.cursorLineIndex == 0{ + if len(curLine) == 0 && i.cursorLineIndex == 0 { return } // at the beginning of a line somewhere in the buffer if i.cursorLinePos == 0 { - prevLine := i.lines[i.cursorLineIndex - 1] + prevLine := i.lines[i.cursorLineIndex-1] // remove the newline character from the prevline - prevLine = prevLine[:len(curLine) - 1] + curLine + prevLine = prevLine[:len(curLine)-1] + curLine i.lines = append(i.lines[:i.cursorLineIndex], i.lines[i.cursorLineIndex+1:]...) i.cursorLineIndex-- i.cursorLinePos = len(prevLine) - 1 @@ -176,14 +175,14 @@ func (i *Input) backspace() { } // I'm at the end of a line - if i.cursorLinePos == len(curLine) - 1 { - i.lines[i.cursorLineIndex] = curLine[:len(curLine) - 1] + if i.cursorLinePos == len(curLine)-1 { + i.lines[i.cursorLineIndex] = curLine[:len(curLine)-1] i.cursorLinePos-- return } // I'm in the middle of a line - i.lines[i.cursorLineIndex] = curLine[:i.cursorLinePos - 1] + curLine[i.cursorLinePos:] + i.lines[i.cursorLineIndex] = curLine[:i.cursorLinePos-1] + curLine[i.cursorLinePos:] i.cursorLinePos-- } @@ -192,7 +191,7 @@ func (i *Input) addString(key string) { if key == NEW_LINE { // special case when we go back to the beginning of a buffer with multiple lines, prepend a new line if i.cursorLineIndex == 0 && len(i.lines) > 1 { - i.lines = append([]string { "" }, i.lines...) + i.lines = append([]string{""}, i.lines...) // this case handles newlines at the end of the file or in the middle of the file } else { @@ -209,9 +208,9 @@ func (i *Input) addString(key string) { i.lines = append( i.lines[:i.cursorLineIndex+1], append( - []string{ newString }, - i.lines[i.cursorLineIndex+1:]... - )... + []string{newString}, + i.lines[i.cursorLineIndex+1:]..., + )..., ) } // increment the line index, reset the cursor to the beginning and return to skip the next step @@ -221,7 +220,7 @@ func (i *Input) addString(key string) { } // cursor is at the end of the line - if i.cursorLinePos == len(i.lines[i.cursorLineIndex]) { + if i.cursorLinePos == len(i.lines[i.cursorLineIndex]) { //i.debugMessage ="end" i.lines[i.cursorLineIndex] += key @@ -255,7 +254,7 @@ func (i *Input) moveUp() { } // The previous line is just as long, we can move to the same position in the line - prevLine := i.lines[i.cursorLineIndex - 1] + prevLine := i.lines[i.cursorLineIndex-1] if len(prevLine) >= i.cursorLinePos { i.cursorLineIndex-- } else { @@ -267,13 +266,13 @@ func (i *Input) moveUp() { func (i *Input) moveDown() { // we are already on the last line, we just need to move the position to the end of the line - if i.cursorLineIndex == len(i.lines) - 1 { + if i.cursorLineIndex == len(i.lines)-1 { i.cursorLinePos = len(i.lines[i.cursorLineIndex]) - 1 return } // check if the cursor can move to the same position in the next line, otherwise move it to the end - nextLine := i.lines[i.cursorLineIndex + 1] + nextLine := i.lines[i.cursorLineIndex+1] if len(nextLine) >= i.cursorLinePos { i.cursorLineIndex++ } else { @@ -304,7 +303,6 @@ func (i *Input) moveRight() { i.cursorLinePos++ } - // Buffer implements Bufferer interface. func (i *Input) Buffer() Buffer { buf := i.Block.Buffer() @@ -346,18 +344,18 @@ func (i *Input) Buffer() Buffer { } /* - if i.DebugMode { - position := fmt.Sprintf("%s li: %d lp: %d n: %d", i.debugMessage, i.cursorLineIndex, i.cursorLinePos, len(i.lines)) + if i.DebugMode { + position := fmt.Sprintf("%s li: %d lp: %d n: %d", i.debugMessage, i.cursorLineIndex, i.cursorLinePos, len(i.lines)) - for idx, char := range position { - buf.Set(i.innerArea.Min.X+i.innerArea.Dx()-len(position) + idx, - i.innerArea.Min.Y+i.innerArea.Dy()-1, - Cell{Ch: char, Fg: i.TextFgColor, Bg: i.TextBgColor}) + for idx, char := range position { + buf.Set(i.innerArea.Min.X+i.innerArea.Dx()-len(position) + idx, + i.innerArea.Min.Y+i.innerArea.Dy()-1, + Cell{Ch: char, Fg: i.TextFgColor, Bg: i.TextBgColor}) + } } - } */ - termbox.SetCursor(i.cursorLinePos + 1, i.cursorLineIndex + 1) + termbox.SetCursor(i.cursorLinePos+1, i.cursorLineIndex+1) return buf } @@ -372,9 +370,9 @@ func (i *Input) getCharString(s string) string { func (i *Input) getInputEvt(key string) EvtInput { return EvtInput{ - KeyStr: key, - LineText: i.lines[i.cursorLineIndex], + KeyStr: key, + LineText: i.lines[i.cursorLineIndex], CursorPosition: i.cursorLinePos, - LineIndex: i.cursorLineIndex, + LineIndex: i.cursorLineIndex, } } diff --git a/input_test.go b/input_test.go index b48edc3..336821c 100644 --- a/input_test.go +++ b/input_test.go @@ -1,8 +1,8 @@ package termui import ( - "testing" "strings" + "testing" ) // TODO: More tests! @@ -81,4 +81,4 @@ func TestInput_MultiLine_MoveLeftToPreviousLine(t *testing.T) { if input.cursorLinePos != len(TESTING_LINE) { t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) } -} \ No newline at end of file +} From 42e72e0111501cf76a7e5e29eb6a866152931c16 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Wed, 1 Mar 2017 10:11:43 -0800 Subject: [PATCH 3/6] Bug fixes and added line numbering option --- input.go | 102 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/input.go b/input.go index defd028..4a42c28 100644 --- a/input.go +++ b/input.go @@ -1,9 +1,10 @@ package termui import ( + "github.com/gizak/termui" "github.com/nsf/termbox-go" + "strconv" "strings" - //"fmt" ) // default mappings between /sys/kbd events and multi-line inputs @@ -23,6 +24,7 @@ var singleLineCharMap = map[string]string{ } const NEW_LINE = "\n" +const LINE_NO_MIN_SPACE = 1000 // EvtInput defines the structure for the /input/* events. The event contains the last keystroke, the full text // for the current line, and the position of the cursor in the current line as well as the index of the current @@ -50,6 +52,7 @@ type Input struct { IsMultiLine bool TextBuilder TextBuilder SpecialChars map[string]string + ShowLineNo bool //DebugMode bool //debugMessage string @@ -69,13 +72,14 @@ func NewInput(s string, isMultiLine bool) *Input { TextBgColor: ThemeAttr("par.text.bg"), TextBuilder: NewMarkdownTxBuilder(), IsMultiLine: isMultiLine, + ShowLineNo: false, cursorLineIndex: 0, cursorLinePos: 0, } if s != "" { - textArea.lines = strings.Split(s, NEW_LINE) + textArea.SetText(s) } if isMultiLine { @@ -112,14 +116,10 @@ func (i *Input) StartCapture() { break } newString := i.getCharString(key) - /*i.bufferXPos += len(newString) - i.CurStringOffset += len(newString) - i.Text += newString*/ i.addString(newString) } SendCustomEvt("/input/kbd", i.getInputEvt(key)) - //print(i.Text) - i.Buffer() + Render(i) } }) @@ -148,6 +148,10 @@ func (i *Input) Text() string { } } +func (i *Input) SetText(text string) { + i.lines = strings.Split(text, NEW_LINE) +} + // Lines returns the slice of strings with the content of the input field. By default lines are separated by \n func (i *Input) Lines() []string { return i.lines @@ -267,7 +271,7 @@ func (i *Input) moveUp() { func (i *Input) moveDown() { // we are already on the last line, we just need to move the position to the end of the line if i.cursorLineIndex == len(i.lines)-1 { - i.cursorLinePos = len(i.lines[i.cursorLineIndex]) - 1 + i.cursorLinePos = len(i.lines[i.cursorLineIndex]) return } @@ -284,8 +288,11 @@ func (i *Input) moveDown() { func (i *Input) moveLeft() { // if we are at the beginning of the line move the cursor to the previous line if i.cursorLinePos == 0 { + origLine := i.cursorLineIndex i.moveUp() - i.cursorLinePos = len(i.lines[i.cursorLineIndex]) + if origLine > 0 { + i.cursorLinePos = len(i.lines[i.cursorLineIndex]) + } return } @@ -295,8 +302,11 @@ func (i *Input) moveLeft() { func (i *Input) moveRight() { // if we are at the end of the line move to the next if i.cursorLinePos >= len(i.lines[i.cursorLineIndex]) { + origLine := i.cursorLineIndex i.moveDown() - i.cursorLinePos = 0 + if origLine < len(i.lines)-1 { + i.cursorLinePos = 0 + } return } @@ -307,14 +317,16 @@ func (i *Input) moveRight() { func (i *Input) Buffer() Buffer { buf := i.Block.Buffer() - bufferLines := i.lines[:] - if i.IsMultiLine { - firstLine := 0 - lastLine := firstLine + i.innerArea.Dy() + // offset used to display the line numbers + textXOffset := 0 - if i.cursorLineIndex > lastLine { - firstLine += i.cursorLineIndex - lastLine - lastLine += i.cursorLineIndex - lastLine + bufferLines := i.lines[:] + firstLine := 0 + lastLine := i.innerArea.Dy() + if i.IsMultiLine { + if i.cursorLineIndex >= lastLine { + firstLine += i.cursorLineIndex - lastLine + 1 + lastLine += i.cursorLineIndex - lastLine + 1 } if len(i.lines) < lastLine { @@ -324,25 +336,76 @@ func (i *Input) Buffer() Buffer { } } + if i.ShowLineNo { + // forcing space for up to 1K + if lastLine < LINE_NO_MIN_SPACE { + textXOffset = len(strconv.Itoa(LINE_NO_MIN_SPACE)) + 2 + } else { + textXOffset = len(strconv.Itoa(lastLine)) + 2 // one space at the beginning and one at the end + } + } + text := strings.Join(bufferLines, NEW_LINE) + // if the last line is empty then we add a fake space to make sure line numbers are displayed + if len(bufferLines) > 0 && bufferLines[len(bufferLines)-1] == "" && i.ShowLineNo { + text += " " + } + fg, bg := i.TextFgColor, i.TextBgColor cs := i.TextBuilder.Build(text, fg, bg) y, x, n := 0, 0, 0 + lineNoCnt := 1 for n < len(cs) { w := cs[n].Width() + if x == 0 && i.ShowLineNo { + curLineNoString := " " + strconv.Itoa(lineNoCnt) + + strings.Join(make([]string, textXOffset-len(strconv.Itoa(lineNoCnt))-1), " ") + //i.debugMessage = "Line no: " + curLineNoString + curLineNoRunes := i.TextBuilder.Build(curLineNoString, fg, bg) + for lineNo := 0; lineNo < len(curLineNoRunes); lineNo++ { + buf.Set(i.innerArea.Min.X+x+lineNo, i.innerArea.Min.Y+y, curLineNoRunes[lineNo]) + } + lineNoCnt++ + } + if cs[n].Ch == '\n' { y++ + n++ x = 0 // set x = 0 + continue } - buf.Set(i.innerArea.Min.X+x, i.innerArea.Min.Y+y, cs[n]) + buf.Set(i.innerArea.Min.X+x+textXOffset, i.innerArea.Min.Y+y, cs[n]) n++ x += w } + xOffset := termui.TermWidth() - i.innerArea.Dx() + textXOffset + 1 + if i.BorderLeft { + xOffset-- + } + if i.BorderRight { + xOffset-- + } + + yOffset := termui.TermHeight() - i.innerArea.Dy() + if i.BorderTop { + yOffset-- + } + if i.BorderBottom { + yOffset-- + } + if lastLine > i.innerArea.Dy() { + yOffset += i.innerArea.Dy() - 1 + } else { + yOffset += i.cursorLineIndex + } + + termbox.SetCursor(i.cursorLinePos+xOffset, yOffset) + /* if i.DebugMode { position := fmt.Sprintf("%s li: %d lp: %d n: %d", i.debugMessage, i.cursorLineIndex, i.cursorLinePos, len(i.lines)) @@ -354,9 +417,6 @@ func (i *Input) Buffer() Buffer { } } */ - - termbox.SetCursor(i.cursorLinePos+1, i.cursorLineIndex+1) - return buf } From 3aa0498c165c9862bc72bb90c26a240f2dd2bfe3 Mon Sep 17 00:00:00 2001 From: Stefano Buliani Date: Wed, 1 Mar 2017 12:30:57 -0800 Subject: [PATCH 4/6] Fixed bug with cursor position and added envets by input name --- input.go | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/input.go b/input.go index 4a42c28..ad0b7f0 100644 --- a/input.go +++ b/input.go @@ -1,7 +1,6 @@ package termui import ( - "github.com/gizak/termui" "github.com/nsf/termbox-go" "strconv" "strings" @@ -36,14 +35,15 @@ type EvtInput struct { LineIndex int } -// Input is the main object for a text input. The object exposes the following public properties -// TextFgColor: color for the text -// TextBgColor: background color for the text box +// Input is the main object for a text input. The object exposes the following public properties: +// TextFgColor: color for the text. +// TextBgColor: background color for the text box. // IsCapturing: true if the input is currently capturing keyboard events, this is controlled by the StartCapture and -// StopCapture methods -// IsMultiline: Whether we should accept multiple lines of input or this is a singe line form field -// TextBuilder: An implementation of the TextBuilder interface to customize the look of the text on the screen -// SpecialChars: a map[string]string of characters from the /sys/kbd events to actual strings in the content +// StopCapture methods. +// IsMultiline: Whether we should accept multiple lines of input or this is a singe line form field. +// TextBuilder: An implementation of the TextBuilder interface to customize the look of the text on the screen. +// SpecialChars: a map[string]string of characters from the /sys/kbd events to actual strings in the content. +// Name: When specified, the Input uses its name to propagate events, for example /input//kbd. type Input struct { Block TextFgColor Attribute @@ -53,6 +53,9 @@ type Input struct { TextBuilder TextBuilder SpecialChars map[string]string ShowLineNo bool + Name string + CursorX int + CursorY int //DebugMode bool //debugMessage string @@ -118,7 +121,11 @@ func (i *Input) StartCapture() { newString := i.getCharString(key) i.addString(newString) } - SendCustomEvt("/input/kbd", i.getInputEvt(key)) + if i.Name == "" { + SendCustomEvt("/input/kbd", i.getInputEvt(key)) + } else { + SendCustomEvt("/input/" + i.Name + "/kbd", i.getInputEvt(key)) + } Render(i) } @@ -383,28 +390,25 @@ func (i *Input) Buffer() Buffer { x += w } - xOffset := termui.TermWidth() - i.innerArea.Dx() + textXOffset + 1 + cursorXOffset := i.X + textXOffset if i.BorderLeft { - xOffset-- - } - if i.BorderRight { - xOffset-- + cursorXOffset++ } - yOffset := termui.TermHeight() - i.innerArea.Dy() + cursorYOffset := i.Y// termui.TermHeight() - i.innerArea.Dy() if i.BorderTop { - yOffset-- - } - if i.BorderBottom { - yOffset-- + cursorYOffset++ } if lastLine > i.innerArea.Dy() { - yOffset += i.innerArea.Dy() - 1 + cursorYOffset += i.innerArea.Dy() - 1 } else { - yOffset += i.cursorLineIndex + cursorYOffset += i.cursorLineIndex + } + if i.IsCapturing { + i.CursorX = i.cursorLinePos+cursorXOffset + i.CursorY = cursorYOffset + termbox.SetCursor(i.cursorLinePos+cursorXOffset, cursorYOffset) } - - termbox.SetCursor(i.cursorLinePos+xOffset, yOffset) /* if i.DebugMode { From 921b9172a109091d06ce459cb1631aa2d52a9b05 Mon Sep 17 00:00:00 2001 From: sapessi Date: Tue, 18 Jul 2017 14:29:47 -0700 Subject: [PATCH 5/6] Fixed issues mentioned in pull request and added unit tests --- input.go | 7 ++++++- input_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/input.go b/input.go index ad0b7f0..8796303 100644 --- a/input.go +++ b/input.go @@ -82,7 +82,7 @@ func NewInput(s string, isMultiLine bool) *Input { } if s != "" { - textArea.SetText(s) + textArea.addString(s) } if isMultiLine { @@ -168,6 +168,11 @@ func (i *Input) Lines() []string { // TODO: handle delete key func (i *Input) backspace() { + // we have no lines yet, nothing to do. + if len(i.lines) == 0 { + return; + } + curLine := i.lines[i.cursorLineIndex] // at the beginning of the buffer, nothing to do if len(curLine) == 0 && i.cursorLineIndex == 0 { diff --git a/input_test.go b/input_test.go index 336821c..09c0a81 100644 --- a/input_test.go +++ b/input_test.go @@ -82,3 +82,31 @@ func TestInput_MultiLine_MoveLeftToPreviousLine(t *testing.T) { t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) } } + +// Test issues mentioned in the pull request: https://github.com/gizak/termui/pull/129 +func TestInput_BackspaceOnFirstChar_NoAction(t *testing.T) { + input := NewInput("", false) + + // backspace here should not throw an error + input.backspace() + + input.addString(TESTING_LINE) + + if input.cursorLinePos != len(TESTING_LINE) { + t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) + } +} + +// Test issues mentioned in the pull request: https://github.com/gizak/termui/pull/129 +func TestInput_LeftOnFirstChar_NoAction(t *testing.T) { + input := NewInput("", false) + + // backspace here should not throw an error + input.moveLeft() + + input.addString(TESTING_LINE) + + if input.cursorLinePos != len(TESTING_LINE) { + t.Errorf("Expected cursor to be at position %d, found it at %d", len(TESTING_LINE), input.cursorLinePos) + } +} From 9199addba79436d0a0a31da61cb46e3c250d4cf0 Mon Sep 17 00:00:00 2001 From: sapessi Date: Sat, 12 May 2018 06:18:21 -0700 Subject: [PATCH 6/6] Bug fixes and added formatter --- input.go | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/input.go b/input.go index 8796303..291b82e 100644 --- a/input.go +++ b/input.go @@ -1,9 +1,10 @@ package termui import ( - "github.com/nsf/termbox-go" "strconv" "strings" + + "github.com/nsf/termbox-go" ) // default mappings between /sys/kbd events and multi-line inputs @@ -25,6 +26,9 @@ var singleLineCharMap = map[string]string{ const NEW_LINE = "\n" const LINE_NO_MIN_SPACE = 1000 +// TextRenderer can modify text content before it is added to the buffer for rendering +type TextRenderer func(word string, lineNo int, linePos int) string + // EvtInput defines the structure for the /input/* events. The event contains the last keystroke, the full text // for the current line, and the position of the cursor in the current line as well as the index of the current // line in the full text of the input @@ -56,6 +60,7 @@ type Input struct { Name string CursorX int CursorY int + Formatter TextRenderer //DebugMode bool //debugMessage string @@ -124,7 +129,7 @@ func (i *Input) StartCapture() { if i.Name == "" { SendCustomEvt("/input/kbd", i.getInputEvt(key)) } else { - SendCustomEvt("/input/" + i.Name + "/kbd", i.getInputEvt(key)) + SendCustomEvt("/input/"+i.Name+"/kbd", i.getInputEvt(key)) } Render(i) @@ -170,7 +175,7 @@ func (i *Input) Lines() []string { func (i *Input) backspace() { // we have no lines yet, nothing to do. if len(i.lines) == 0 { - return; + return } curLine := i.lines[i.cursorLineIndex] @@ -363,9 +368,23 @@ func (i *Input) Buffer() Buffer { if len(bufferLines) > 0 && bufferLines[len(bufferLines)-1] == "" && i.ShowLineNo { text += " " } + finalText := "" + if i.Formatter != nil { + for _, w := range strings.Split(text, " ") { + if strings.HasPrefix(w, "\n") { + finalText += "\n" + i.Formatter(strings.Trim(w, "\n"), 0, 0) + " " + } else if strings.HasSuffix(w, "\n") { + finalText += i.Formatter(strings.Trim(w, "\n"), 0, 0) + "\n" + } else { + finalText += i.Formatter(strings.Trim(w, "\n"), 0, 0) + " " + } + } + } else { + finalText = text + } fg, bg := i.TextFgColor, i.TextBgColor - cs := i.TextBuilder.Build(text, fg, bg) + cs := i.TextBuilder.Build(finalText, fg, bg) y, x, n := 0, 0, 0 lineNoCnt := 1 @@ -400,7 +419,7 @@ func (i *Input) Buffer() Buffer { cursorXOffset++ } - cursorYOffset := i.Y// termui.TermHeight() - i.innerArea.Dy() + cursorYOffset := i.Y // termui.TermHeight() - i.innerArea.Dy() if i.BorderTop { cursorYOffset++ } @@ -410,22 +429,11 @@ func (i *Input) Buffer() Buffer { cursorYOffset += i.cursorLineIndex } if i.IsCapturing { - i.CursorX = i.cursorLinePos+cursorXOffset + i.CursorX = i.cursorLinePos + cursorXOffset i.CursorY = cursorYOffset termbox.SetCursor(i.cursorLinePos+cursorXOffset, cursorYOffset) } - /* - if i.DebugMode { - position := fmt.Sprintf("%s li: %d lp: %d n: %d", i.debugMessage, i.cursorLineIndex, i.cursorLinePos, len(i.lines)) - - for idx, char := range position { - buf.Set(i.innerArea.Min.X+i.innerArea.Dx()-len(position) + idx, - i.innerArea.Min.Y+i.innerArea.Dy()-1, - Cell{Ch: char, Fg: i.TextFgColor, Bg: i.TextBgColor}) - } - } - */ return buf }