Skip to content

Commit b3f7d3e

Browse files
authored
Readline NY (#33)
1 parent d8f27c9 commit b3f7d3e

File tree

3 files changed

+91
-52
lines changed

3 files changed

+91
-52
lines changed

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/chzyer/readline v1.5.1
1111
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
1212
github.com/fatih/color v1.18.0
13+
github.com/nyaosorg/go-readline-ny v1.9.0
1314
github.com/spf13/cobra v1.8.0
1415
github.com/spf13/viper v1.18.2
1516
)
@@ -22,8 +23,12 @@ require (
2223
github.com/magiconair/properties v1.8.7 // indirect
2324
github.com/mattn/go-colorable v0.1.13 // indirect
2425
github.com/mattn/go-isatty v0.0.20 // indirect
26+
github.com/mattn/go-runewidth v0.0.16 // indirect
27+
github.com/mattn/go-tty v0.0.7 // indirect
2528
github.com/mitchellh/mapstructure v1.5.0 // indirect
29+
github.com/nyaosorg/go-box/v2 v2.2.1 // indirect
2630
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
31+
github.com/rivo/uniseg v0.4.7 // indirect
2732
github.com/sagikazarmark/locafero v0.4.0 // indirect
2833
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
2934
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -36,7 +41,7 @@ require (
3641
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
3742
golang.org/x/sys v0.31.0 // indirect
3843
golang.org/x/term v0.30.0 // indirect
39-
golang.org/x/text v0.14.0 // indirect
44+
golang.org/x/text v0.19.0 // indirect
4045
gopkg.in/ini.v1 v1.67.0 // indirect
4146
gopkg.in/yaml.v3 v3.0.1 // indirect
4247
)

go.sum

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,24 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
4141
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
4242
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
4343
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
44+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
45+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
46+
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
47+
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
4448
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
4549
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
50+
github.com/nyaosorg/go-box/v2 v2.2.1 h1:1SAtgLE+uYCA8oycJGKFrzsYaOWmxXWiqH8PR9wvnIo=
51+
github.com/nyaosorg/go-box/v2 v2.2.1/go.mod h1:IAW2+ri9apF0QTlZ5r5lkU+Rfnsigkmt6yODV7JMZ5E=
52+
github.com/nyaosorg/go-readline-ny v1.9.0 h1:5jXBhg5qeOyJK/2R3lo6+VLx30HdjPh2dw7WZmv8J5c=
53+
github.com/nyaosorg/go-readline-ny v1.9.0/go.mod h1:54AzdC//M5EzTWRdvUHv2ChuYgp58mRrStTlpxiCmT0=
4654
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
4755
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
4856
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4957
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
5058
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
60+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
61+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
5162
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
5263
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
5364
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -91,8 +102,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
91102
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
92103
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
93104
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
94-
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
95-
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
105+
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
106+
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
96107
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
97108
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
98109
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/chat.go

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
"time"
1111

1212
"github.com/alvinunreal/tmuxai/config"
13-
"github.com/alvinunreal/tmuxai/logger"
14-
"github.com/chzyer/readline"
13+
"github.com/nyaosorg/go-readline-ny"
14+
"github.com/nyaosorg/go-readline-ny/completion"
15+
"github.com/nyaosorg/go-readline-ny/keys"
16+
"github.com/nyaosorg/go-readline-ny/simplehistory"
1517
)
1618

