diff --git a/backend/services/cli_service.go b/backend/services/cli_service.go new file mode 100644 index 0000000..e419f39 --- /dev/null +++ b/backend/services/cli_service.go @@ -0,0 +1,160 @@ +package services + +import ( + "context" + "fmt" + "github.com/redis/go-redis/v9" + "github.com/wailsapp/wails/v2/pkg/runtime" + "strings" + "sync" + "tinyrdm/backend/types" + sliceutil "tinyrdm/backend/utils/slice" + strutil "tinyrdm/backend/utils/string" +) + +type cliService struct { + ctx context.Context + ctxCancel context.CancelFunc + mutex sync.Mutex + clients map[string]redis.UniversalClient + selectedDB map[string]int +} + +type cliOutput struct { + Content string `json:"content"` // output content + Prompt string `json:"prompt,omitempty"` // new line prompt, empty if not ready to input +} + +var cli *cliService +var onceCli sync.Once + +func Cli() *cliService { + if cli == nil { + onceCli.Do(func() { + cli = &cliService{ + clients: map[string]redis.UniversalClient{}, + selectedDB: map[string]int{}, + } + }) + } + return cli +} + +func (c *cliService) runCommand(server, data string) { + if cmds := strings.Split(data, " "); len(cmds) > 0 && len(cmds[0]) > 0 { + if client, err := c.getRedisClient(server); err == nil { + args := sliceutil.Map(cmds, func(i int) any { + return cmds[i] + }) + if result, err := client.Do(c.ctx, args...).Result(); err == nil || err == redis.Nil { + if strings.ToLower(cmds[0]) == "select" { + // switch database + if db, ok := strutil.AnyToInt(cmds[1]); ok { + c.selectedDB[server] = db + } + } + + c.echo(server, strutil.AnyToString(result), true) + } else { + c.echoError(server, err.Error()) + } + return + } + } + + c.echoReady(server) +} + +func (c *cliService) echo(server, data string, newLineReady bool) { + output := cliOutput{ + Content: data, + } + if newLineReady { + output.Prompt = fmt.Sprintf("%s:db%d> ", server, c.selectedDB[server]) + } + runtime.EventsEmit(c.ctx, "cmd:output:"+server, output) +} + +func (c *cliService) echoReady(server string) { + c.echo(server, "", true) +} + +func (c *cliService) echoError(server, data string) { + c.echo(server, "\x1b[31m"+data+"\x1b[0m", true) +} + +func (c *cliService) getRedisClient(server string) (redis.UniversalClient, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + client, ok := c.clients[server] + if !ok { + var err error + conf := Connection().getConnection(server) + if conf == nil { + return nil, fmt.Errorf("no connection profile named: %s", server) + } + if client, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil { + return nil, err + } + c.clients[server] = client + } + return client, nil +} + +func (c *cliService) Start(ctx context.Context) { + c.ctx, c.ctxCancel = context.WithCancel(ctx) +} + +// StartCli start a cli session +func (c *cliService) StartCli(server string, db int) (resp types.JSResp) { + client, err := c.getRedisClient(server) + if err != nil { + resp.Msg = err.Error() + return + } + client.Do(c.ctx, "select", db) + c.selectedDB[server] = db + + // monitor input + runtime.EventsOn(c.ctx, "cmd:input:"+server, func(data ...interface{}) { + if len(data) > 0 { + if str, ok := data[0].(string); ok { + c.runCommand(server, str) + return + } + } + c.echoReady(server) + }) + + // echo prefix + c.echoReady(server) + resp.Success = true + return +} + +// CloseCli close cli session +func (c *cliService) CloseCli(server string) (resp types.JSResp) { + c.mutex.Lock() + defer c.mutex.Unlock() + + if client, ok := c.clients[server]; ok { + client.Close() + delete(c.clients, server) + delete(c.selectedDB, server) + } + runtime.EventsOff(c.ctx, "cmd:input:"+server) + resp.Success = true + return +} + +// CloseAll close all cli sessions +func (c *cliService) CloseAll() { + if c.ctxCancel != nil { + c.ctxCancel() + } + + for server := range c.clients { + c.CloseCli(server) + } +} diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index fc61e2e..9d4da89 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -71,7 +71,7 @@ func (c *connectionService) Start(ctx context.Context) { c.ctx = ctx } -func (c *connectionService) Stop(ctx context.Context) { +func (c *connectionService) Stop() { for _, item := range c.connMap { if item.client != nil { item.cancelFunc() @@ -307,9 +307,13 @@ func (c *connectionService) ListConnection() (resp types.JSResp) { return } +func (c *connectionService) getConnection(name string) *types.Connection { + return c.conns.GetConnection(name) +} + // GetConnection get connection profile by name func (c *connectionService) GetConnection(name string) (resp types.JSResp) { - conn := c.conns.GetConnection(name) + conn := c.getConnection(name) resp.Success = conn != nil resp.Data = conn return diff --git a/backend/utils/string/any_convert.go b/backend/utils/string/any_convert.go new file mode 100644 index 0000000..0372016 --- /dev/null +++ b/backend/utils/string/any_convert.go @@ -0,0 +1,107 @@ +package strutil + +import ( + "encoding/json" + "strconv" + sliceutil "tinyrdm/backend/utils/slice" +) + +func AnyToString(value interface{}) (s string) { + if value == nil { + return + } + + switch value.(type) { + case float64: + ft := value.(float64) + s = strconv.FormatFloat(ft, 'f', -1, 64) + case float32: + ft := value.(float32) + s = strconv.FormatFloat(float64(ft), 'f', -1, 64) + case int: + it := value.(int) + s = strconv.Itoa(it) + case uint: + it := value.(uint) + s = strconv.Itoa(int(it)) + case int8: + it := value.(int8) + s = strconv.Itoa(int(it)) + case uint8: + it := value.(uint8) + s = strconv.Itoa(int(it)) + case int16: + it := value.(int16) + s = strconv.Itoa(int(it)) + case uint16: + it := value.(uint16) + s = strconv.Itoa(int(it)) + case int32: + it := value.(int32) + s = strconv.Itoa(int(it)) + case uint32: + it := value.(uint32) + s = strconv.Itoa(int(it)) + case int64: + it := value.(int64) + s = strconv.FormatInt(it, 10) + case uint64: + it := value.(uint64) + s = strconv.FormatUint(it, 10) + case string: + s = value.(string) + case bool: + val, _ := value.(bool) + if val { + s = "True" + } else { + s = "False" + } + case []byte: + s = string(value.([]byte)) + case []string: + ss := value.([]string) + anyStr := sliceutil.Map(ss, func(i int) string { + str := AnyToString(ss[i]) + return strconv.Itoa(i+1) + ") \"" + str + "\"" + }) + s = sliceutil.JoinString(anyStr, "\r\n") + case []any: + as := value.([]any) + anyItems := sliceutil.Map(as, func(i int) string { + str := AnyToString(as[i]) + return strconv.Itoa(i+1) + ") \"" + str + "\"" + }) + s = sliceutil.JoinString(anyItems, "\r\n") + default: + b, _ := json.Marshal(value) + s = string(b) + } + + return +} + +//func AnyToHex(val any) (string, bool) { +// var src string +// switch val.(type) { +// case string: +// src = val.(string) +// case []byte: +// src = string(val.([]byte)) +// } +// +// if len(src) <= 0 { +// return "", false +// } +// +// var output strings.Builder +// for i := range src { +// if !utf8.ValidString(src[i : i+1]) { +// output.WriteString(fmt.Sprintf("\\x%02x", src[i:i+1])) +// } else { +// output.WriteString(src[i : i+1]) +// } +// } +// +// return output.String(), true +//} diff --git a/frontend/src/components/content/ContentPane.vue b/frontend/src/components/content/ContentPane.vue index 8271dac..7870bcf 100644 --- a/frontend/src/components/content/ContentPane.vue +++ b/frontend/src/components/content/ContentPane.vue @@ -203,6 +203,7 @@ const onSwitchSubTab = (name) => { @@ -301,7 +302,6 @@ const onSwitchSubTab = (name) => { + + + diff --git a/frontend/src/langs/en.json b/frontend/src/langs/en.json index ebfcca7..aac6724 100644 --- a/frontend/src/langs/en.json +++ b/frontend/src/langs/en.json @@ -94,6 +94,7 @@ "type": "Type", "score": "Score", "total": "Length: {size}", + "cli_welcome": "Welcome to Tiny RDM Redis Console", "sub_tab": { "status": "Status", "key_detail": "Key Detail", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index c50bc7f..b928d60 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -94,6 +94,7 @@ "type": "类型", "score": "分值", "total": "总数:{size}", + "cli_welcome": "欢迎使用Tiny RDM的Redis命令行控制台", "sub_tab": { "status": "状态", "key_detail": "键详情", diff --git a/frontend/src/styles/style.scss b/frontend/src/styles/style.scss index b688c04..5130c93 100644 --- a/frontend/src/styles/style.scss +++ b/frontend/src/styles/style.scss @@ -22,7 +22,7 @@ body { background-color: #0000; line-height: 1.5; font-family: v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - //--wails-draggable: drag; + overflow: hidden; } #app { diff --git a/go.sum b/go.sum index 66c465b..5820771 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4= github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8= -github.com/wailsapp/go-webview2 v1.0.8 h1:hyoFPlMSfb/NM64wuVbgBaq1MASJjqsSUYhN+Rbcr9Y= -github.com/wailsapp/go-webview2 v1.0.8/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= +github.com/wailsapp/go-webview2 v1.0.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w= +github.com/wailsapp/go-webview2 v1.0.10/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c= diff --git a/main.go b/main.go index decedeb..807406d 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ func main() { // Create an instance of the app structure sysSvc := services.System() connSvc := services.Connection() + cliSvc := services.Cli() prefSvc := services.Preferences() prefSvc.SetAppVersion(version) windowWidth, windowHeight := prefSvc.GetWindowSize() @@ -56,16 +57,19 @@ func main() { OnStartup: func(ctx context.Context) { sysSvc.Start(ctx) connSvc.Start(ctx) + cliSvc.Start(ctx) services.GA().SetSecretKey(gaMeasurementID, gaSecretKey) services.GA().Startup(version) }, OnShutdown: func(ctx context.Context) { - connSvc.Stop(ctx) + connSvc.Stop() + cliSvc.CloseAll() }, Bind: []interface{}{ sysSvc, connSvc, + cliSvc, prefSvc, }, Mac: &mac.Options{