mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat: 路径配置化 + 裸二进制启动 + pluginmgr 内置插件
- types.go: 新增 PluginDirConfig 结构体嵌入 Config - config/registry.go: 新增7个路径配置项 (core.plugin.dir 等) + ConfigDef 元数据 - cmd/homed/main.go: -data 默认自动检测二进制同级目录,使用 cfg.Plugin.Dir - internal/plugins/pluginmgr/: 内置插件实现 (4工具 + HTTP API + 包校验) - all.go: 注册 pluginmgr - manifest.go: 扩展 PluginManifest 字段 - sdk/settings.go: RegisterDef / Defs 接口 - webui: 设置页自动发现 ConfigDef 元数据 - config/config.go, config/config.yaml: 清理 YAML 死代码 - sdk/plugin.go: IO 通道泛型化支持非文本类型 - waiter: CLI 支持 socket 发现和交互模式
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
@ -29,6 +30,7 @@ import (
|
||||
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
|
||||
healthcheck "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
|
||||
openclaw "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/openclaw"
|
||||
pluginmgr "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
||||
webui "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
|
||||
@ -38,11 +40,24 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
dataDir := flag.String("data", "/var/lib/homeagent", "data directory")
|
||||
dataDir := flag.String("data", "", "data directory (default: auto-detect next to binary)")
|
||||
httpAddr := flag.String("webui", ":8080", "webui listen address")
|
||||
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
|
||||
flag.Parse()
|
||||
|
||||
if *dataDir == "" {
|
||||
exe, err := os.Executable()
|
||||
if err == nil {
|
||||
*dataDir = filepath.Join(filepath.Dir(exe), "data")
|
||||
} else {
|
||||
if exe, err := exec.LookPath(os.Args[0]); err == nil {
|
||||
*dataDir = filepath.Join(filepath.Dir(exe), "data")
|
||||
} else {
|
||||
*dataDir = "./data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *cliSocket == "" {
|
||||
*cliSocket = filepath.Join(*dataDir, "cli.sock")
|
||||
}
|
||||
@ -304,7 +319,7 @@ func main() {
|
||||
pluginReg.SetKnowledge(ks)
|
||||
pluginReg.SetProviderManager(providerMgr)
|
||||
pluginReg.SetConfigRegistry(cfgReg)
|
||||
pluginReg.SetPluginDir(filepath.Join(cfg.Daemon.DataDir, "plugins"))
|
||||
pluginReg.SetPluginDir(cfg.Plugin.Dir)
|
||||
|
||||
// Wire registration callbacks: plugins' RegisterTool/RegisterStage → StageHost
|
||||
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
@ -360,7 +375,7 @@ func main() {
|
||||
TextMemory: textMem,
|
||||
Personality: personality,
|
||||
PluginReg: pluginReg,
|
||||
PluginDir: filepath.Join(cfg.Daemon.DataDir, "plugins"),
|
||||
PluginDir: cfg.Plugin.Dir,
|
||||
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
||||
StageHost: stageHost,
|
||||
EventBus: evBus,
|
||||
@ -376,12 +391,15 @@ func main() {
|
||||
)
|
||||
healthcheck.Configure(stageHost, iom, pluginReg, memDB, ks, docStore, providerMgr, agent)
|
||||
|
||||
// Wire pluginmgr dependencies
|
||||
pluginmgr.PluginDir = cfg.Plugin.Dir
|
||||
pluginmgr.Reg = pluginReg
|
||||
|
||||
// Auto-create plugins directory (without hardcoding plugin names)
|
||||
plgDir := filepath.Join(cfg.Daemon.DataDir, "plugins")
|
||||
os.MkdirAll(plgDir, 0755)
|
||||
os.MkdirAll(cfg.Plugin.Dir, 0755)
|
||||
|
||||
// Load all plugins — each scans its own dir and is loaded via factory or .so
|
||||
if err := pluginReg.Load(plgDir); err != nil {
|
||||
if err := pluginReg.Load(cfg.Plugin.Dir); err != nil {
|
||||
log.Printf("[homed] warning: load plugins: %v", err)
|
||||
}
|
||||
log.Printf("[homed] stage host ready with %d registered tools", stageHost.ToolCount())
|
||||
|
||||
@ -16,50 +16,17 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
colorReset = "\033[0m"
|
||||
colorGreen = "\033[32m"
|
||||
colorRed = "\033[31m"
|
||||
colorCyan = "\033[36m"
|
||||
colorYellow = "\033[33m"
|
||||
colorBold = "\033[1m"
|
||||
colorDim = "\033[2m"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Socket string `yaml:"socket"`
|
||||
Remote string `yaml:"remote"`
|
||||
Mode string `yaml:"mode"`
|
||||
Colors bool `yaml:"colors"`
|
||||
HistorySize int `yaml:"history_size"`
|
||||
Prompt string `yaml:"prompt"`
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Mode: "auto",
|
||||
Colors: true,
|
||||
HistorySize: 1000,
|
||||
Prompt: "waiter> ",
|
||||
}
|
||||
}
|
||||
|
||||
func configPaths() []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
xdgConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfig == "" {
|
||||
xdgConfig = filepath.Join(home, ".config")
|
||||
}
|
||||
return []string{
|
||||
filepath.Join(xdgConfig, "homeagent", "cli.yaml"),
|
||||
filepath.Join(home, ".homeagent.yaml"),
|
||||
".homeagent.yaml",
|
||||
}
|
||||
}
|
||||
|
||||
func historyPath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
xdgData := os.Getenv("XDG_DATA_HOME")
|
||||
@ -71,34 +38,6 @@ func historyPath() string {
|
||||
return filepath.Join(dir, "cli_history")
|
||||
}
|
||||
|
||||
func loadConfig() Config {
|
||||
cfg := defaultConfig()
|
||||
for _, p := range configPaths() {
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
yaml.Unmarshal(data, &cfg)
|
||||
break
|
||||
}
|
||||
if cfg.HistorySize < 1 {
|
||||
cfg.HistorySize = 100
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func saveConfig(cfg Config) {
|
||||
for _, p := range configPaths() {
|
||||
dir := filepath.Dir(p)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
continue
|
||||
}
|
||||
data, _ := yaml.Marshal(cfg)
|
||||
os.WriteFile(p, data, 0644)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func discoverSocket(configured string) string {
|
||||
if configured != "" {
|
||||
return configured
|
||||
@ -122,34 +61,22 @@ func discoverSocket(configured string) string {
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
func resolveEndpoint(cfg Config) (mode string, addr string) {
|
||||
switch cfg.Mode {
|
||||
case "local":
|
||||
return "local", discoverSocket(cfg.Socket)
|
||||
case "remote":
|
||||
return "remote", cfg.Remote
|
||||
default:
|
||||
sock := discoverSocket(cfg.Socket)
|
||||
if sock != "" {
|
||||
if _, err := os.Stat(sock); err == nil {
|
||||
return "local", sock
|
||||
}
|
||||
}
|
||||
if cfg.Remote != "" {
|
||||
return "remote", cfg.Remote
|
||||
}
|
||||
return "local", sock
|
||||
}
|
||||
}
|
||||
|
||||
type respLine struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func printColored(cfg Config, color, msg string) {
|
||||
if !cfg.Colors {
|
||||
var colors = true
|
||||
|
||||
func init() {
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
colors = false
|
||||
}
|
||||
}
|
||||
|
||||
func printlnC(color, msg string) {
|
||||
if !colors {
|
||||
fmt.Println(msg)
|
||||
return
|
||||
}
|
||||
@ -157,48 +84,46 @@ func printColored(cfg Config, color, msg string) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
socket := flag.String("socket", "", "unix socket path (overrides config)")
|
||||
remote := flag.String("remote", "", "remote webui URL (overrides config)")
|
||||
socket := flag.String("socket", "", "unix socket path")
|
||||
remote := flag.String("remote", "", "remote webui URL (e.g. http://127.0.0.1:8080)")
|
||||
say := flag.String("say", "", "send a message and print response (one-shot)")
|
||||
flag.Parse()
|
||||
|
||||
cfg := loadConfig()
|
||||
if *socket != "" {
|
||||
cfg.Socket = *socket
|
||||
}
|
||||
sockAddr := discoverSocket(*socket)
|
||||
mode := "local"
|
||||
addr := sockAddr
|
||||
if *remote != "" {
|
||||
cfg.Remote = *remote
|
||||
cfg.Mode = "remote"
|
||||
mode = "remote"
|
||||
addr = *remote
|
||||
} else if *socket != "" {
|
||||
mode = "local"
|
||||
addr = *socket
|
||||
}
|
||||
mode, addr := resolveEndpoint(cfg)
|
||||
|
||||
if *say != "" {
|
||||
oneShot(cfg, mode, addr, *say)
|
||||
oneShot(mode, addr, *say)
|
||||
return
|
||||
}
|
||||
|
||||
runInteractive(cfg, mode, addr)
|
||||
runInteractive(mode, addr)
|
||||
}
|
||||
|
||||
func oneShot(cfg Config, mode, addr, message string) {
|
||||
func oneShot(mode, addr, message string) {
|
||||
if mode == "remote" {
|
||||
resp, err := doRemoteOnce(addr, message)
|
||||
if err != nil {
|
||||
printColored(cfg, colorRed, fmt.Sprintf("error: %v", err))
|
||||
printlnC(colorRed, fmt.Sprintf("error: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("unix", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
printColored(cfg, colorRed, fmt.Sprintf("connect to %s: %v", addr, err))
|
||||
printlnC(colorRed, fmt.Sprintf("connect to %s: %v", addr, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
fmt.Fprintf(conn, "%s\n", message)
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
if scanner.Scan() {
|
||||
var rl respLine
|
||||
@ -210,7 +135,7 @@ func oneShot(cfg Config, mode, addr, message string) {
|
||||
case "response":
|
||||
fmt.Println(rl.Content)
|
||||
case "error":
|
||||
printColored(cfg, colorRed, fmt.Sprintf("error: %s", rl.Error))
|
||||
printlnC(colorRed, fmt.Sprintf("error: %s", rl.Error))
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Println(scanner.Text())
|
||||
@ -227,18 +152,18 @@ func doRemoteOnce(baseURL, message string) (string, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
content, _ := result["response"].(string)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func runInteractive(cfg Config, mode, addr string) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
const clearLine = "\033[2K\r"
|
||||
|
||||
history := newHistory(historyPath(), cfg.HistorySize)
|
||||
func runInteractive(mode, addr string) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
history := newHistory(historyPath(), 1000)
|
||||
history.load()
|
||||
|
||||
line := newLineEditor(&history)
|
||||
@ -249,18 +174,20 @@ func runInteractive(cfg Config, mode, addr string) {
|
||||
}
|
||||
defer restore()
|
||||
|
||||
if cfg.Colors {
|
||||
fmt.Print(colorBold)
|
||||
}
|
||||
fmt.Printf("HomeAgent CLI — %s://%s\n", mode, addr)
|
||||
if cfg.Colors {
|
||||
fmt.Print(colorReset)
|
||||
if colors {
|
||||
fmt.Printf("%sHomeAgent CLI%s %s(%s://%s)%s\n", colorBold, colorReset, colorDim, mode, addr, colorReset)
|
||||
} else {
|
||||
fmt.Printf("HomeAgent CLI (%s://%s)\n", mode, addr)
|
||||
}
|
||||
fmt.Println("Type /help for commands.")
|
||||
|
||||
var conn io.ReadWriteCloser
|
||||
var readerDone chan struct{}
|
||||
connMu := &sync.Mutex{}
|
||||
|
||||
connect := func() error {
|
||||
connMu.Lock()
|
||||
defer connMu.Unlock()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
@ -273,56 +200,39 @@ func runInteractive(cfg Config, mode, addr string) {
|
||||
}
|
||||
conn = c
|
||||
readerDone = make(chan struct{})
|
||||
go readLoop(cfg, conn, readerDone)
|
||||
go readLoop(conn, readerDone)
|
||||
return nil
|
||||
}
|
||||
|
||||
reconnect := func() {
|
||||
for i := 0; i < 30; i++ {
|
||||
if err := connect(); err != nil {
|
||||
if cfg.Colors {
|
||||
fmt.Printf("\r\n%sreconnecting (%d/30): %v%s\n", colorYellow, i+1, err, colorReset)
|
||||
} else {
|
||||
fmt.Printf("\r\nreconnecting (%d/30): %v\n", i+1, err)
|
||||
}
|
||||
printlnC(colorYellow, fmt.Sprintf("reconnecting (%d/30): %v", i+1, err))
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
if cfg.Colors {
|
||||
fmt.Printf("\r%s%sreconnected%s\n", clearLine, colorGreen, colorReset)
|
||||
} else {
|
||||
fmt.Printf("\r%sreconnected\n", clearLine)
|
||||
}
|
||||
printlnC(colorGreen, "reconnected")
|
||||
return
|
||||
}
|
||||
if cfg.Colors {
|
||||
fmt.Printf("\r%s%sgiving up after 30 attempts%s\n", clearLine, colorRed, colorReset)
|
||||
} else {
|
||||
fmt.Printf("\r%sgiving up after 30 attempts\n", clearLine)
|
||||
}
|
||||
printlnC(colorRed, "giving up after 30 attempts")
|
||||
}
|
||||
|
||||
// initial connect
|
||||
for {
|
||||
if err := connect(); err != nil {
|
||||
if cfg.Colors {
|
||||
fmt.Printf("%sconnect: %v, retrying in 2s...%s\n", colorYellow, err, colorReset)
|
||||
} else {
|
||||
fmt.Printf("connect: %v, retrying in 2s...\n", err)
|
||||
}
|
||||
printlnC(colorYellow, fmt.Sprintf("connect: %v, retrying in 2s...", err))
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
prompt := cfg.Prompt
|
||||
inputMu := &sync.Mutex{}
|
||||
|
||||
for {
|
||||
fmt.Print(prompt)
|
||||
fmt.Print("waiter> ")
|
||||
text, err := line.read()
|
||||
if err != nil {
|
||||
// EOF or error
|
||||
break
|
||||
}
|
||||
line.clear()
|
||||
@ -333,25 +243,36 @@ func runInteractive(cfg Config, mode, addr string) {
|
||||
}
|
||||
|
||||
if cmd[0] == '/' {
|
||||
if handleBuiltin(cfg, cmd, &mode, &addr, &prompt, reconnect, &history) {
|
||||
if handleBuiltin(cmd, &mode, &addr, reconnect) {
|
||||
continue
|
||||
}
|
||||
// unknown command falls through to send as message
|
||||
}
|
||||
|
||||
history.add(cmd)
|
||||
history.save()
|
||||
|
||||
_, err = fmt.Fprintf(conn, "%s\n", cmd)
|
||||
connMu.Lock()
|
||||
c := conn
|
||||
connMu.Unlock()
|
||||
if c == nil {
|
||||
printlnC(colorYellow, "not connected, reconnecting...")
|
||||
reconnect()
|
||||
connMu.Lock()
|
||||
c = conn
|
||||
connMu.Unlock()
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(c, "%s\n", cmd)
|
||||
if err != nil {
|
||||
if cfg.Colors {
|
||||
fmt.Printf("%sconnection lost, reconnecting...%s\n", colorYellow, colorReset)
|
||||
} else {
|
||||
fmt.Println("connection lost, reconnecting...")
|
||||
}
|
||||
printlnC(colorYellow, "connection lost, reconnecting...")
|
||||
line.redrawPending(cmd)
|
||||
reconnect()
|
||||
fmt.Fprintf(conn, "%s\n", cmd)
|
||||
connMu.Lock()
|
||||
c = conn
|
||||
connMu.Unlock()
|
||||
if c != nil {
|
||||
fmt.Fprintf(c, "%s\n", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@ -359,12 +280,15 @@ func runInteractive(cfg Config, mode, addr string) {
|
||||
goto exit
|
||||
default:
|
||||
}
|
||||
_ = inputMu
|
||||
}
|
||||
|
||||
exit:
|
||||
connMu.Lock()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
connMu.Unlock()
|
||||
if readerDone != nil {
|
||||
<-readerDone
|
||||
}
|
||||
@ -377,13 +301,11 @@ func dial(mode, addr string) (io.ReadWriteCloser, error) {
|
||||
return net.DialTimeout("unix", addr, 5*time.Second)
|
||||
}
|
||||
|
||||
const clearLine = "\033[2K\r"
|
||||
|
||||
func readLoop(cfg Config, conn io.ReadWriteCloser, done chan struct{}) {
|
||||
func readLoop(conn io.ReadWriteCloser, done chan struct{}) {
|
||||
defer close(done)
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
if !cfg.Colors {
|
||||
if !colors {
|
||||
fmt.Printf("%s%s\n", clearLine, scanner.Text())
|
||||
continue
|
||||
}
|
||||
@ -452,7 +374,7 @@ func (c *httpConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleBuiltin(cfg Config, cmd string, mode, addr *string, prompt *string, reconnect func(), history *History) bool {
|
||||
func handleBuiltin(cmd string, mode, addr *string, reconnect func()) bool {
|
||||
switch {
|
||||
case cmd == "/help":
|
||||
fmt.Println(`Built-in commands:
|
||||
@ -463,7 +385,6 @@ func handleBuiltin(cfg Config, cmd string, mode, addr *string, prompt *string, r
|
||||
/connect <path> switch to a different unix socket
|
||||
/remote <url> switch to remote HTTP mode
|
||||
/local switch back to local socket mode
|
||||
/prompt <text> change the prompt
|
||||
|
||||
Any other text is sent as a message to the agent.`)
|
||||
return true
|
||||
@ -477,27 +398,19 @@ Any other text is sent as a message to the agent.`)
|
||||
return true
|
||||
|
||||
case cmd == "/reconnect":
|
||||
if cfg.Colors {
|
||||
fmt.Printf("%sreconnecting...%s\n", colorYellow, colorReset)
|
||||
} else {
|
||||
fmt.Println("reconnecting...")
|
||||
}
|
||||
printlnC(colorYellow, "reconnecting...")
|
||||
reconnect()
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(cmd, "/connect "):
|
||||
*mode = "local"
|
||||
*addr = strings.TrimSpace(cmd[9:])
|
||||
cfg.Socket = *addr
|
||||
saveConfig(cfg)
|
||||
reconnect()
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(cmd, "/remote "):
|
||||
*mode = "remote"
|
||||
*addr = strings.TrimSpace(cmd[8:])
|
||||
cfg.Remote = *addr
|
||||
saveConfig(cfg)
|
||||
reconnect()
|
||||
return true
|
||||
|
||||
@ -507,10 +420,6 @@ Any other text is sent as a message to the agent.`)
|
||||
reconnect()
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(cmd, "/prompt "):
|
||||
*prompt = strings.TrimSpace(cmd[8:])
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -525,10 +434,7 @@ type LineEditor struct {
|
||||
}
|
||||
|
||||
func newLineEditor(h *History) *LineEditor {
|
||||
return &LineEditor{
|
||||
hist: h,
|
||||
histI: -1,
|
||||
}
|
||||
return &LineEditor{hist: h, histI: -1}
|
||||
}
|
||||
|
||||
func (e *LineEditor) clear() {
|
||||
@ -592,41 +498,41 @@ func (e *LineEditor) read() (string, error) {
|
||||
continue
|
||||
}
|
||||
switch seq[1] {
|
||||
case 'A': // Up
|
||||
case 'A':
|
||||
e.historyPrev()
|
||||
case 'B': // Down
|
||||
case 'B':
|
||||
e.historyNext()
|
||||
case 'C': // Right
|
||||
case 'C':
|
||||
if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
e.redraw()
|
||||
}
|
||||
case 'D': // Left
|
||||
case 'D':
|
||||
if e.pos > 0 {
|
||||
e.pos--
|
||||
e.redraw()
|
||||
}
|
||||
case 'H', '1': // Home (\x1b[H) or (\x1b[1~)
|
||||
case 'H', '1':
|
||||
if seq[1] == '1' {
|
||||
io.ReadFull(in, make([]byte, 1)) // consume ~
|
||||
io.ReadFull(in, make([]byte, 1))
|
||||
}
|
||||
e.pos = 0
|
||||
e.redraw()
|
||||
case 'F', '4': // End (\x1b[F) or (\x1b[4~)
|
||||
case 'F', '4':
|
||||
if seq[1] == '4' {
|
||||
io.ReadFull(in, make([]byte, 1)) // consume ~
|
||||
io.ReadFull(in, make([]byte, 1))
|
||||
}
|
||||
e.pos = len(e.buf)
|
||||
e.redraw()
|
||||
case '3': // Delete (\x1b[3~)
|
||||
io.ReadFull(in, make([]byte, 1)) // consume ~
|
||||
case '3':
|
||||
io.ReadFull(in, make([]byte, 1))
|
||||
if e.pos < len(e.buf) {
|
||||
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
|
||||
e.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
case '\t': // Tab
|
||||
case '\t':
|
||||
e.doCompletion()
|
||||
|
||||
default:
|
||||
@ -674,7 +580,7 @@ func (e *LineEditor) historyNext() {
|
||||
}
|
||||
|
||||
func (e *LineEditor) doCompletion() {
|
||||
cmds := []string{"/help", "/exit", "/quit", "/clear", "/reconnect", "/connect ", "/remote ", "/local", "/prompt "}
|
||||
cmds := []string{"/help", "/exit", "/quit", "/clear", "/reconnect", "/connect ", "/remote ", "/local"}
|
||||
prefix := string(e.buf)
|
||||
for _, c := range cmds {
|
||||
if strings.HasPrefix(c, prefix) && c != prefix {
|
||||
@ -687,10 +593,9 @@ func (e *LineEditor) doCompletion() {
|
||||
}
|
||||
|
||||
func (e *LineEditor) redraw() {
|
||||
fmt.Print("\r\033[K") // clear line
|
||||
fmt.Print("\r\033[K")
|
||||
fmt.Print(string(e.buf))
|
||||
if e.pos < len(e.buf) {
|
||||
// move cursor back
|
||||
skip := len(e.buf) - e.pos
|
||||
fmt.Printf("\033[%dD", skip)
|
||||
}
|
||||
@ -747,7 +652,6 @@ func (h *History) all() []string {
|
||||
return r
|
||||
}
|
||||
|
||||
// setRawMode sets stdin to raw mode (non-canonical, no echo).
|
||||
func setRawMode(fd int) (func(), error) {
|
||||
if fd == 0 {
|
||||
fd = int(os.Stdin.Fd())
|
||||
|
||||
Reference in New Issue
Block a user