implement simple command completions

This commit is contained in:
sawka
2022-08-10 18:33:32 -07:00
parent a175d236c1
commit 81c564aca9
6 changed files with 243 additions and 58 deletions
+1
View File
@@ -18,6 +18,7 @@ require (
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
)
+1
View File
@@ -787,6 +787,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+200 -50
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -55,6 +57,44 @@ func SubMetaCmd(cmd string) string {
}
}
var ValidCommands = []string{
"/run",
"/eval",
"/screen", "/screen:open", "/screen:close",
"/session", "/session:open", "/session:close",
"/comment",
"/cd",
"/compgen",
}
func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
switch SubMetaCmd(pk.MetaCmd) {
case "run":
return RunCommand(ctx, pk)
case "eval":
return EvalCommand(ctx, pk)
case "screen":
return ScreenCommand(ctx, pk)
case "session":
return SessionCommand(ctx, pk)
case "comment":
return CommentCommand(ctx, pk)
case "cd":
return CdCommand(ctx, pk)
case "compgen":
return CompGenCommand(ctx, pk)
default:
return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd)
}
}
func firstArg(pk *scpacket.FeCommandPacketType) string {
if len(pk.Args) == 0 {
return ""
@@ -212,34 +252,6 @@ func resolveIds(ctx context.Context, pk *scpacket.FeCommandPacketType, rtype int
return rtn, nil
}
func HandleCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
switch SubMetaCmd(pk.MetaCmd) {
case "run":
return RunCommand(ctx, pk)
case "eval":
return EvalCommand(ctx, pk)
case "screen":
return ScreenCommand(ctx, pk)
case "session":
return SessionCommand(ctx, pk)
case "comment":
return CommentCommand(ctx, pk)
case "cd":
return CdCommand(ctx, pk)
case "compgen":
return CompGenCommand(ctx, pk)
default:
return nil, fmt.Errorf("invalid command '/%s', no handler", pk.MetaCmd)
}
}
func RunCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote)
if err != nil {
@@ -300,18 +312,24 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
if metaCmd == "" {
metaCmd = "run"
}
var args []string
if metaCmd == "run" || metaCmd == "comment" {
args = []string{commandStr}
} else {
args = strings.Fields(commandStr)
}
newPk := &scpacket.FeCommandPacketType{
MetaCmd: metaCmd,
MetaSubCmd: metaSubCmd,
Args: args,
Kwargs: pk.Kwargs,
}
if metaCmd == "run" || metaCmd == "comment" {
newPk.Args = []string{commandStr}
} else {
allArgs := strings.Fields(commandStr)
for _, arg := range allArgs {
if strings.Index(arg, "=") == -1 {
newPk.Args = append(newPk.Args, arg)
continue
}
fields := strings.SplitN(arg, "=", 2)
newPk.Kwargs[fields[0]] = fields[1]
}
}
return HandleCommand(ctx, newPk)
}
@@ -388,10 +406,14 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up
if curRemote == nil {
return nil, fmt.Errorf("invalid remote, cannot execute command")
}
_, err = curRemote.PacketRpc(ctx, cdPacket)
resp, err := curRemote.PacketRpc(ctx, cdPacket)
if err != nil {
return nil, err
}
if err = resp.Err(); err != nil {
return nil, err
}
fmt.Printf("cd-resp %#v\n", resp)
remote, err := sstore.UpdateRemoteCwd(ctx, ids.RemoteName, ids.SessionId, ids.WindowId, ids.RemoteId, newDir)
if err != nil {
return nil, err
@@ -402,6 +424,10 @@ func CdCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.Up
WindowId: ids.WindowId,
Remotes: []*sstore.RemoteInstance{remote},
},
Info: &sstore.InfoMsgType{
InfoMsg: fmt.Sprintf("[%s] current directory = %s", ids.RemoteName, newDir),
TimeoutMs: 2000,
},
}
return update, nil
}
@@ -431,38 +457,162 @@ func getStrArr(v interface{}, field string) []string {
return sarr
}
func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote)
if err != nil {
return nil, fmt.Errorf("/compgen error: %w", err)
func getBool(v interface{}, field string) bool {
if v == nil {
return false
}
m, ok := v.(map[string]interface{})
if !ok {
return false
}
fieldVal := m[field]
if fieldVal == nil {
return false
}
bval, ok := fieldVal.(bool)
if !ok {
return false
}
return bval
}
func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket {
sort.Strings(comps)
update := sstore.InfoUpdate{
Info: &sstore.InfoMsgType{
InfoTitle: fmt.Sprintf("%s completions", compType),
InfoStrings: comps,
InfoStringsMore: hasMore,
},
}
return update
}
func makeInsertUpdateFromComps(pos int64, prefix string, comps []string, hasMore bool) sstore.UpdatePacket {
if hasMore {
return nil
}
lcp := longestPrefix(prefix, comps)
if lcp == prefix || len(lcp) < len(prefix) || !strings.HasPrefix(lcp, prefix) {
return nil
}
insertChars := lcp[len(prefix):]
clu := &sstore.CmdLineType{InsertChars: insertChars, InsertPos: pos}
return sstore.InfoUpdate{CmdLine: clu}
}
func longestPrefix(root string, comps []string) string {
if len(comps) == 0 {
return root
}
if len(comps) == 1 {
comp := comps[0]
if len(comp) >= len(root) && strings.HasPrefix(comp, root) {
return comps[0] + " "
}
}
lcp := comps[0]
for i := 1; i < len(comps); i++ {
s := comps[i]
for j := 0; j < len(lcp); j++ {
if j >= len(s) || lcp[j] != s[j] {
lcp = lcp[0:j]
break
}
}
}
if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) {
return root
}
return lcp
}
var wsRe = regexp.MustCompile("\\s+")
func doMetaCompGen(ctx context.Context, ids resolvedIds, prefix string) ([]string, bool, error) {
var comps []string
for _, cmd := range ValidCommands {
if strings.HasPrefix(cmd, prefix) {
comps = append(comps, cmd)
}
}
return comps, false, nil
}
func doCompGen(ctx context.Context, ids resolvedIds, prefix string, compType string) ([]string, bool, error) {
if compType == "metacommand" {
return doMetaCompGen(ctx, ids, prefix)
}
compType := argN(pk, 0)
prefix := argN(pk, 1)
if !packet.IsValidCompGenType(compType) {
return nil, fmt.Errorf("/compgen invalid type '%s'", compType)
return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType)
}
cgPacket := packet.MakeCompGenPacket()
cgPacket.ReqId = uuid.New().String()
cgPacket.CompType = compType
cgPacket.Prefix = prefix
if ids.RemoteState == nil {
return nil, fmt.Errorf("/compgen invalid remote state")
return nil, false, fmt.Errorf("/compgen invalid remote state")
}
cgPacket.Cwd = ids.RemoteState.Cwd
curRemote := remote.GetRemoteById(ids.RemoteId)
if curRemote == nil {
return nil, fmt.Errorf("invalid remote, cannot execute command")
return nil, false, fmt.Errorf("invalid remote, cannot execute command")
}
resp, err := curRemote.PacketRpc(ctx, cgPacket)
if err != nil {
return nil, err
return nil, false, err
}
if err = resp.Err(); err != nil {
return nil, false, err
}
comps := getStrArr(resp.Data, "comps")
update := sstore.InfoUpdate{
InfoTitle: fmt.Sprintf("%s completions", compType),
InfoStrings: comps,
hasMore := getBool(resp.Data, "hasmore")
return comps, hasMore, nil
}
func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
ids, err := resolveIds(ctx, pk, R_Session|R_Window|R_Remote)
if err != nil {
return nil, fmt.Errorf("/compgen error: %w", err)
}
return update, nil
cmdLine := firstArg(pk)
pos := len(cmdLine)
if pk.Kwargs["comppos"] != "" {
posArg, err := strconv.Atoi(pk.Kwargs["comppos"])
if err != nil {
return nil, fmt.Errorf("/compgen invalid comppos '%s': %w", pk.Kwargs["comppos"], err)
}
pos = posArg
}
if pos < 0 {
pos = 0
}
if pos > len(cmdLine) {
pos = len(cmdLine)
}
showComps := resolveBool(pk.Kwargs["compshow"], false)
prefix := cmdLine[:pos]
parts := strings.Split(prefix, " ")
compType := "file"
if len(parts) > 0 && strings.HasPrefix(parts[0], "/") {
compType = "metacommand"
} else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") {
compType = "directory"
} else if len(parts) <= 1 {
compType = "command"
}
lastPart := ""
if len(parts) > 0 {
lastPart = parts[len(parts)-1]
}
comps, hasMore, err := doCompGen(ctx, ids, lastPart, compType)
if err != nil {
return nil, err
}
if showComps {
return makeInfoFromComps(compType, comps, hasMore), nil
}
return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil
}
func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
+13
View File
@@ -103,7 +103,9 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) {
err := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT * FROM session`
tx.SelectWrap(&rtn, query)
sessionMap := make(map[string]*SessionType)
for _, session := range rtn {
sessionMap[session.SessionId] = session
session.Full = true
}
var screens []*ScreenType
@@ -132,6 +134,15 @@ func GetAllSessions(ctx context.Context) ([]*SessionType, error) {
}
screen.Windows = append(screen.Windows, sw)
}
query = `SELECT * FROM remote_instance WHERE sessionscope`
var ris []*RemoteInstance
tx.SelectWrap(&ris, query)
for _, ri := range ris {
s := sessionMap[ri.SessionId]
if s != nil {
s.Remotes = append(s.Remotes, ri)
}
}
return nil
})
return rtn, err
@@ -154,6 +165,8 @@ func GetWindowById(ctx context.Context, sessionId string, windowId string) (*Win
for _, m := range cmdMaps {
window.Cmds = append(window.Cmds, CmdFromMap(m))
}
query = `SELECT * FROM remote_instance WHERE sessionid = ? AND windowid = ? AND NOT sessionscope`
tx.SelectWrap(&window.Remotes, query, sessionId, windowId)
return nil
})
return rtnWindow, err
+4 -1
View File
@@ -194,9 +194,12 @@ type RemoteInstance struct {
Name string `json:"name"`
SessionId string `json:"sessionid"`
WindowId string `json:"windowid"`
RemoteId string `json"remoteid"`
RemoteId string `json:"remoteid"`
SessionScope bool `json:"sessionscope"`
State RemoteState `json:"state"`
// only for updates
Remove bool `json:"remove,omitempty"`
}
type LineType struct {
+24 -7
View File
@@ -10,6 +10,7 @@ const WindowUpdateStr = "window"
const CmdUpdateStr = "cmd"
const LineCmdUpdateStr = "line+cmd"
const InfoUpdateStr = "info"
const CompGenUpdateStr = "compgen"
type UpdatePacket interface {
UpdateType() string
@@ -33,8 +34,8 @@ func (PtyDataUpdate) UpdateType() string {
}
type WindowUpdate struct {
Window WindowType `json:"window"`
Remove bool `json:"remove,omitempty"`
Window WindowType `json:"window"`
Info *InfoMsgType `json:"info,omitempty"`
}
func (WindowUpdate) UpdateType() string {
@@ -44,6 +45,7 @@ func (WindowUpdate) UpdateType() string {
type SessionUpdate struct {
Sessions []*SessionType `json:"sessions"`
ActiveSessionId string `json:"activesessionid,omitempty"`
Info *InfoMsgType `json:"info,omitempty"`
}
func (SessionUpdate) UpdateType() string {
@@ -62,18 +64,33 @@ func MakeSingleSessionUpdate(sessionId string) (*SessionUpdate, *SessionType) {
}
type LineUpdate struct {
Line *LineType `json:"line"`
Cmd *CmdType `json:"cmd,omitempty"`
Remove bool `json:"remove,omitempty"`
Line *LineType `json:"line"`
Cmd *CmdType `json:"cmd,omitempty"`
Remove bool `json:"remove,omitempty"`
Info *InfoMsgType `json:"info,omitempty"`
}
func (LineUpdate) UpdateType() string {
return LineCmdUpdateStr
}
type InfoMsgType struct {
InfoTitle string `json:"infotitle"`
InfoError string `json:"infoerror,omitempty"`
InfoMsg string `json:"infomsg,omitempty"`
InfoStrings []string `json:"infostrings"`
InfoStringsMore bool `json:"infostringsmore"`
TimeoutMs int64 `json:"timeoutms,omitempty"`
}
type CmdLineType struct {
InsertChars string `json:"insertchars"`
InsertPos int64 `json:"insertpos"`
}
type InfoUpdate struct {
InfoTitle string `json:"infotitle"`
InfoStrings []string `json:"infostrings"`
Info *InfoMsgType `json:"info,omitempty"`
CmdLine *CmdLineType `json:"cmdline,omitempty"`
}
func (InfoUpdate) UpdateType() string {