Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

term: add support to catch keys directly when entered #5

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ type Terminal struct {
// may be empty if the terminal doesn't support them.
Escape *EscapeCodes

// OnKey stores pairs of runes (which represents a keycode) and functions.
// Whenever a key is entered and the keycode of it exists in this map
// the corresponding functions gets called. The function takes the key
// as its only argument and returns if the normal behaviour, if existent,
// should continue.
OnKey map[rune]func(key rune) (resume bool)

// lock protects the terminal and the state in this object from
// concurrent processing of a key press and a Write() call.
lock sync.Mutex
Expand Down Expand Up @@ -449,9 +456,21 @@ func visualLength(runes []rune) int {
return length
}

// handleOnKey checks if the given key has a OnKey function assigned to it
// which, if existent, gets called. Returns false if the corresponding OnKey
// function returns also false, on all other cases true
func (t *Terminal) handleOnKey(key rune) bool {
f, ok := t.OnKey[key]
return !ok || ok && f(key)
}

// handleKey processes the given key and, optionally, returns a line of text
// that the user has entered.
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
if !t.handleOnKey(key) {
return
}

Comment on lines +471 to +474
Copy link
Contributor

@ardnew ardnew Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of inserting the check here as well as sprinkled judiciously throughout readLine (a little hard to follow), consider inserting it in one common location. I haven't verified, but immediately after checking for utf8.RuneError, around line 759, seems like it would catch all these same cases.

(I assume handleKey was the first guess at this "common location", which sounds very reasonable to me, given the name)

if t.pasteActive && key != keyEnter {
t.addKeyToLine(key)
return
Expand Down Expand Up @@ -739,22 +758,30 @@ func (t *Terminal) readLine() (line string, err error) {
}
if !t.pasteActive {
if key == keyCtrlD {
if len(t.line) == 0 {
return "", io.EOF
if t.handleOnKey(keyCtrlD) {
if len(t.line) == 0 {
return "", io.EOF
}
}
}
if key == keyCtrlC {
return "", io.EOF
if t.handleOnKey(keyCtrlC) {
return "", io.EOF
}
}
if key == keyPasteStart {
t.pasteActive = true
if len(t.line) == 0 {
lineIsPasted = true
if t.handleOnKey(keyPasteStart) {
t.pasteActive = true
if len(t.line) == 0 {
lineIsPasted = true
}
}
continue
}
} else if key == keyPasteEnd {
t.pasteActive = false
if t.handleOnKey(keyPasteEnd) {
t.pasteActive = false
}
continue
}
if !t.pasteActive {
Expand Down
59 changes: 59 additions & 0 deletions terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,65 @@ func TestKeyPresses(t *testing.T) {
}
}

var onKeyTests = []struct {
in string
out string
onKey map[rune]func(key rune) bool
err error
}{
{
// Ctrl-C should terminate the readline
in: "a\003\r",
err: io.EOF,
},
{
// Ctrl-C should do nothing
in: "a\003\r",
onKey: map[rune]func(key rune) bool{keyCtrlC: func(key rune) bool {
return false
}},
out: "a",
},
{
// catch Ctrl-D but terminate with Ctrl-C
in: "\004\r",
onKey: map[rune]func(key rune) bool{keyCtrlC: func(key rune) bool {
return false
}},
err: io.EOF,
},
{
// catch Ctrl-D but resume (expressed with the positive return)
in: "\004\r",
onKey: map[rune]func(key rune) bool{keyCtrlD: func(key rune) bool {
return true
}},
err: io.EOF,
},
}

func TestOnKey(t *testing.T) {
for i, test := range onKeyTests {
for j := 1; j < len(test.in); j++ {
c := &MockTerminal{
toSend: []byte(test.in),
bytesPerRead: j,
}
ss := NewTerminal(c, "")
ss.OnKey = test.onKey
line, err := ss.ReadLine()
if line != test.out {
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.out)
break
}
if err != test.err {
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
break
}
}
}
}

var renderTests = []struct {
in string
received string
Expand Down