feat: add command line mode
This commit is contained in:
parent
5b4683a735
commit
1cf893149a
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ func (c *connectionService) Start(ctx context.Context) {
|
||||||
c.ctx = ctx
|
c.ctx = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connectionService) Stop(ctx context.Context) {
|
func (c *connectionService) Stop() {
|
||||||
for _, item := range c.connMap {
|
for _, item := range c.connMap {
|
||||||
if item.client != nil {
|
if item.client != nil {
|
||||||
item.cancelFunc()
|
item.cancelFunc()
|
||||||
|
@ -307,9 +307,13 @@ func (c *connectionService) ListConnection() (resp types.JSResp) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *connectionService) getConnection(name string) *types.Connection {
|
||||||
|
return c.conns.GetConnection(name)
|
||||||
|
}
|
||||||
|
|
||||||
// GetConnection get connection profile by name
|
// GetConnection get connection profile by name
|
||||||
func (c *connectionService) GetConnection(name string) (resp types.JSResp) {
|
func (c *connectionService) GetConnection(name string) (resp types.JSResp) {
|
||||||
conn := c.conns.GetConnection(name)
|
conn := c.getConnection(name)
|
||||||
resp.Success = conn != nil
|
resp.Success = conn != nil
|
||||||
resp.Data = conn
|
resp.Data = conn
|
||||||
return
|
return
|
||||||
|
|
|
@ -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
|
||||||
|
//}
|
|
@ -203,6 +203,7 @@ const onSwitchSubTab = (name) => {
|
||||||
<n-tabs
|
<n-tabs
|
||||||
:tabs-padding="5"
|
:tabs-padding="5"
|
||||||
:theme-overrides="{
|
:theme-overrides="{
|
||||||
|
tabFontWeightActive: 'normal',
|
||||||
tabGapSmallLine: '10px',
|
tabGapSmallLine: '10px',
|
||||||
tabGapMediumLine: '10px',
|
tabGapMediumLine: '10px',
|
||||||
tabGapLargeLine: '10px',
|
tabGapLargeLine: '10px',
|
||||||
|
@ -270,7 +271,7 @@ const onSwitchSubTab = (name) => {
|
||||||
<span>{{ $t('interface.sub_tab.cli') }}</span>
|
<span>{{ $t('interface.sub_tab.cli') }}</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
<content-cli />
|
<content-cli :name="currentServer.name" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<!-- slow log pane -->
|
<!-- slow log pane -->
|
||||||
|
@ -301,7 +302,6 @@ const onSwitchSubTab = (name) => {
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.content-sub-tab {
|
.content-sub-tab {
|
||||||
margin-bottom: 5px;
|
|
||||||
background-color: v-bind('themeVars.bodyColor');
|
background-color: v-bind('themeVars.bodyColor');
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,396 @@
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
import { Terminal } from 'xterm'
|
||||||
|
import { FitAddon } from 'xterm-addon-fit'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import 'xterm/css/xterm.css'
|
||||||
|
import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
|
||||||
|
import { get, isEmpty, set, size } from 'lodash'
|
||||||
|
import { CloseCli, StartCli } from 'wailsjs/go/services/cliService.js'
|
||||||
|
import usePreferencesStore from 'stores/preferences.js'
|
||||||
|
import { i18nGlobal } from '@/utils/i18n.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: String,
|
||||||
|
activated: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const prefStore = usePreferencesStore()
|
||||||
|
const termRef = ref(null)
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {xterm.Terminal|null}
|
||||||
|
*/
|
||||||
|
let termInst = null
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {xterm-addon-fit.FitAddon|null}
|
||||||
|
*/
|
||||||
|
let fitAddonInst = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return {{fitAddon: xterm-addon-fit.FitAddon, term: Terminal}}
|
||||||
|
*/
|
||||||
|
const newTerm = () => {
|
||||||
|
const term = new Terminal({
|
||||||
|
allowProposedApi: true,
|
||||||
|
fontSize: prefStore.general.fontSize || 14,
|
||||||
|
cursorBlink: true,
|
||||||
|
disableStdin: false,
|
||||||
|
screenReaderMode: true,
|
||||||
|
// LogLevel: 'debug',
|
||||||
|
theme: {
|
||||||
|
// foreground: '#ECECEC',
|
||||||
|
background: '#000000',
|
||||||
|
// cursor: 'help',
|
||||||
|
// lineHeight: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
term.open(termRef.value)
|
||||||
|
term.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
term.onData(onTermData)
|
||||||
|
return { term, fitAddon }
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervalID
|
||||||
|
onMounted(() => {
|
||||||
|
const { term, fitAddon } = newTerm()
|
||||||
|
termInst = term
|
||||||
|
fitAddonInst = fitAddon
|
||||||
|
// window.addEventListener('resize', resizeTerm)
|
||||||
|
|
||||||
|
term.writeln('\r\n' + i18nGlobal.t('interface.cli_welcome'))
|
||||||
|
// term.write('\x1b[4h') // insert mode
|
||||||
|
CloseCli(props.name)
|
||||||
|
StartCli(props.name, 0)
|
||||||
|
|
||||||
|
EventsOn(`cmd:output:${props.name}`, receiveTermOutput)
|
||||||
|
fitAddon.fit()
|
||||||
|
term.focus()
|
||||||
|
|
||||||
|
intervalID = setInterval(() => {
|
||||||
|
if (props.activated) {
|
||||||
|
resizeTerm()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(intervalID)
|
||||||
|
// window.removeEventListener('resize', resizeTerm)
|
||||||
|
EventsOff(`cmd:output:${props.name}`)
|
||||||
|
termInst.dispose()
|
||||||
|
termInst = null
|
||||||
|
console.warn('destroy term')
|
||||||
|
})
|
||||||
|
|
||||||
|
const resizeTerm = () => {
|
||||||
|
if (fitAddonInst != null) {
|
||||||
|
fitAddonInst.fit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => prefStore.general.fontSize,
|
||||||
|
(fontSize) => {
|
||||||
|
if (termInst != null) {
|
||||||
|
termInst.options.fontSize = fontSize
|
||||||
|
}
|
||||||
|
resizeTerm()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const prefixContent = computed(() => {
|
||||||
|
return '\x1b[33m' + promptPrefix.value + '\x1b[0m'
|
||||||
|
})
|
||||||
|
|
||||||
|
let promptPrefix = ref('')
|
||||||
|
let inputCursor = 0
|
||||||
|
const inputHistory = []
|
||||||
|
let historyIndex = 0
|
||||||
|
let waitForOutput = false
|
||||||
|
const onTermData = (data) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const cc = data.charCodeAt(0)
|
||||||
|
switch (cc) {
|
||||||
|
case 127: // backspace
|
||||||
|
deleteInput(true)
|
||||||
|
return
|
||||||
|
|
||||||
|
case 13: // enter
|
||||||
|
// try to process local command first
|
||||||
|
console.log('enter con', getCurrentInput())
|
||||||
|
switch (getCurrentInput()) {
|
||||||
|
case 'clear':
|
||||||
|
case 'clr':
|
||||||
|
termInst.clear()
|
||||||
|
replaceTermInput()
|
||||||
|
newInputLine()
|
||||||
|
return
|
||||||
|
|
||||||
|
default: // send command to server
|
||||||
|
flushTermInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 27:
|
||||||
|
switch (data.substring(1)) {
|
||||||
|
case '[A': // arrow up
|
||||||
|
changeHistory(true)
|
||||||
|
return
|
||||||
|
case '[B': // arrow down
|
||||||
|
changeHistory(false)
|
||||||
|
return
|
||||||
|
case '[C': // arrow right ->
|
||||||
|
moveInputCursor(1)
|
||||||
|
return
|
||||||
|
case '[D': // arrow left <-
|
||||||
|
moveInputCursor(-1)
|
||||||
|
return
|
||||||
|
case '[3~': // del
|
||||||
|
deleteInput(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case 9: // tab
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInput(data)
|
||||||
|
// term.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* move input cursor by step
|
||||||
|
* @param {number} step above 0 indicate move right; 0 indicate move to last
|
||||||
|
*/
|
||||||
|
const moveInputCursor = (step) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateCursor = false
|
||||||
|
if (step > 0) {
|
||||||
|
// move right
|
||||||
|
const currentLine = getCurrentInput()
|
||||||
|
if (inputCursor + step <= currentLine.length) {
|
||||||
|
inputCursor += step
|
||||||
|
updateCursor = true
|
||||||
|
}
|
||||||
|
} else if (step < 0) {
|
||||||
|
// move left
|
||||||
|
if (inputCursor + step >= 0) {
|
||||||
|
inputCursor += step
|
||||||
|
updateCursor = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// update cursor position only
|
||||||
|
const currentLine = getCurrentInput()
|
||||||
|
inputCursor = Math.min(Math.max(0, inputCursor), currentLine.length)
|
||||||
|
updateCursor = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateCursor) {
|
||||||
|
termInst.write(`\x1B[${size(promptPrefix.value) + inputCursor + 1}G`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update current input cache and refresh term
|
||||||
|
* @param {string} data
|
||||||
|
*/
|
||||||
|
const updateInput = (data) => {
|
||||||
|
if (data == null || data.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLine = getCurrentInput()
|
||||||
|
if (inputCursor < currentLine.length) {
|
||||||
|
// insert
|
||||||
|
currentLine = currentLine.substring(0, inputCursor) + data + currentLine.substring(inputCursor)
|
||||||
|
replaceTermInput()
|
||||||
|
termInst.write(currentLine)
|
||||||
|
moveInputCursor(data.length)
|
||||||
|
} else {
|
||||||
|
// append
|
||||||
|
currentLine += data
|
||||||
|
termInst.write(data)
|
||||||
|
inputCursor += data.length
|
||||||
|
}
|
||||||
|
updateCurrentInput(currentLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {boolean} back backspace or not
|
||||||
|
*/
|
||||||
|
const deleteInput = (back = false) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentLine = getCurrentInput()
|
||||||
|
if (inputCursor < currentLine.length) {
|
||||||
|
// delete middle part
|
||||||
|
if (back) {
|
||||||
|
currentLine = currentLine.substring(0, inputCursor - 1) + currentLine.substring(inputCursor)
|
||||||
|
inputCursor -= 1
|
||||||
|
} else {
|
||||||
|
currentLine = currentLine.substring(0, inputCursor) + currentLine.substring(inputCursor + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// delete last one
|
||||||
|
currentLine = currentLine.slice(0, -1)
|
||||||
|
inputCursor -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceTermInput()
|
||||||
|
termInst.write(currentLine)
|
||||||
|
updateCurrentInput(currentLine)
|
||||||
|
moveInputCursor(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentInput = () => {
|
||||||
|
return get(inputHistory, historyIndex, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCurrentInput = (input) => {
|
||||||
|
set(inputHistory, historyIndex, input || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInputLine = () => {
|
||||||
|
if (historyIndex >= 0 && historyIndex < inputHistory.length - 1) {
|
||||||
|
// edit prev history, move to last
|
||||||
|
const pop = inputHistory.splice(historyIndex, 1)
|
||||||
|
inputHistory[inputHistory.length - 1] = pop[0]
|
||||||
|
}
|
||||||
|
if (get(inputHistory, inputHistory.length - 1, '')) {
|
||||||
|
historyIndex = inputHistory.length
|
||||||
|
updateCurrentInput('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get prev or next history record
|
||||||
|
* @param prev
|
||||||
|
* @return {*|null}
|
||||||
|
*/
|
||||||
|
const changeHistory = (prev) => {
|
||||||
|
let currentLine = null
|
||||||
|
if (prev) {
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex -= 1
|
||||||
|
currentLine = inputHistory[historyIndex]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (historyIndex < inputHistory.length - 1) {
|
||||||
|
historyIndex += 1
|
||||||
|
currentLine = inputHistory[historyIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine != null) {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceTermInput()
|
||||||
|
termInst.write(currentLine)
|
||||||
|
moveInputCursor(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* flush terminal input and send current prompt to server
|
||||||
|
* @param {boolean} flushCmd
|
||||||
|
*/
|
||||||
|
const flushTermInput = (flushCmd = false) => {
|
||||||
|
const currentLine = getCurrentInput()
|
||||||
|
console.log('===send cmd', currentLine, currentLine.length)
|
||||||
|
EventsEmit(`cmd:input:${props.name}`, currentLine)
|
||||||
|
inputCursor = 0
|
||||||
|
// historyIndex = inputHistory.length
|
||||||
|
waitForOutput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear current input line and replace with new content
|
||||||
|
* @param {string} [content]
|
||||||
|
*/
|
||||||
|
const replaceTermInput = (content = '') => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// erase current line and write new content
|
||||||
|
termInst.write('\r\x1B[K' + prefixContent.value + (content || ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* process receive output content
|
||||||
|
* @param {{content, prompt}} data
|
||||||
|
*/
|
||||||
|
const receiveTermOutput = (data) => {
|
||||||
|
if (termInst == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { content, prompt } = data || {}
|
||||||
|
if (!isEmpty(content)) {
|
||||||
|
termInst.write('\r\n' + content)
|
||||||
|
}
|
||||||
|
if (!isEmpty(prompt)) {
|
||||||
|
promptPrefix.value = prompt
|
||||||
|
termInst.write('\r\n' + prefixContent.value)
|
||||||
|
waitForOutput = false
|
||||||
|
inputCursor = 0
|
||||||
|
newInputLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-empty description="coming soon" class="empty-content"></n-empty>
|
<div ref="termRef" class="xterm" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.xterm {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.xterm-screen {
|
||||||
|
padding: 0 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar {
|
||||||
|
background-color: #000000;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-decoration-overview-ruler {
|
||||||
|
right: 1px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"total": "Length: {size}",
|
"total": "Length: {size}",
|
||||||
|
"cli_welcome": "Welcome to Tiny RDM Redis Console",
|
||||||
"sub_tab": {
|
"sub_tab": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"key_detail": "Key Detail",
|
"key_detail": "Key Detail",
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"score": "分值",
|
"score": "分值",
|
||||||
"total": "总数:{size}",
|
"total": "总数:{size}",
|
||||||
|
"cli_welcome": "欢迎使用Tiny RDM的Redis命令行控制台",
|
||||||
"sub_tab": {
|
"sub_tab": {
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"key_detail": "键详情",
|
"key_detail": "键详情",
|
||||||
|
|
|
@ -22,7 +22,7 @@ body {
|
||||||
background-color: #0000;
|
background-color: #0000;
|
||||||
line-height: 1.5;
|
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";
|
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 {
|
#app {
|
||||||
|
|
4
go.sum
4
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/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 h1:Ah2/69Z24rwD6OByyOdpJDmttftz0FTF8Q4QZ/SF1E4=
|
||||||
github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68/go.mod h1:EqKqAeKddSL9XSGnfXd/7iLncccKhR16HBKVva7ENw8=
|
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.10 h1:PP5Hug6pnQEAhfRzLCoOh2jJaPdrqeRgJKZhyYyDV/w=
|
||||||
github.com/wailsapp/go-webview2 v1.0.8/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c=
|
github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c=
|
||||||
|
|
6
main.go
6
main.go
|
@ -28,6 +28,7 @@ func main() {
|
||||||
// Create an instance of the app structure
|
// Create an instance of the app structure
|
||||||
sysSvc := services.System()
|
sysSvc := services.System()
|
||||||
connSvc := services.Connection()
|
connSvc := services.Connection()
|
||||||
|
cliSvc := services.Cli()
|
||||||
prefSvc := services.Preferences()
|
prefSvc := services.Preferences()
|
||||||
prefSvc.SetAppVersion(version)
|
prefSvc.SetAppVersion(version)
|
||||||
windowWidth, windowHeight := prefSvc.GetWindowSize()
|
windowWidth, windowHeight := prefSvc.GetWindowSize()
|
||||||
|
@ -56,16 +57,19 @@ func main() {
|
||||||
OnStartup: func(ctx context.Context) {
|
OnStartup: func(ctx context.Context) {
|
||||||
sysSvc.Start(ctx)
|
sysSvc.Start(ctx)
|
||||||
connSvc.Start(ctx)
|
connSvc.Start(ctx)
|
||||||
|
cliSvc.Start(ctx)
|
||||||
|
|
||||||
services.GA().SetSecretKey(gaMeasurementID, gaSecretKey)
|
services.GA().SetSecretKey(gaMeasurementID, gaSecretKey)
|
||||||
services.GA().Startup(version)
|
services.GA().Startup(version)
|
||||||
},
|
},
|
||||||
OnShutdown: func(ctx context.Context) {
|
OnShutdown: func(ctx context.Context) {
|
||||||
connSvc.Stop(ctx)
|
connSvc.Stop()
|
||||||
|
cliSvc.CloseAll()
|
||||||
},
|
},
|
||||||
Bind: []interface{}{
|
Bind: []interface{}{
|
||||||
sysSvc,
|
sysSvc,
|
||||||
connSvc,
|
connSvc,
|
||||||
|
cliSvc,
|
||||||
prefSvc,
|
prefSvc,
|
||||||
},
|
},
|
||||||
Mac: &mac.Options{
|
Mac: &mac.Options{
|
||||||
|
|
Loading…
Reference in New Issue