Skip to content

Commit

Permalink
feat: add command line mode
Browse files Browse the repository at this point in the history
  • Loading branch information
tiny-craft committed Oct 26, 2023
1 parent 5b4683a commit 1cf8931
Show file tree
Hide file tree
Showing 10 changed files with 677 additions and 11 deletions.
160 changes: 160 additions & 0 deletions backend/services/cli_service.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
8 changes: 6 additions & 2 deletions backend/services/connection_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions backend/utils/string/any_convert.go
Original file line number Diff line number Diff line change
@@ -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
//}
4 changes: 2 additions & 2 deletions frontend/src/components/content/ContentPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ const onSwitchSubTab = (name) => {
<n-tabs
:tabs-padding="5"
:theme-overrides="{
tabFontWeightActive: 'normal',
tabGapSmallLine: '10px',
tabGapMediumLine: '10px',
tabGapLargeLine: '10px',
Expand Down Expand Up @@ -270,7 +271,7 @@ const onSwitchSubTab = (name) => {
<span>{{ $t('interface.sub_tab.cli') }}</span>
</n-space>
</template>
<content-cli />
<content-cli :name="currentServer.name" />
</n-tab-pane>

<!-- slow log pane -->
Expand Down Expand Up @@ -301,7 +302,6 @@ const onSwitchSubTab = (name) => {

<style lang="scss">
.content-sub-tab {
margin-bottom: 5px;
background-color: v-bind('themeVars.bodyColor');
height: 100%;
}
Expand Down
Loading

0 comments on commit 1cf8931

Please sign in to comment.