mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
307 lines
9.1 KiB
Go
307 lines
9.1 KiB
Go
// This file is part of Gopher2600.
|
|
//
|
|
// Gopher2600 is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Gopher2600 is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// *** NOTE: all historical versions of this file, as found in any
|
|
// git repository, are also covered by the licence, even when this
|
|
// notice is not present ***
|
|
|
|
package colorterm
|
|
|
|
import (
|
|
"gopher2600/debugger/terminal"
|
|
"gopher2600/debugger/terminal/colorterm/ansi"
|
|
"gopher2600/debugger/terminal/colorterm/easyterm"
|
|
"gopher2600/errors"
|
|
"gopher2600/gui"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// #cursor #keys #tab #completion
|
|
|
|
// TermRead implements the terminal.Terminal interface
|
|
func (ct *ColorTerminal) TermRead(input []byte, prompt terminal.Prompt, events chan gui.Event, eventHandler func(gui.Event) error) (int, error) {
|
|
|
|
if ct.silenced {
|
|
return 0, nil
|
|
}
|
|
|
|
// ctrl-c handling: currently, we put the terminal into rawmode and listen
|
|
// for ctrl-c event using the readRune reader.
|
|
|
|
ct.RawMode()
|
|
defer ct.CanonicalMode()
|
|
|
|
// er is used to store encoded runes (length of 4 should be enough)
|
|
er := make([]byte, 4)
|
|
|
|
inputLen := 0
|
|
cursorPos := 0
|
|
history := len(ct.commandHistory)
|
|
|
|
// liveBuffInput is used to store the latest input when we scroll through
|
|
// history - we don't want to lose what we've typed in case the user wants
|
|
// to resume where we left off
|
|
liveHistory := make([]byte, cap(input))
|
|
liveHistoryLen := 0
|
|
|
|
// the method for cursor placement is as follows:
|
|
// for each iteration in the loop
|
|
// 1. store current cursor position
|
|
// 2. clear the current line
|
|
// 3. output the prompt
|
|
// 4. output the input buffer
|
|
// 5. restore the cursor position
|
|
//
|
|
// for this to work we need to place the cursor in it's initial position
|
|
// before we begin the loop
|
|
ct.EasyTerm.TermPrint("\r")
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(len(prompt.Content)))
|
|
|
|
for {
|
|
ct.EasyTerm.TermPrint(ansi.CursorStore)
|
|
ct.TermPrintLine(prompt.Style, "%s%s", ansi.ClearLine, prompt.Content)
|
|
ct.TermPrintLine(terminal.StyleInput, string(input[:inputLen]))
|
|
ct.EasyTerm.TermPrint(ansi.CursorRestore)
|
|
|
|
select {
|
|
case event := <-events:
|
|
// handle functions that are passsed on over interruptChannel. these can
|
|
// be things like events from the television GUI. eg. mouse clicks,
|
|
// key presses, etc.
|
|
ct.EasyTerm.TermPrint(ansi.CursorStore)
|
|
err := eventHandler(event)
|
|
ct.EasyTerm.TermPrint(ansi.CursorRestore)
|
|
if err != nil {
|
|
return inputLen + 1, err
|
|
}
|
|
|
|
case readRune := <-ct.reader:
|
|
if readRune.err != nil {
|
|
return inputLen, readRune.err
|
|
}
|
|
|
|
switch readRune.r {
|
|
case easyterm.KeyTab:
|
|
if ct.tabCompletion != nil {
|
|
s := ct.tabCompletion.Complete(string(input[:cursorPos]))
|
|
|
|
// the difference in the length of the new input and the old
|
|
// input
|
|
d := len(s) - cursorPos
|
|
|
|
if inputLen+d <= len(input) {
|
|
// append everything after the cursor to the new string and copy
|
|
// into input array
|
|
s += string(input[cursorPos:])
|
|
copy(input, []byte(s))
|
|
|
|
// advance character to end of completed word
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(d))
|
|
cursorPos += d
|
|
|
|
// note new used-length of input array
|
|
inputLen += d
|
|
}
|
|
}
|
|
|
|
case easyterm.KeyInterrupt:
|
|
// CTRL-C -- note that there is a ctrl-c signal handler, set up in
|
|
// debugger.Start(), that controls the main debugging loop. this
|
|
// ctrl-c handler by contrast, controls the user input loop
|
|
|
|
if inputLen > 0 {
|
|
// clear current input
|
|
inputLen = 0
|
|
cursorPos = 0
|
|
ct.EasyTerm.TermPrint("\r")
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(len(prompt.Content)))
|
|
} else {
|
|
// there is no input so return UserInterrupt error
|
|
ct.EasyTerm.TermPrint("\n")
|
|
return inputLen + 1, errors.New(errors.UserInterrupt)
|
|
}
|
|
|
|
case easyterm.KeySuspend:
|
|
// CTRL-Z
|
|
return inputLen + 1, errors.New(errors.UserSuspend)
|
|
|
|
case easyterm.KeyCarriageReturn:
|
|
// CARRIAGE RETURN
|
|
|
|
// check to see if input is the same as the last history entry
|
|
newEntry := false
|
|
if inputLen > 0 {
|
|
newEntry = true
|
|
if len(ct.commandHistory) > 0 {
|
|
lastHistoryEntry := ct.commandHistory[len(ct.commandHistory)-1].input
|
|
if len(lastHistoryEntry) == inputLen {
|
|
newEntry = false
|
|
for i := 0; i < inputLen; i++ {
|
|
if input[i] != lastHistoryEntry[i] {
|
|
newEntry = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if input is not the same as the last history entry then append a
|
|
// new entry to the history list
|
|
if newEntry {
|
|
nh := make([]byte, inputLen)
|
|
copy(nh, input[:inputLen])
|
|
ct.commandHistory = append(ct.commandHistory, command{input: nh})
|
|
}
|
|
|
|
ct.EasyTerm.TermPrint("\r\n")
|
|
return inputLen + 1, nil
|
|
|
|
case easyterm.KeyEsc:
|
|
// ESCAPE SEQUENCE BEGIN
|
|
readRune = <-ct.reader
|
|
if readRune.err != nil {
|
|
return inputLen, readRune.err
|
|
}
|
|
switch readRune.r {
|
|
case easyterm.EscCursor:
|
|
// CURSOR KEY
|
|
readRune = <-ct.reader
|
|
if readRune.err != nil {
|
|
return inputLen, readRune.err
|
|
}
|
|
|
|
switch readRune.r {
|
|
case easyterm.CursorUp:
|
|
// move up through command history
|
|
if len(ct.commandHistory) > 0 {
|
|
// if we're at the end of the command history then store
|
|
// the current input in liveBuffInput for possible later editing
|
|
if history == len(ct.commandHistory) {
|
|
copy(liveHistory, input[:inputLen])
|
|
liveHistoryLen = inputLen
|
|
}
|
|
|
|
if history > 0 {
|
|
history--
|
|
l := len(ct.commandHistory[history].input)
|
|
|
|
// length check in case input buffer is
|
|
// shorted from when history entry was added
|
|
if l < len(input) {
|
|
copy(input, ct.commandHistory[history].input)
|
|
inputLen = len(ct.commandHistory[history].input)
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(inputLen - cursorPos))
|
|
cursorPos = inputLen
|
|
}
|
|
}
|
|
}
|
|
case easyterm.CursorDown:
|
|
// move down through command history
|
|
if len(ct.commandHistory) > 0 {
|
|
if history < len(ct.commandHistory)-1 {
|
|
history++
|
|
l := len(ct.commandHistory[history].input)
|
|
if l < len(input) {
|
|
copy(input, ct.commandHistory[history].input)
|
|
inputLen = len(ct.commandHistory[history].input)
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(inputLen - cursorPos))
|
|
cursorPos = inputLen
|
|
}
|
|
} else if history == len(ct.commandHistory)-1 {
|
|
history++
|
|
|
|
// length check not really required because
|
|
// liveHistroy should not ever be greater
|
|
// in length than that of input buffer
|
|
if liveHistoryLen < len(input) {
|
|
copy(input, liveHistory)
|
|
inputLen = liveHistoryLen
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(inputLen - cursorPos))
|
|
cursorPos = inputLen
|
|
}
|
|
}
|
|
}
|
|
case easyterm.CursorForward:
|
|
// move forward through current command input
|
|
if cursorPos < inputLen {
|
|
ct.EasyTerm.TermPrint(ansi.CursorForwardOne)
|
|
cursorPos++
|
|
}
|
|
case easyterm.CursorBackward:
|
|
// move backward through current command input
|
|
if cursorPos > 0 {
|
|
ct.EasyTerm.TermPrint(ansi.CursorBackwardOne)
|
|
cursorPos--
|
|
}
|
|
|
|
case easyterm.EscDelete:
|
|
// DELETE
|
|
if cursorPos < inputLen {
|
|
copy(input[cursorPos:], input[cursorPos+1:])
|
|
inputLen--
|
|
history = len(ct.commandHistory)
|
|
}
|
|
|
|
// eat the third character in the sequence
|
|
readRune = <-ct.reader
|
|
|
|
case easyterm.EscHome:
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(-cursorPos))
|
|
cursorPos = 0
|
|
|
|
case easyterm.EscEnd:
|
|
ct.EasyTerm.TermPrint(ansi.CursorMove(inputLen - cursorPos))
|
|
cursorPos = inputLen
|
|
}
|
|
}
|
|
|
|
case easyterm.KeyBackspace:
|
|
// BACKSPACE
|
|
if cursorPos > 0 {
|
|
copy(input[cursorPos-1:], input[cursorPos:])
|
|
ct.EasyTerm.TermPrint(ansi.CursorBackwardOne)
|
|
cursorPos--
|
|
inputLen--
|
|
history = len(ct.commandHistory)
|
|
}
|
|
|
|
default:
|
|
if unicode.IsDigit(readRune.r) || unicode.IsLetter(readRune.r) || unicode.IsSpace(readRune.r) || unicode.IsPunct(readRune.r) || unicode.IsSymbol(readRune.r) {
|
|
|
|
l := utf8.EncodeRune(er, readRune.r)
|
|
|
|
// make sure we don't overflow the input buffer
|
|
if cursorPos+l <= len(input) {
|
|
ct.EasyTerm.TermPrint(ansi.CursorForwardOne)
|
|
|
|
// insert new character into input stream at current cursor
|
|
// position
|
|
copy(input[cursorPos+l:], input[cursorPos:])
|
|
copy(input[cursorPos:], er[:l])
|
|
cursorPos++
|
|
|
|
inputLen += l
|
|
|
|
// make sure history pointer is at the end of the command
|
|
// history array
|
|
history = len(ct.commandHistory)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|