Skip to content

Commit b9747bd

Browse files
authored
Merge pull request #81 from h3x0c4t/unicode
Unicode support
2 parents 45ba7a7 + 1a194fa commit b9747bd

3 files changed

Lines changed: 76 additions & 14 deletions

File tree

internal/core/keys.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/reeflective/readline/inputrc"
1111
"github.com/reeflective/readline/internal/strutil"
12+
"github.com/rivo/uniseg"
1213
)
1314

1415
const (
@@ -22,7 +23,7 @@ var Stdin io.ReadCloser = os.Stdin
2223

2324
var rxRcvCursorPos = regexp.MustCompile(`\x1b\[([0-9]+);([0-9]+)R`)
2425

25-
// Keys is used read, manage and use keys input by the shell user.
26+
// Keys is used to read, manage and use keys input by the shell user.
2627
type Keys struct {
2728
buf []byte // Keys read and waiting to be used.
2829
matched []rune // Keys that have been successfully matched against a bind.
@@ -97,6 +98,20 @@ func WaitAvailableKeys(keys *Keys, cfg *inputrc.Config) {
9798
}
9899
}
99100

101+
// PeekKey returns the first key in the stack, without removing it.
102+
func PeekKey(keys *Keys) (key byte, empty bool) {
103+
switch {
104+
case len(keys.buf) > 0:
105+
key = keys.buf[0]
106+
case len(keys.macroKeys) > 0:
107+
key = byte(keys.macroKeys[0])
108+
default:
109+
return byte(0), true
110+
}
111+
112+
return key, false
113+
}
114+
100115
// PopKey is used to pop a key off the key stack without
101116
// yet marking this key as having matched a bind command.
102117
func PopKey(keys *Keys) (key byte, empty bool) {
@@ -114,18 +129,40 @@ func PopKey(keys *Keys) (key byte, empty bool) {
114129
return key, false
115130
}
116131

117-
// PeekKey returns the first key in the stack, without removing it.
118-
func PeekKey(keys *Keys) (key byte, empty bool) {
132+
// PeekChar returns the first character in the stack, without
133+
// removing it (in order to provide Unicode support).
134+
func PeekChar(keys *Keys) (char []byte, empty bool) {
119135
switch {
120136
case len(keys.buf) > 0:
121-
key = keys.buf[0]
137+
// Use the uniseg library to correctly determine where each characters stop.
138+
char, _, _, _ = uniseg.FirstGraphemeCluster(keys.buf, -1)
139+
return char, false
140+
122141
case len(keys.macroKeys) > 0:
123-
key = byte(keys.macroKeys[0])
142+
// Macros already store keys as runes, just pick one.
143+
// Maybe long-term we should find a remedy to this: storing
144+
// macro keys as runes is inconsistent with the rest of our code.
145+
return []byte(string(keys.macroKeys[0])), false
146+
124147
default:
125-
return byte(0), true
148+
return nil, true
126149
}
150+
}
127151

128-
return key, false
152+
// PopChar is used to pop a character off the key stack without
153+
// yet marking this key as having matched a bind command.
154+
func PopChar(keys *Keys) (char []byte, empty bool) {
155+
switch {
156+
case len(keys.buf) > 0:
157+
char, keys.buf, _, _ = uniseg.FirstGraphemeCluster(keys.buf, -1)
158+
case len(keys.macroKeys) > 0:
159+
char = []byte(string(keys.macroKeys[0]))
160+
keys.macroKeys = keys.macroKeys[1:]
161+
default:
162+
return nil, true
163+
}
164+
165+
return char, false
129166
}
130167

131168
// MatchedKeys is used to indicate how many keys have been evaluated against the shell

internal/keymap/dispatch.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,35 @@ func MatchMain(eng *Engine) (bind inputrc.Bind, command func(), prefix bool) {
9696
return bind, command, prefix
9797
}
9898

99+
func (m *Engine) makeMatch(active, prefixed inputrc.Bind) (prefix bool) {
100+
m.active = active
101+
m.prefixed = prefixed
102+
return m.prefixed.Action != ""
103+
}
104+
99105
func (m *Engine) dispatchKeys(binds map[string]inputrc.Bind) (bind inputrc.Bind, prefix bool, read, matched []byte) {
106+
// Support for Unicode: if the character is multi-byte (UTF-8), consume all its bytes
107+
// and treat it as a single self-insert action. Note that we just peek the characters
108+
// here, so if it's actually not a UTF-8 character, we just keep going and re-peek later.
109+
char, empty := core.PeekChar(m.keys)
110+
if empty {
111+
return m.active, prefix, read, matched
112+
}
113+
114+
if len(char) > 1 {
115+
read = append(read, char...)
116+
match := inputrc.Bind{
117+
Action: "self-insert",
118+
Macro: false,
119+
}
120+
121+
matched = append(matched, char...)
122+
prefix = m.makeMatch(match, inputrc.Bind{})
123+
core.PopChar(m.keys)
124+
125+
return m.active, prefix, read, matched
126+
}
127+
100128
for {
101129
// Read a single byte from the input buffer.
102130
// This mimics the way Bash reads input when the inputrc option `byte-oriented` is set.
@@ -114,9 +142,7 @@ func (m *Engine) dispatchKeys(binds map[string]inputrc.Bind) (bind inputrc.Bind,
114142
// If the current keys have no matches but the previous
115143
// matching process found a prefix, use it with the keys.
116144
if match.Action == "" && len(prefixed) == 0 {
117-
prefix = false
118-
m.active = m.prefixed
119-
m.prefixed = inputrc.Bind{}
145+
prefix = m.makeMatch(m.prefixed, inputrc.Bind{})
120146

121147
// FIX related to Github issue #73, where someone
122148
// complains not being able to input Unicode characters
@@ -148,9 +174,7 @@ func (m *Engine) dispatchKeys(binds map[string]inputrc.Bind) (bind inputrc.Bind,
148174
}
149175

150176
// Or an exact match, so drop any prefixed one.
151-
prefix = false
152-
m.active = match
153-
m.prefixed = inputrc.Bind{}
177+
prefix = m.makeMatch(match, inputrc.Bind{})
154178

155179
break
156180
}

internal/strutil/len.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package strutil
33
import (
44
"strings"
55

6+
"github.com/rivo/uniseg"
7+
68
"github.com/reeflective/readline/internal/color"
79
"github.com/reeflective/readline/internal/term"
8-
"github.com/rivo/uniseg"
910
)
1011

1112
// FormatTabs replaces all '\t' occurrences in a string with 6 spaces each.

0 commit comments

Comments
 (0)