1719
// Message represents a chat message
@@ -37,30 +39,42 @@ func NewCLIInterface(manager *Manager) *CLIInterface {
3739
func (c *CLIInterface) Start(initMessage string) error {
3840
c.printWelcomeMessage()
3941

40-
rl, err := readline.NewEx(&readline.Config{
41-
Prompt: c.manager.GetPrompt(),
42-
HistoryFile: config.GetConfigFilePath("history"),
43-
HistorySearchFold: true,
44-
InterruptPrompt: "^C",
45-
EOFPrompt: "exit",
46-
DisableAutoSaveHistory: false,
47-
AutoComplete: c.newCompleter(),
48-
})
49-
if err != nil {
50-
return fmt.Errorf("failed to initialize readline: %w", err)
42+
// Initialize history
43+
history := simplehistory.New()
44+
historyFilePath := config.GetConfigFilePath("history")
45+
46+
// Load history from file if it exists
47+
if historyData, err := os.ReadFile(historyFilePath); err == nil {
48+
for _, line := range strings.Split(string(historyData), "\n") {
49+
if line = strings.TrimSpace(line); line != "" {
50+
history.Add(line)
51+
}
52+
}
53+
}
54+
55+
// Initialize editor
56+
editor := &readline.Editor{
57+
PromptWriter: func(w io.Writer) (int, error) {
58+
return io.WriteString(w, c.manager.GetPrompt())
59+
},
60+
History: history,
61+
HistoryCycling: true,
5162
}
52-
defer rl.Close()
63+
64+
// Bind TAB key to completion
65+
editor.BindKey(keys.CtrlI, c.newCompleter())
5366

5467
if initMessage != "" {
5568
fmt.Printf("%s%s\n", c.manager.GetPrompt(), initMessage)
5669
c.processInput(initMessage)
5770
}
5871

72+
ctx := context.Background()
73+
5974
for {
60-
rl.SetPrompt(c.manager.GetPrompt())
75+
line, err := editor.ReadLine(ctx)
6176

62-
line, err := rl.Readline()
63-
if err == readline.ErrInterrupt {
77+
if err == readline.CtrlC {
6478
// Ctrl+C pressed, clear the line and continue
6579
continue
6680
} else if err == io.EOF {
@@ -70,15 +84,31 @@ func (c *CLIInterface) Start(initMessage string) error {
7084
return err
7185
}
7286

73-
input := strings.TrimSpace(line)
74-
if input == "exit" || input == "quit" {
87+
// Save history
88+
if line != "" {
89+
history.Add(line)
90+
91+
// Build history data by iterating through all entries
92+
historyLines := make([]string, 0, history.Len())
93+
for i := 0; i < history.Len(); i++ {
94+
historyLines = append(historyLines, history.At(i))
95+
}
96+
historyData := strings.Join(historyLines, "\n")
97+
os.WriteFile(historyFilePath, []byte(historyData), 0644)
98+
}
99+
100+
// Process the input (preserving multiline content)
101+
input := line // Keep the original line including newlines
102+
103+
// Check for exit/quit commands (only if it's the entire line content)
104+
trimmed := strings.TrimSpace(input)
105+
if trimmed == "exit" || trimmed == "quit" {
75106
return nil
76107
}
77-
if input == "" {
108+
if trimmed == "" {
78109
continue
79110
}
80111

81-
logger.Debug("Processing User input: %s", input)
82112
c.processInput(input)
83113
}
84114
}
@@ -128,33 +158,26 @@ func (c *CLIInterface) processInput(input string) {
128158
signal.Stop(sigChan)
129159
}
130160

131-
// newCompleter creates a readline.AutoCompleter for command completion
132-
func (c *CLIInterface) newCompleter() readline.AutoCompleter {
133-
configCompleter := readline.PcItem("/config",
134-
readline.PcItem("set",
135-
readline.PcItemDynamic(func(_ string) []string {
136-
// Only return the allowed keys
137-
return AllowedConfigKeys
138-
}),
139-
),
140-
readline.PcItem("get",
141-
readline.PcItemDynamic(func(_ string) []string {
142-
// Only return the allowed keys
143-
return AllowedConfigKeys
144-
}),
145-
),
146-
)
147-
148-
// Create completers for each base command using the global subCommands variable
149-
completers := make([]readline.PrefixCompleterInterface, 0, len(commands))
150-
for _, cmd := range commands {
151-
// Special handling for config to add nested completion
152-
if cmd == "/config" {
153-
completers = append(completers, configCompleter)
154-
} else {
155-
completers = append(completers, readline.PcItem(cmd))
156-
}
161+
// newCompleter creates a completion handler for command completion
162+
func (c *CLIInterface) newCompleter() *completion.CmdCompletionOrList2 {
163+
return &completion.CmdCompletionOrList2{
164+
Delimiter: " ",
165+
Postfix: " ",
166+
Candidates: func(field []string) (forComp []string, forList []string) {
167+
// Handle top-level commands
168+
if len(field) == 0 || (len(field) == 1 && !strings.HasSuffix(field[0], " ")) {
169+
return commands, commands
170+
}
171+
172+
// Handle /config subcommands
173+
if len(field) > 0 && field[0] == "/config" {
174+
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
175+
return []string{"set", "get"}, []string{"set", "get"}
176+
} else if len(field) == 2 || (len(field) == 3 && !strings.HasSuffix(field[2], " ")) {
177+
return AllowedConfigKeys, AllowedConfigKeys
178+
}
179+
}
180+
return nil, nil
181+
},
157182
}
158-
159-
return readline.NewPrefixCompleter(completers...)
160183
}

0 commit comments

Comments
 (0)