diff --git a/backend/services/cli_service.go b/backend/services/cli_service.go
new file mode 100644
index 00000000..e419f39d
--- /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 fc61e2eb..9d4da892 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 00000000..0372016b
--- /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 8271dacb..7870bcfe 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 ebfcca71..aac6724a 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 c50bc7fd..b928d605 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 b688c045..5130c939 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 66c465b8..5820771d 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 decedebc..807406de 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{