mirror of
				https://github.com/tiny-craft/tiny-rdm.git
				synced 2025-10-31 18:42:33 +08:00 
			
		
		
		
	feat: add command line mode
This commit is contained in:
		
							
								
								
									
										160
									
								
								backend/services/cli_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								backend/services/cli_service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
|  | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										107
									
								
								backend/utils/string/any_convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								backend/utils/string/any_convert.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|             :tabs-padding="5" | ||||
|             :theme-overrides="{ | ||||
|                 tabFontWeightActive: 'normal', | ||||
|                 tabGapSmallLine: '10px', | ||||
|                 tabGapMediumLine: '10px', | ||||
|                 tabGapLargeLine: '10px', | ||||
| @@ -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 --> | ||||
| @@ -301,7 +302,6 @@ const onSwitchSubTab = (name) => { | ||||
|  | ||||
| <style lang="scss"> | ||||
| .content-sub-tab { | ||||
|     margin-bottom: 5px; | ||||
|     background-color: v-bind('themeVars.bodyColor'); | ||||
|     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> | ||||
|     <n-empty description="coming soon" class="empty-content"></n-empty> | ||||
|     <div ref="termRef" class="xterm" /> | ||||
| </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", | ||||
|     "score": "Score", | ||||
|     "total": "Length: {size}", | ||||
|     "cli_welcome": "Welcome to Tiny RDM Redis Console", | ||||
|     "sub_tab": { | ||||
|       "status": "Status", | ||||
|       "key_detail": "Key Detail", | ||||
|   | ||||
| @@ -94,6 +94,7 @@ | ||||
|     "type": "类型", | ||||
|     "score": "分值", | ||||
|     "total": "总数:{size}", | ||||
|     "cli_welcome": "欢迎使用Tiny RDM的Redis命令行控制台", | ||||
|     "sub_tab": { | ||||
|       "status": "状态", | ||||
|       "key_detail": "键详情", | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										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/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= | ||||
|   | ||||
							
								
								
									
										6
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								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{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 tiny-craft
					tiny-craft