├── .prettierignore ├── frontend ├── package.json.md5 ├── README.md ├── src │ ├── assets │ │ ├── images │ │ │ └── icon.png │ │ └── fonts │ │ │ └── nunito-v16-latin-regular.woff2 │ ├── consts │ │ ├── key_view_type.js │ │ ├── tree_context_menu.js │ │ ├── connection_type.js │ │ ├── browser_tab_type.js │ │ ├── value_view_type.js │ │ └── support_redis_type.js │ ├── langs │ │ └── index.js │ ├── utils │ │ ├── platform.js │ │ ├── i18n.js │ │ ├── extra_theme.js │ │ ├── key_convert.js │ │ ├── version.js │ │ ├── monaco.js │ │ ├── rgb.js │ │ └── theme.js │ ├── styles │ │ ├── content.scss │ │ └── style.scss │ ├── components │ │ ├── icons │ │ │ ├── WindowMin.vue │ │ │ ├── Filter.vue │ │ │ ├── WindowMax.vue │ │ │ ├── Key.vue │ │ │ ├── Binary.vue │ │ │ ├── AddLink.vue │ │ │ ├── Edit.vue │ │ │ ├── More.vue │ │ │ ├── LoadAll.vue │ │ │ ├── AddGroup.vue │ │ │ ├── Layer.vue │ │ │ ├── WindowClose.vue │ │ │ ├── WindowRestore.vue │ │ │ ├── Refresh.vue │ │ │ ├── Add.vue │ │ │ ├── EditFile.vue │ │ │ ├── Save.vue │ │ │ ├── Record.vue │ │ │ ├── Monitor.vue │ │ │ ├── Search.vue │ │ │ ├── Delete.vue │ │ │ ├── FullScreen.vue │ │ │ ├── OffScreen.vue │ │ │ ├── Sort.vue │ │ │ ├── Pin.vue │ │ │ ├── Copy.vue │ │ │ ├── Update.vue │ │ │ ├── Code.vue │ │ │ ├── Terminal.vue │ │ │ ├── Help.vue │ │ │ ├── Unlink.vue │ │ │ ├── Pub.vue │ │ │ ├── Timer.vue │ │ │ ├── Log.vue │ │ │ ├── Database.vue │ │ │ ├── LoadList.vue │ │ │ ├── Conversion.vue │ │ │ ├── Close.vue │ │ │ ├── Clear.vue │ │ │ ├── Structure.vue │ │ │ ├── ListView.vue │ │ │ ├── Config.vue │ │ │ ├── TreeView.vue │ │ │ ├── Connect.vue │ │ │ ├── Server.vue │ │ │ ├── Github.vue │ │ │ ├── Folder.vue │ │ │ ├── CopyLink.vue │ │ │ ├── Detail.vue │ │ │ ├── Status.vue │ │ │ └── Cluster.vue │ │ ├── new_value │ │ │ ├── NewStringValue.vue │ │ │ ├── NewListValue.vue │ │ │ ├── NewSetValue.vue │ │ │ ├── NewHashValue.vue │ │ │ ├── AddListValue.vue │ │ │ ├── NewStreamValue.vue │ │ │ ├── NewZSetValue.vue │ │ │ ├── AddHashValue.vue │ │ │ └── AddZSetValue.vue │ │ ├── sidebar │ │ │ ├── ConnectionTreeItem.vue │ │ │ ├── ConnectionPane.vue │ │ │ └── BrowserPane.vue │ │ ├── common │ │ │ ├── EditableTableRow.vue │ │ │ ├── FileOpenInput.vue │ │ │ ├── RedisTypeTag.vue │ │ │ ├── EditableTableColumn.vue │ │ │ ├── SwitchButton.vue │ │ │ ├── IconButton.vue │ │ │ ├── DropdownSelector.vue │ │ │ ├── ResizeableWrapper.vue │ │ │ └── ToolbarControlWidget.vue │ │ ├── content │ │ │ └── ContentServerPane.vue │ │ ├── content_value │ │ │ ├── FormatSelector.vue │ │ │ └── ContentSearchInput.vue │ │ └── dialogs │ │ │ ├── AboutDialog.vue │ │ │ ├── RenameKeyDialog.vue │ │ │ ├── FlushDbDialog.vue │ │ │ ├── SetTtlDialog.vue │ │ │ ├── GroupDialog.vue │ │ │ └── KeyFilterDialog.vue │ ├── main.js │ └── App.vue ├── .prettierrc ├── index.html ├── package.json └── vite.config.js ├── screenshots ├── dark_en.png ├── dark_zh.png ├── light_en.png └── light_zh.png ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING_zh.md └── CONTRIBUTING.md ├── .gitignore ├── docs └── index.html ├── backend ├── consts │ └── default_config.go ├── types │ ├── view_type.go │ ├── redis_wrapper.go │ ├── preferences.go │ ├── js_resp.go │ └── connection.go ├── utils │ ├── constraints.go │ ├── string │ │ ├── common.go │ │ ├── key_convert.go │ │ └── any_convert.go │ ├── math │ │ └── math_util.go │ └── redis │ │ └── log_hook.go ├── storage │ ├── local_storage.go │ └── preferences.go └── services │ ├── ga_service.go │ └── system_service.go ├── wails.json ├── go.mod ├── README_zh.md ├── main.go └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | /frontend/wailsjs/** 2 | -------------------------------------------------------------------------------- /frontend/package.json.md5: -------------------------------------------------------------------------------- 1 | 3dc6322e376d91c1cb4a6bd5312d176f -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend of Tiny RDM 2 | 3 | Use Vue3 + Vite 4 | -------------------------------------------------------------------------------- /screenshots/dark_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/screenshots/dark_en.png -------------------------------------------------------------------------------- /screenshots/dark_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/screenshots/dark_zh.png -------------------------------------------------------------------------------- /screenshots/light_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/screenshots/light_en.png -------------------------------------------------------------------------------- /screenshots/light_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/screenshots/light_zh.png -------------------------------------------------------------------------------- /frontend/src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/frontend/src/assets/images/icon.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE]' 5 | --- 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | node_modules 3 | frontend/dist 4 | frontend/wailsjs 5 | frontend/package.json.md5 6 | design/ 7 | .vscode 8 | .idea 9 | test 10 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/nunito-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctfang/tiny-rdm/main/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /frontend/src/consts/key_view_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * all types of redis key viewing 3 | * @enum {number} 4 | */ 5 | export const KeyViewType = { 6 | Tree: 0, 7 | List: 1, 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/langs/index.js: -------------------------------------------------------------------------------- 1 | import en from './en-us' 2 | import pt from './pt-br' 3 | import zh from './zh-cn' 4 | 5 | export const lang = { 6 | en, 7 | pt, 8 | zh, 9 | } 10 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "semi": false, 6 | "bracketSameLine": true, 7 | "endOfLine": "auto", 8 | "htmlWhitespaceSensitivity": "ignore" 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/consts/tree_context_menu.js: -------------------------------------------------------------------------------- 1 | import { ConnectionType } from './connection_type.js' 2 | 3 | export const contextMenuKey = { 4 | [ConnectionType.Server]: { 5 | key: '', 6 | label: '', 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tiny RDM 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/consts/connection_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * all types of connection item 3 | * @enum {number} 4 | */ 5 | export const ConnectionType = { 6 | Group: 0, 7 | Server: 1, 8 | RedisDB: 2, 9 | RedisKey: 3, 10 | RedisValue: 4, 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/utils/platform.js: -------------------------------------------------------------------------------- 1 | import { Environment } from 'wailsjs/runtime/runtime.js' 2 | 3 | let os = '' 4 | 5 | export async function loadEnvironment() { 6 | const env = await Environment() 7 | os = env.platform 8 | } 9 | 10 | export function isMacOS() { 11 | return os === 'darwin' 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/styles/content.scss: -------------------------------------------------------------------------------- 1 | .content-container { 2 | height: 100%; 3 | overflow: hidden; 4 | box-sizing: border-box; 5 | } 6 | 7 | .empty-content { 8 | height: 100%; 9 | justify-content: center; 10 | } 11 | 12 | .content-value { 13 | user-select: text; 14 | } 15 | 16 | .tab-content { 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/consts/browser_tab_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * all types of Browser sub tabs 3 | * @enum {string} 4 | */ 5 | export const BrowserTabType = { 6 | Status: 'status', 7 | KeyDetail: 'key_detail', 8 | Cli: 'cli', 9 | SlowLog: 'slow_log', 10 | CmdMonitor: 'cmd_monitor', 11 | PubMessage: 'pub_message', 12 | } 13 | -------------------------------------------------------------------------------- /backend/consts/default_config.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const DEFAULT_FONT_SIZE = 14 4 | const DEFAULT_ASIDE_WIDTH = 300 5 | const DEFAULT_WINDOW_WIDTH = 1024 6 | const DEFAULT_WINDOW_HEIGHT = 768 7 | const MIN_WINDOW_WIDTH = 960 8 | const MIN_WINDOW_HEIGHT = 640 9 | const DEFAULT_LOAD_SIZE = 10000 10 | const DEFAULT_SCAN_SIZE = 3000 11 | -------------------------------------------------------------------------------- /frontend/src/utils/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { lang } from '@/langs/index.js' 3 | 4 | export const i18n = createI18n({ 5 | locale: 'en-us', 6 | fallbackLocale: 'en-us', 7 | globalInjection: true, 8 | legacy: false, 9 | messages: { 10 | ...lang, 11 | }, 12 | }) 13 | 14 | export const i18nGlobal = i18n.global 15 | -------------------------------------------------------------------------------- /backend/types/view_type.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const FORMAT_RAW = "Raw" 4 | const FORMAT_JSON = "JSON" 5 | const FORMAT_HEX = "Hex" 6 | const FORMAT_BINARY = "Binary" 7 | 8 | const DECODE_NONE = "None" 9 | const DECODE_BASE64 = "Base64" 10 | const DECODE_GZIP = "GZip" 11 | const DECODE_DEFLATE = "Deflate" 12 | const DECODE_ZSTD = "ZStd" 13 | const DECODE_BROTLI = "Brotli" 14 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tiny RDM 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /backend/utils/constraints.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Hashable interface { 4 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string 5 | } 6 | 7 | type SignedNumber interface { 8 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64 9 | } 10 | 11 | type UnsignedNumber interface { 12 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WindowMin.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Tiny RDM Version** 10 | What version of Tiny RDM are you using? 11 | 12 | **OS Version** 13 | Which OS and version you launch? (Mac/Windows/Linux) 14 | 15 | **Redis Version** 16 | Which version of Redis are you using? 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | Steps to Reproduce: 22 | 23 | 1. 24 | 2. 25 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewStringValue.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Filter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /frontend/src/consts/value_view_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * string format types 3 | * @enum {string} 4 | */ 5 | export const formatTypes = { 6 | RAW: 'Raw', 7 | JSON: 'JSON', 8 | // XML: 'XML', 9 | // YML: 'YML', 10 | HEX: 'Hex', 11 | BINARY: 'Binary', 12 | } 13 | 14 | /** 15 | * string decode types 16 | * @enum {string} 17 | */ 18 | export const decodeTypes = { 19 | NONE: 'None', 20 | BASE64: 'Base64', 21 | GZIP: 'GZip', 22 | DEFLATE: 'Deflate', 23 | ZSTD: 'ZStd', 24 | BROTLI: 'Brotli', 25 | // PHP: 'PHP', 26 | // Java: 'Java', 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/ConnectionTreeItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "tinyrdm", 4 | "outputfilename": "Tiny RDM", 5 | "frontend:install": "npm install", 6 | "frontend:build": "npm run build", 7 | "frontend:dev:watcher": "npm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "tiny-craft", 11 | "email": "lykinhuang@outlook.com" 12 | }, 13 | "info": { 14 | "companyName": "Tiny Craft", 15 | "productName": "Tiny RDM", 16 | "productVersion": "1.0.0", 17 | "copyright": "Copyright © 2023", 18 | "comments": "Tiny Redis Desktop Manager" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WindowMax.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Key.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Binary.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/common/EditableTableRow.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /backend/utils/string/common.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | func containsBinary(str string) bool { 8 | //buf := []byte(str) 9 | //size := 0 10 | //for start := 0; start < len(buf); start += size { 11 | // var r rune 12 | // if r, size = utf8.DecodeRune(buf[start:]); r == utf8.RuneError { 13 | // return true 14 | // } 15 | //} 16 | rs := []rune(str) 17 | for _, r := range rs { 18 | if !unicode.IsPrint(r) && !unicode.IsSpace(r) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | func isSameChar(str string) bool { 26 | if len(str) <= 0 { 27 | return false 28 | } 29 | 30 | rs := []rune(str) 31 | first := rs[0] 32 | for _, r := range rs { 33 | if r != first { 34 | return false 35 | } 36 | } 37 | 38 | return true 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/icons/AddLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Edit.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/icons/More.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "bytes": "^3.1.2", 13 | "dayjs": "^1.11.10", 14 | "highlight.js": "^11.9.0", 15 | "lodash": "^4.17.21", 16 | "monaco-editor": "^0.44.0", 17 | "pinia": "^2.1.7", 18 | "sass": "^1.69.5", 19 | "vue": "^3.3.9", 20 | "vue-i18n": "^9.7.1", 21 | "xterm": "^5.3.0", 22 | "xterm-addon-fit": "^0.8.0" 23 | }, 24 | "devDependencies": { 25 | "@vitejs/plugin-vue": "^4.5.0", 26 | "naive-ui": "^2.35.0", 27 | "prettier": "^3.1.0", 28 | "unplugin-auto-import": "^0.16.7", 29 | "unplugin-icons": "^0.17.4", 30 | "unplugin-vue-components": "^0.25.2", 31 | "vite": "^5.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/utils/extra_theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef ExtraTheme 3 | * @property {string} titleColor 4 | * @property {string} sidebarColor 5 | * @property {string} splitColor 6 | */ 7 | 8 | /** 9 | * 10 | * @type ExtraTheme 11 | */ 12 | export const extraLightTheme = { 13 | titleColor: '#F2F2F2', 14 | ribbonColor: '#F9F9F9', 15 | ribbonActiveColor: '#E3E3E3', 16 | sidebarColor: '#F2F2F2', 17 | splitColor: '#DADADA', 18 | } 19 | 20 | /** 21 | * 22 | * @type ExtraTheme 23 | */ 24 | export const extraDarkTheme = { 25 | titleColor: '#262626', 26 | ribbonColor: '#2C2C2C', 27 | ribbonActiveColor: '#363636', 28 | sidebarColor: '#262626', 29 | splitColor: '#474747', 30 | } 31 | 32 | /** 33 | * 34 | * @param {boolean} dark 35 | * @return ExtraTheme 36 | */ 37 | export const extraTheme = (dark) => { 38 | return dark ? extraDarkTheme : extraLightTheme 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/icons/LoadAll.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/components/icons/AddGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Layer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WindowClose.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/common/FileOpenInput.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/utils/key_convert.js: -------------------------------------------------------------------------------- 1 | import { join, map } from 'lodash' 2 | 3 | /** 4 | * converted binary data in strings to hex format 5 | * @param {string|number[]} key 6 | * @return {string} 7 | */ 8 | export function decodeRedisKey(key) { 9 | if (key instanceof Array) { 10 | // char array, convert to hex string 11 | return join( 12 | map(key, (k) => { 13 | if (k >= 32 && k <= 126) { 14 | return String.fromCharCode(k) 15 | } 16 | return '\\x' + k.toString(16).toUpperCase().padStart(2, '0') 17 | }), 18 | '', 19 | ) 20 | } 21 | 22 | return key 23 | } 24 | 25 | /** 26 | * convert char code array to string 27 | * @param {string|number[]} key 28 | * @return {string} 29 | */ 30 | export function nativeRedisKey(key) { 31 | if (key instanceof Array) { 32 | return map(key, (c) => String.fromCharCode(c)).join('') 33 | } 34 | return key 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/utils/version.js: -------------------------------------------------------------------------------- 1 | import { get, isEmpty, map, size, split, trimStart } from 'lodash' 2 | 3 | const toVerArr = (ver) => { 4 | const v = trimStart(ver, 'v') 5 | let vParts = split(v, '.') 6 | if (isEmpty(vParts)) { 7 | vParts = ['0'] 8 | } 9 | return map(vParts, (v) => { 10 | let vNum = parseInt(v) 11 | return isNaN(vNum) ? 0 : vNum 12 | }) 13 | } 14 | 15 | /** 16 | * compare two version strings 17 | * @param {string} v1 18 | * @param {string} v2 19 | * @return {number} 20 | */ 21 | export const compareVersion = (v1, v2) => { 22 | if (v1 !== v2) { 23 | const v1Nums = toVerArr(v1) 24 | const v2Nums = toVerArr(v2) 25 | const length = Math.max(size(v1Nums), size(v2Nums)) 26 | 27 | for (let i = 0; i < length; i++) { 28 | const num1 = get(v1Nums, i, 0) 29 | const num2 = get(v2Nums, i, 0) 30 | if (num1 !== num2) { 31 | return num1 > num2 ? 1 : -1 32 | } 33 | } 34 | } 35 | return 0 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/icons/WindowRestore.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import AutoImport from 'unplugin-auto-import/vite' 3 | import Icons from 'unplugin-icons/vite' 4 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { defineConfig } from 'vite' 7 | 8 | const rootPath = new URL('.', import.meta.url).pathname 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | AutoImport({ 14 | imports: [ 15 | { 16 | 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'], 17 | }, 18 | ], 19 | }), 20 | Components({ 21 | resolvers: [NaiveUiResolver()], 22 | }), 23 | Icons(), 24 | ], 25 | resolve: { 26 | alias: { 27 | '@': rootPath + 'src', 28 | stores: rootPath + 'src/stores', 29 | wailsjs: rootPath + 'wailsjs', 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Refresh.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Add.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/consts/support_redis_type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * all redis type 3 | * @enum {string} 4 | */ 5 | export const types = { 6 | STRING: 'STRING', 7 | HASH: 'HASH', 8 | LIST: 'LIST', 9 | SET: 'SET', 10 | ZSET: 'ZSET', 11 | STREAM: 'STREAM', 12 | } 13 | 14 | /** 15 | * mark color for redis types 16 | * @enum {string} 17 | */ 18 | export const typesColor = { 19 | [types.STRING]: '#8B5CF6', 20 | [types.HASH]: '#3B82F6', 21 | [types.LIST]: '#10B981', 22 | [types.SET]: '#F59E0B', 23 | [types.ZSET]: '#EF4444', 24 | [types.STREAM]: '#EC4899', 25 | } 26 | 27 | /** 28 | * background mark color for redis types 29 | * @enum {string} 30 | */ 31 | export const typesBgColor = { 32 | [types.STRING]: '#F2EDFB', 33 | [types.HASH]: '#E4F0FC', 34 | [types.LIST]: '#E3F3EB', 35 | [types.SET]: '#FDF1DF', 36 | [types.ZSET]: '#FAEAED', 37 | [types.STREAM]: '#FDE6F1', 38 | } 39 | 40 | // export const typesName = Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.name])) 41 | 42 | export const validType = (t) => { 43 | return types.hasOwnProperty(t) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/icons/EditFile.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Save.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Record.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Monitor.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Search.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Delete.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/components/icons/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/icons/OffScreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Sort.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Pin.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING_zh.md: -------------------------------------------------------------------------------- 1 | ## Tiny RDM 代码贡献指南 2 | 3 | ### 多国语言贡献 4 | 5 | #### 增加新的语言 6 | 1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件,文件名格式为“{语言}-{地区}.json”,如英文为“en-us.json”,简体中文为“zh-cn.json”,建议直接复制[en-us.json](../frontend/src/langs/en-us.json)文件进行改名。 7 | 2. 填充内容:参考[en-us.json](../frontend/src/langs/en-us.json),或者直接克隆一份文件,对语言部分内容进行修改。 8 | 3. 代码修改:在[frontend/src/langs/index.js](.../frontend/src/langs/index.js)文件内导入新增的语言数据 9 | ```javascript 10 | import en from './en-us' 11 | // import your new localize file 'zh-cn' here 12 | import zh from './zh-cn' 13 | 14 | export const lang = { 15 | en, 16 | // export new language data 'zh' here 17 | zh, 18 | } 19 | ``` 20 | 4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request)) 21 | 22 | ### 代码提交`(待完善)` 23 | 24 | #### PR提交规范 25 | PR提交格式为“: ” 26 | - type: 提交类型 27 | - description: 提交内容描述 28 | 29 | 其中提交类型如下: 30 | 31 | | 提交类型 | 类型描述 | 32 | |----------|--------------| 33 | | revert | 回退某个commit提交 | 34 | | feat | 新功能/新特性 | 35 | | perf | 功能、体验等方面的优化 | 36 | | fix | 修复问题 | 37 | | style | 样式相关修改 | 38 | | docs | 文档更新 | 39 | | refactor | 代码重构 | 40 | | chore | 杂项修改 | 41 | | ci | 自动化流程配置或脚本修改 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewListValue.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Copy.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Update.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewSetValue.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/content/ContentServerPane.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Code.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /backend/types/redis_wrapper.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ListEntryItem struct { 4 | Value any `json:"v"` 5 | DisplayValue string `json:"dv,omitempty"` 6 | } 7 | 8 | type ListReplaceItem struct { 9 | Index int64 `json:"index"` 10 | Value any `json:"v,omitempty"` 11 | DisplayValue string `json:"dv,omitempty"` 12 | } 13 | 14 | type HashEntryItem struct { 15 | Key string `json:"k"` 16 | Value any `json:"v"` 17 | DisplayValue string `json:"dv,omitempty"` 18 | } 19 | 20 | type HashReplaceItem struct { 21 | Key any `json:"k"` 22 | NewKey any `json:"nk"` 23 | Value any `json:"v"` 24 | DisplayValue string `json:"dv,omitempty"` 25 | } 26 | 27 | type SetEntryItem struct { 28 | Value any `json:"v"` 29 | DisplayValue string `json:"dv,omitempty"` 30 | } 31 | 32 | type ZSetEntryItem struct { 33 | Score float64 `json:"s"` 34 | Value string `json:"v"` 35 | DisplayValue string `json:"dv,omitempty"` 36 | } 37 | 38 | type ZSetReplaceItem struct { 39 | Score float64 `json:"s"` 40 | Value string `json:"v"` 41 | NewValue string `json:"nv"` 42 | DisplayValue string `json:"dv,omitempty"` 43 | } 44 | 45 | type StreamEntryItem struct { 46 | ID string `json:"id"` 47 | Value map[string]any `json:"v"` 48 | DisplayValue string `json:"dv,omitempty"` 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Terminal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/utils/monaco.js: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor' 2 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' 3 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' 4 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker' 5 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker' 6 | 7 | export const setupMonaco = () => { 8 | window.MonacoEnvironment = { 9 | getWorker: (_, label) => { 10 | switch (label) { 11 | case 'json': 12 | return new jsonWorker() 13 | case 'css': 14 | case 'scss': 15 | case 'less': 16 | return new cssWorker() 17 | case 'html': 18 | return new htmlWorker() 19 | default: 20 | return new editorWorker() 21 | } 22 | }, 23 | } 24 | 25 | // setup light theme 26 | monaco.editor.defineTheme('rdm-light', { 27 | base: 'vs', 28 | inherit: true, 29 | rules: [], 30 | colors: { 31 | 'editorLineNumber.foreground': '#BABBBD', 32 | 'editorLineNumber.activeForeground': '#777D83', 33 | }, 34 | }) 35 | 36 | // setup dark theme 37 | monaco.editor.defineTheme('rdm-dark', { 38 | base: 'vs-dark', 39 | inherit: true, 40 | rules: [], 41 | colors: {}, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/common/RedisTypeTag.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Help.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /backend/storage/local_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/vrischmann/userdir" 5 | "os" 6 | "path" 7 | ) 8 | 9 | // localStorage provides reading and writing application data to the user's 10 | // configuration directory. 11 | type localStorage struct { 12 | ConfPath string 13 | } 14 | 15 | // NewLocalStore returns a localStore instance. 16 | func NewLocalStore(filename string) *localStorage { 17 | return &localStorage{ 18 | ConfPath: path.Join(userdir.GetConfigHome(), "TinyRDM", filename), 19 | } 20 | } 21 | 22 | // Load reads the given file in the user's configuration directory and returns 23 | // its contents. 24 | func (l *localStorage) Load() ([]byte, error) { 25 | d, err := os.ReadFile(l.ConfPath) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return d, err 30 | } 31 | 32 | // Store writes data to the user's configuration directory at the given 33 | // filename. 34 | func (l *localStorage) Store(data []byte) error { 35 | dir := path.Dir(l.ConfPath) 36 | if err := ensureDirExists(dir); err != nil { 37 | return err 38 | } 39 | if err := os.WriteFile(l.ConfPath, data, 0777); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | // ensureDirExists checks for the existence of the directory at the given path, 46 | // which is created if it does not exist. 47 | func ensureDirExists(path string) error { 48 | _, err := os.Stat(path) 49 | if os.IsNotExist(err) { 50 | if err = os.Mkdir(path, 0777); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Unlink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/common/EditableTableColumn.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | 37 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Pub.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Timer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Log.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { createApp, nextTick } from 'vue' 3 | import App from './App.vue' 4 | import './styles/style.scss' 5 | import dayjs from 'dayjs' 6 | import duration from 'dayjs/plugin/duration' 7 | import relativeTime from 'dayjs/plugin/relativeTime' 8 | import { i18n } from '@/utils/i18n.js' 9 | import { setupDiscreteApi } from '@/utils/discrete.js' 10 | import usePreferencesStore from 'stores/preferences.js' 11 | import { loadEnvironment } from '@/utils/platform.js' 12 | import { setupMonaco } from '@/utils/monaco.js' 13 | 14 | dayjs.extend(duration) 15 | dayjs.extend(relativeTime) 16 | 17 | async function setupApp() { 18 | const app = createApp(App) 19 | app.use(i18n) 20 | app.use(createPinia()) 21 | 22 | await loadEnvironment() 23 | setupMonaco() 24 | const prefStore = usePreferencesStore() 25 | await prefStore.loadPreferences() 26 | await setupDiscreteApi() 27 | app.config.errorHandler = (err, instance, info) => { 28 | // TODO: add "send error message to author" later 29 | nextTick().then(() => { 30 | try { 31 | const content = err.toString() 32 | $notification.error(content, { 33 | title: i18n.global.t('common.error'), 34 | meta: 'Please see console output for more detail', 35 | }) 36 | console.error(err) 37 | } catch (e) {} 38 | }) 39 | } 40 | // app.config.warnHandler = (message) => { 41 | // console.warn(message) 42 | // } 43 | app.mount('#app') 44 | } 45 | 46 | setupApp() 47 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Database.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/components/icons/LoadList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewHashValue.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Conversion.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /backend/utils/string/key_convert.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "strconv" 5 | sliceutil "tinyrdm/backend/utils/slice" 6 | ) 7 | 8 | // EncodeRedisKey encode the redis key to integer array 9 | // if key contains binary which could not display on ui, convert the key to char array 10 | func EncodeRedisKey(key string) any { 11 | if containsBinary(key) { 12 | b := []byte(key) 13 | arr := make([]int, len(b)) 14 | for i, bb := range b { 15 | arr[i] = int(bb) 16 | } 17 | return arr 18 | } 19 | return key 20 | } 21 | 22 | // DecodeRedisKey decode redis key to readable string 23 | func DecodeRedisKey(key any) string { 24 | switch key.(type) { 25 | case string: 26 | return key.(string) 27 | 28 | case []any: 29 | arr := key.([]any) 30 | bytes := sliceutil.Map(arr, func(i int) byte { 31 | if c, ok := AnyToInt(arr[i]); ok { 32 | return byte(c) 33 | } 34 | return '0' 35 | }) 36 | return string(bytes) 37 | 38 | case []int: 39 | arr := key.([]int) 40 | b := make([]byte, len(arr)) 41 | for i, bb := range arr { 42 | b[i] = byte(bb) 43 | } 44 | return string(b) 45 | } 46 | return "" 47 | } 48 | 49 | // AnyToInt convert any value to int 50 | func AnyToInt(val any) (int, bool) { 51 | switch val.(type) { 52 | case string: 53 | num, err := strconv.Atoi(val.(string)) 54 | if err != nil { 55 | return 0, false 56 | } 57 | return num, true 58 | case float64: 59 | return int(val.(float64)), true 60 | case float32: 61 | return int(val.(float32)), true 62 | case int64: 63 | return int(val.(int64)), true 64 | case int32: 65 | return int(val.(int32)), true 66 | case int: 67 | return val.(int), true 68 | case bool: 69 | if val.(bool) { 70 | return 1, true 71 | } else { 72 | return 0, true 73 | } 74 | } 75 | return 0, false 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Close.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Clear.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/AddListValue.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/ConnectionPane.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | 43 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Structure.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/icons/ListView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Config.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/icons/TreeView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Connect.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /backend/utils/math/math_util.go: -------------------------------------------------------------------------------- 1 | package mathutil 2 | 3 | import ( 4 | "math" 5 | . "tinyrdm/backend/utils" 6 | ) 7 | 8 | // MaxWithIndex 查找所有元素中的最大值 9 | func MaxWithIndex[T Hashable](items ...T) (T, int) { 10 | selIndex := -1 11 | for i, t := range items { 12 | if selIndex < 0 { 13 | selIndex = i 14 | } else { 15 | if t > items[selIndex] { 16 | selIndex = i 17 | } 18 | } 19 | } 20 | return items[selIndex], selIndex 21 | } 22 | 23 | // MinWithIndex 查找所有元素中的最小值 24 | func MinWithIndex[T Hashable](items ...T) (T, int) { 25 | selIndex := -1 26 | for i, t := range items { 27 | if selIndex < 0 { 28 | selIndex = i 29 | } else { 30 | if t < items[selIndex] { 31 | selIndex = i 32 | } 33 | } 34 | } 35 | return items[selIndex], selIndex 36 | } 37 | 38 | // Clamp 返回限制在minVal和maxVal范围内的value 39 | func Clamp[T Hashable](value T, minVal T, maxVal T) T { 40 | if minVal > maxVal { 41 | minVal, maxVal = maxVal, minVal 42 | } 43 | if value < minVal { 44 | value = minVal 45 | } else if value > maxVal { 46 | value = maxVal 47 | } 48 | return value 49 | } 50 | 51 | // Abs 计算绝对值 52 | func Abs[T SignedNumber](val T) T { 53 | return T(math.Abs(float64(val))) 54 | } 55 | 56 | // Floor 向下取整 57 | func Floor[T SignedNumber | UnsignedNumber](val T) T { 58 | return T(math.Floor(float64(val))) 59 | } 60 | 61 | // Ceil 向上取整 62 | func Ceil[T SignedNumber | UnsignedNumber](val T) T { 63 | return T(math.Ceil(float64(val))) 64 | } 65 | 66 | // Round 四舍五入取整 67 | func Round[T SignedNumber | UnsignedNumber](val T) T { 68 | return T(math.Round(float64(val))) 69 | } 70 | 71 | // Sum 计算所有元素总和 72 | func Sum[T SignedNumber | UnsignedNumber](items ...T) T { 73 | var sum T 74 | for _, item := range items { 75 | sum += item 76 | } 77 | return sum 78 | } 79 | 80 | // Average 计算所有元素的平均值 81 | func Average[T SignedNumber | UnsignedNumber](items ...T) T { 82 | return Sum(items...) / T(len(items)) 83 | } 84 | -------------------------------------------------------------------------------- /frontend/src/components/common/SwitchButton.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Server.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewStreamValue.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /backend/types/preferences.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "tinyrdm/backend/consts" 4 | 5 | type Preferences struct { 6 | Behavior PreferencesBehavior `json:"behavior" yaml:"behavior"` 7 | General PreferencesGeneral `json:"general" yaml:"general"` 8 | Editor PreferencesEditor `json:"editor" yaml:"editor"` 9 | } 10 | 11 | func NewPreferences() Preferences { 12 | return Preferences{ 13 | Behavior: PreferencesBehavior{ 14 | AsideWidth: consts.DEFAULT_ASIDE_WIDTH, 15 | WindowWidth: consts.DEFAULT_WINDOW_WIDTH, 16 | WindowHeight: consts.DEFAULT_WINDOW_HEIGHT, 17 | }, 18 | General: PreferencesGeneral{ 19 | Theme: "auto", 20 | Language: "auto", 21 | FontSize: consts.DEFAULT_FONT_SIZE, 22 | ScanSize: consts.DEFAULT_SCAN_SIZE, 23 | CheckUpdate: true, 24 | }, 25 | Editor: PreferencesEditor{ 26 | FontSize: consts.DEFAULT_FONT_SIZE, 27 | }, 28 | } 29 | } 30 | 31 | type PreferencesBehavior struct { 32 | AsideWidth int `json:"asideWidth" yaml:"aside_width"` 33 | WindowWidth int `json:"windowWidth" yaml:"window_width"` 34 | WindowHeight int `json:"windowHeight" yaml:"window_height"` 35 | WindowMaximised bool `json:"windowMaximised" yaml:"window_maximised"` 36 | } 37 | 38 | type PreferencesGeneral struct { 39 | Theme string `json:"theme" yaml:"theme"` 40 | Language string `json:"language" yaml:"language"` 41 | Font string `json:"font" yaml:"font,omitempty"` 42 | FontSize int `json:"fontSize" yaml:"font_size"` 43 | ScanSize int `json:"scanSize" yaml:"scan_size"` 44 | UseSysProxy bool `json:"useSysProxy" yaml:"use_sys_proxy,omitempty"` 45 | UseSysProxyHttp bool `json:"useSysProxyHttp" yaml:"use_sys_proxy_http,omitempty"` 46 | CheckUpdate bool `json:"checkUpdate" yaml:"check_update"` 47 | SkipVersion string `json:"skipVersion" yaml:"skip_version,omitempty"` 48 | } 49 | 50 | type PreferencesEditor struct { 51 | Font string `json:"font" yaml:"font,omitempty"` 52 | FontSize int `json:"fontSize" yaml:"font_size"` 53 | } 54 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Tiny RDM Contribute Guide 2 | 3 | ### Multi-language Contributions 4 | 5 | #### Adding New Language 6 | 7 | 1. New file: Add a new JSON file in the [frontend/src/langs](../frontend/src/langs/), with the file naming format is " 8 | {language}-{region}.json", e.g. English is "en-us.json", simplified Chinese is "zh-cn.json". Highly recommended to duplicate the [en-us.json](../frontend/src/langs/en-us.json) file and rename it. 9 | 2. Fill content: Refer to [en-us.json](../frontend/src/langs/en-us.json), or duplicate the file and modify the language content. 10 | 3. Update codes: Edit[frontend/src/langs/index.js](.../frontend/src/langs/index.js), import the new language data inside. 11 | ```javascript 12 | import en from './en-us' 13 | // import your new localize file 'zh-cn' here 14 | import zh from './zh-cn' 15 | 16 | export const lang = { 17 | en, 18 | // export new language data 'zh' here 19 | zh, 20 | } 21 | ``` 22 | 4. Submit review once there are no issues with the translation context in the application. (learn how to submit) 23 | 24 | ### Code Submission`(To be completed)` 25 | 26 | #### Pull Request Title 27 | The format of PR's title like ": " 28 | - type: PR type 29 | - description: PR description 30 | 31 | PR type list below: 32 | 33 | | type | description | 34 | |----------|----------------------------------------------------| 35 | | revert | Revert a commit | 36 | | feat | New features | 37 | | perf | Performance improvements | 38 | | fix | Fix any bugs | 39 | | style | Style updates | 40 | | docs | Document updates | 41 | | refactor | Code refactors | 42 | | chore | Some chores | 43 | | ci | Automation process configuration or script updates | 44 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Github.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Folder.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/content_value/FormatSelector.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module tinyrdm 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/adrg/sysfont v0.1.2 7 | github.com/andybalholm/brotli v1.0.6 8 | github.com/google/uuid v1.4.0 9 | github.com/klauspost/compress v1.17.3 10 | github.com/redis/go-redis/v9 v9.3.0 11 | github.com/vrischmann/userdir v0.0.0-20151206171402-20f291cebd68 12 | github.com/wailsapp/wails/v2 v2.6.0 13 | golang.org/x/crypto v0.15.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/adrg/strutil v0.3.0 // indirect 19 | github.com/adrg/xdg v0.4.0 // indirect 20 | github.com/bep/debounce v1.2.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 23 | github.com/go-ole/go-ole v1.3.0 // indirect 24 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/labstack/echo/v4 v4.11.1 // indirect 27 | github.com/labstack/gommon v0.4.0 // indirect 28 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 29 | github.com/leaanthony/gosod v1.0.3 // indirect 30 | github.com/leaanthony/slicer v1.6.0 // indirect 31 | github.com/mattn/go-colorable v0.1.13 // indirect 32 | github.com/mattn/go-isatty v0.0.19 // indirect 33 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect 34 | github.com/pkg/errors v0.9.1 // indirect 35 | github.com/rivo/uniseg v0.4.4 // indirect 36 | github.com/rogpeppe/go-internal v1.6.1 // indirect 37 | github.com/samber/lo v1.38.1 // indirect 38 | github.com/stretchr/testify v1.8.4 // indirect 39 | github.com/tkrajina/go-reflector v0.5.6 // indirect 40 | github.com/valyala/bytebufferpool v1.0.0 // indirect 41 | github.com/valyala/fasttemplate v1.2.2 // indirect 42 | github.com/wailsapp/go-webview2 v1.0.10 // indirect 43 | github.com/wailsapp/mimetype v1.4.1 // indirect 44 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect 45 | golang.org/x/net v0.18.0 // indirect 46 | golang.org/x/sys v0.14.0 // indirect 47 | golang.org/x/text v0.14.0 // indirect 48 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 49 | ) 50 | 51 | // replace github.com/wailsapp/wails/v2 v2.6.0 => ~/go/pkg/mod 52 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/NewZSetValue.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/AboutDialog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/icons/CopyLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Detail.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/src/utils/rgb.js: -------------------------------------------------------------------------------- 1 | import { padStart, size, startsWith } from 'lodash' 2 | 3 | /** 4 | * @typedef {Object} RGB 5 | * @property {number} r 6 | * @property {number} g 7 | * @property {number} b 8 | * @property {number} [a] 9 | */ 10 | 11 | /** 12 | * parse hex color to rgb object 13 | * @param hex 14 | * @return {RGB} 15 | */ 16 | export function parseHexColor(hex) { 17 | if (size(hex) < 6) { 18 | return { r: 0, g: 0, b: 0 } 19 | } 20 | if (startsWith(hex, '#')) { 21 | hex = hex.slice(1) 22 | } 23 | const bigint = parseInt(hex, 16) 24 | const r = (bigint >> 16) & 255 25 | const g = (bigint >> 8) & 255 26 | const b = bigint & 255 27 | return { r, g, b } 28 | } 29 | 30 | /** 31 | * do gamma correction with an RGB object 32 | * @param {RGB} rgb 33 | * @param {Number} gamma 34 | * @return {RGB} 35 | */ 36 | export function hexGammaCorrection(rgb, gamma) { 37 | if (typeof rgb !== 'object') { 38 | return { r: 0, g: 0, b: 0 } 39 | } 40 | return { 41 | r: Math.max(0, Math.min(255, Math.round(rgb.r * gamma))), 42 | g: Math.max(0, Math.min(255, Math.round(rgb.g * gamma))), 43 | b: Math.max(0, Math.min(255, Math.round(rgb.b * gamma))), 44 | } 45 | } 46 | 47 | /** 48 | * mix two colors 49 | * @param rgba1 50 | * @param rgba2 51 | * @param weight 52 | * @return {{a: number, r: number, b: number, g: number}} 53 | */ 54 | export function mixColors(rgba1, rgba2, weight = 0.5) { 55 | if (rgba1.a === undefined) { 56 | rgba1.a = 255 57 | } 58 | if (rgba2.a === undefined) { 59 | rgba2.a = 255 60 | } 61 | return { 62 | r: Math.floor(rgba1.r * (1 - weight) + rgba2.r * weight), 63 | g: Math.floor(rgba1.g * (1 - weight) + rgba2.g * weight), 64 | b: Math.floor(rgba1.b * (1 - weight) + rgba2.b * weight), 65 | a: Math.floor(rgba1.a * (1 - weight) + rgba2.a * weight), 66 | } 67 | } 68 | 69 | /** 70 | * RGB object to hex color string 71 | * @param {RGB} rgb 72 | * @return {string} 73 | */ 74 | export function toHexColor(rgb) { 75 | return ( 76 | '#' + 77 | padStart(rgb.r.toString(16), 2, '0') + 78 | padStart(rgb.g.toString(16), 2, '0') + 79 | padStart(rgb.b.toString(16), 2, '0') 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Status.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/AddHashValue.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /frontend/src/components/common/IconButton.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash' 2 | 3 | /** 4 | * 5 | * @type import('naive-ui').GlobalThemeOverrides 6 | */ 7 | export const themeOverrides = { 8 | common: { 9 | primaryColor: '#D33A31', 10 | primaryColorHover: '#FF6B6B', 11 | primaryColorPressed: '#D5271C', 12 | primaryColorSuppl: '#FF6B6B', 13 | borderRadius: '4px', 14 | borderRadiusSmall: '3px', 15 | heightMedium: '32px', 16 | lineHeight: 1.5, 17 | scrollbarWidth: '8px', 18 | tabColor: '#FFFFFF', 19 | }, 20 | Button: { 21 | heightMedium: '32px', 22 | }, 23 | Tag: { 24 | // borderRadius: '3px' 25 | heightLarge: '32px', 26 | }, 27 | Input: { 28 | heightMedium: '32px', 29 | }, 30 | Tabs: { 31 | tabGapSmallCard: '2px', 32 | tabGapMediumCard: '2px', 33 | tabGapLargeCard: '2px', 34 | tabFontWeightActive: 450, 35 | }, 36 | Card: { 37 | colorEmbedded: '#FAFAFA', 38 | }, 39 | Form: { 40 | labelFontSizeTopSmall: '12px', 41 | labelFontSizeTopMedium: '13px', 42 | labelFontSizeTopLarge: '13px', 43 | labelHeightSmall: '18px', 44 | labelHeightMedium: '18px', 45 | labelHeightLarge: '18px', 46 | labelPaddingVertical: '0 0 5px 2px', 47 | feedbackHeightSmall: '18px', 48 | feedbackHeightMedium: '18px', 49 | feedbackHeightLarge: '20px', 50 | feedbackFontSizeSmall: '11px', 51 | feedbackFontSizeMedium: '12px', 52 | feedbackFontSizeLarge: '12px', 53 | labelTextColor: 'rgb(113,120,128)', 54 | labelFontWeight: '450', 55 | }, 56 | Radio: { 57 | buttonColorActive: '#D13B37', 58 | buttonTextColorActive: '#FFF', 59 | }, 60 | DataTable: { 61 | thPaddingSmall: '6px 8px', 62 | tdPaddingSmall: '6px 8px', 63 | }, 64 | } 65 | 66 | /** 67 | * 68 | * @type import('naive-ui').GlobalThemeOverrides 69 | */ 70 | const _darkThemeOverrides = { 71 | common: { 72 | bodyColor: '#1E1E1E', 73 | tabColor: '#1E1E1E', 74 | borderColor: '#515151', 75 | }, 76 | Tree: { 77 | nodeTextColor: '#CECED0', 78 | }, 79 | Card: { 80 | colorEmbedded: '#212121', 81 | }, 82 | Dropdown: { 83 | color: '#272727', 84 | }, 85 | } 86 | 87 | export const darkThemeOverrides = merge({}, themeOverrides, _darkThemeOverrides) 88 | -------------------------------------------------------------------------------- /frontend/src/components/icons/Cluster.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/components/content_value/ContentSearchInput.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/RenameKeyDialog.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /backend/services/ga_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/google/uuid" 7 | "net/http" 8 | "runtime" 9 | "strings" 10 | "sync" 11 | "tinyrdm/backend/storage" 12 | ) 13 | 14 | // google analytics service 15 | type gaService struct { 16 | measurementID string 17 | secretKey string 18 | clientID string 19 | } 20 | 21 | type GaDataItem struct { 22 | ClientID string `json:"client_id"` 23 | Events []GaEventItem `json:"events"` 24 | } 25 | 26 | type GaEventItem struct { 27 | Name string `json:"name"` 28 | Params map[string]any `json:"params"` 29 | } 30 | 31 | var ga *gaService 32 | var onceGA sync.Once 33 | 34 | func GA() *gaService { 35 | if ga == nil { 36 | onceGA.Do(func() { 37 | // get or create an unique user id 38 | st := storage.NewLocalStore("device.txt") 39 | uidByte, err := st.Load() 40 | if err != nil { 41 | uidByte = []byte(strings.ReplaceAll(uuid.NewString(), "-", "")) 42 | st.Store(uidByte) 43 | } 44 | 45 | ga = &gaService{ 46 | clientID: string(uidByte), 47 | } 48 | }) 49 | } 50 | return ga 51 | } 52 | 53 | func (a *gaService) SetSecretKey(measurementID, secretKey string) { 54 | a.measurementID = measurementID 55 | a.secretKey = secretKey 56 | } 57 | 58 | func (a *gaService) isValid() bool { 59 | return len(a.measurementID) > 0 && len(a.secretKey) > 0 60 | } 61 | 62 | func (a *gaService) sendEvent(events ...GaEventItem) error { 63 | body, err := json.Marshal(GaDataItem{ 64 | ClientID: a.clientID, 65 | Events: events, 66 | }) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | //url := "https://www.google-analytics.com/debug/mp/collect" 72 | url := "https://www.google-analytics.com/mp/collect" 73 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | q := req.URL.Query() 79 | q.Add("measurement_id", a.measurementID) 80 | q.Add("api_secret", a.secretKey) 81 | req.URL.RawQuery = q.Encode() 82 | 83 | response, err := http.DefaultClient.Do(req) 84 | if err != nil { 85 | return err 86 | } 87 | defer response.Body.Close() 88 | 89 | //if dump, err := httputil.DumpResponse(response, true); err == nil { 90 | // log.Println(string(dump)) 91 | //} 92 | 93 | return nil 94 | } 95 | 96 | // Startup sends application startup event 97 | func (a *gaService) Startup(version string) { 98 | if !a.isValid() { 99 | return 100 | } 101 | 102 | go a.sendEvent(GaEventItem{ 103 | Name: "startup", 104 | Params: map[string]any{ 105 | "os": runtime.GOOS, 106 | "arch": runtime.GOARCH, 107 | "version": version, 108 | }, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

English | 简体中文

2 |
3 | 4 |
5 |

Tiny RDM

6 |
7 | 8 | [![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE) 9 | [![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases) 10 | [![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers) 11 | [![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork) 12 | [![Discord](https://img.shields.io/discord/1170373259133456434?label=Discord&color=5865F2)](https://discord.gg/VTFbBMGjWh) 13 | [![X](https://img.shields.io/badge/Twitter-black?logo=x&logoColor=white)](https://twitter.com/Lykin53448) 14 | 15 | 一个现代化轻量级的跨平台Redis桌面客户端,支持Mac、Windows和Linux 16 |
17 | 18 | 19 | 20 | 21 | screenshot 22 | 23 | 24 | ## 功能特性 25 | 26 | * 极度轻量,基于Webview2,无内嵌浏览器(感谢[Wails](https://github.com/wailsapp/wails)) 27 | * 更精美的界面,无边框窗口,提供浅色/深色主题(感谢[Naive UI](https://github.com/tusen-ai/naive-ui) 28 | 和 [IconPark](https://iconpark.oceanengine.com)) 29 | * 多国语言支持:英文/中文([需要更多语言支持?点我贡献语言](.github/CONTRIBUTING_zh.md)) 30 | * 更好用的连接管理:支持SSH隧道/SSL/哨兵模式/集群模式 31 | * 可视化键值操作,增删查改一应俱全 32 | * 支持多种数据查看格式以及转码/解压方式 33 | * 采用SCAN分段加载,可轻松处理数百万键列表 34 | * 操作命令执行日志展示 35 | * 提供命令行操作 36 | * 提供慢日志展示 37 | * List/Hash/Set/Sorted Set的分段加载和查询 38 | * List/Hash/Set/Sorted Set值的转码显示 39 | 40 | ## 未来版本规划 41 | - [ ] 命令实时监控 42 | - [ ] 发布/订阅支持 43 | - [ ] 引入Monaco Editor 44 | - [ ] 连接配置导入/导出 45 | - [ ] 数据导入/导出 46 | 47 | ## 安装 48 | 49 | 提供Mac、Windows和Linux安装包,可[免费下载](https://github.com/tiny-craft/tiny-rdm/releases)。 50 | 51 | > 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可: 52 | > ``` shell 53 | > sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app 54 | > ``` 55 | 56 | ## 构建项目 57 | ### 运行环境要求 58 | * Go(最新版本) 59 | * Node.js >= 16 60 | * NPM >= 9 61 | 62 | ### 安装wails 63 | ```bash 64 | go install github.com/wailsapp/wails/v2/cmd/wails@latest 65 | ``` 66 | 67 | ### 拉取代码 68 | ```bash 69 | git clone https://github.com/tiny-craft/tiny-rdm --depth=1 70 | ``` 71 | 72 | ### 构建前端代码 73 | ```bash 74 | npm install --prefix ./frontend 75 | ``` 76 | 77 | ### 编译运行开发版本 78 | ```bash 79 | wails dev 80 | ``` 81 | 82 | ## 关于 83 | 此APP由我个人开发,也作为本人第一个开源项目的尝试,由于精力有限,可能会存在BUG或者使用体验上的问题,欢迎提交issue和PR。 84 | 同时本人也在探索开源代码、独立开发和盈利性商业应用之间的平衡关系,欢迎有共同意向的小伙伴加入群聊探讨和交换想法。 85 | * QQ群:831077639 86 | 87 | ## 开源许可 88 | 89 | Tiny RDM 基于 [GNU General Public](/LICENSE) 开源协议. 90 | -------------------------------------------------------------------------------- /frontend/src/components/new_value/AddZSetValue.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /backend/services/system_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "github.com/wailsapp/wails/v2/pkg/runtime" 6 | "log" 7 | "sync" 8 | "time" 9 | "tinyrdm/backend/consts" 10 | "tinyrdm/backend/types" 11 | ) 12 | 13 | type systemService struct { 14 | ctx context.Context 15 | } 16 | 17 | var system *systemService 18 | var onceSystem sync.Once 19 | 20 | func System() *systemService { 21 | if system == nil { 22 | onceSystem.Do(func() { 23 | system = &systemService{} 24 | go system.loopWindowEvent() 25 | }) 26 | } 27 | return system 28 | } 29 | 30 | func (s *systemService) Start(ctx context.Context) { 31 | s.ctx = ctx 32 | 33 | // maximize the window if screen size is lower than the minimum window size 34 | if screen, err := runtime.ScreenGetAll(ctx); err == nil && len(screen) > 0 { 35 | for _, sc := range screen { 36 | if sc.IsCurrent { 37 | if sc.Size.Width < consts.MIN_WINDOW_WIDTH || sc.Size.Height < consts.MIN_WINDOW_HEIGHT { 38 | runtime.WindowMaximise(ctx) 39 | break 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | // SelectFile open file dialog to select a file 47 | func (s *systemService) SelectFile(title string) (resp types.JSResp) { 48 | filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{ 49 | Title: title, 50 | ShowHiddenFiles: true, 51 | }) 52 | if err != nil { 53 | log.Println(err) 54 | resp.Msg = err.Error() 55 | return 56 | } 57 | resp.Success = true 58 | resp.Data = map[string]any{ 59 | "path": filepath, 60 | } 61 | return 62 | } 63 | 64 | func (s *systemService) loopWindowEvent() { 65 | var fullscreen, maximised, minimised, normal bool 66 | var width, height int 67 | var dirty bool 68 | for { 69 | time.Sleep(300 * time.Millisecond) 70 | if s.ctx == nil { 71 | continue 72 | } 73 | 74 | dirty = false 75 | if f := runtime.WindowIsFullscreen(s.ctx); f != fullscreen { 76 | // full-screen switched 77 | fullscreen = f 78 | dirty = true 79 | } 80 | 81 | if w, h := runtime.WindowGetSize(s.ctx); w != width || h != height { 82 | // window size changed 83 | width, height = w, h 84 | dirty = true 85 | } 86 | 87 | if m := runtime.WindowIsMaximised(s.ctx); m != maximised { 88 | maximised = m 89 | dirty = true 90 | } 91 | 92 | if m := runtime.WindowIsMinimised(s.ctx); m != minimised { 93 | minimised = m 94 | dirty = true 95 | } 96 | 97 | if n := runtime.WindowIsNormal(s.ctx); n != normal { 98 | normal = n 99 | dirty = true 100 | } 101 | 102 | if dirty { 103 | runtime.EventsEmit(s.ctx, "window_changed", map[string]any{ 104 | "fullscreen": fullscreen, 105 | "width": width, 106 | "height": height, 107 | "maximised": maximised, 108 | "minimised": minimised, 109 | "normal": normal, 110 | }) 111 | 112 | if !fullscreen && !minimised { 113 | // save window size 114 | Preferences().SaveWindowSize(width, height, maximised) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/components/common/DropdownSelector.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 96 | 97 | 112 | -------------------------------------------------------------------------------- /frontend/src/styles/style.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | //--bg-color: #f8f8f8; 3 | //--bg-color-accent: #fff; 4 | //--bg-color-page: #f2f3f5; 5 | //--text-color-regular: #606266; 6 | //--border-color: #dcdfe6; 7 | --transition-duration-fast: 0.2s; 8 | --transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1); 9 | } 10 | 11 | html { 12 | //text-align: center; 13 | cursor: default; 14 | -webkit-user-select: none; /* Chrome, Safari */ 15 | -moz-user-select: none; /* Firefox */ 16 | user-select: none; 17 | } 18 | 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | background-color: #0000; 23 | line-height: 1.5; 24 | font-family: v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 25 | overflow: hidden; 26 | } 27 | 28 | #app { 29 | height: 100vh; 30 | } 31 | 32 | .flex-box { 33 | display: flex; 34 | } 35 | 36 | .flex-box-v { 37 | @extend .flex-box; 38 | flex-direction: column; 39 | } 40 | 41 | .flex-box-h { 42 | @extend .flex-box; 43 | flex-direction: row; 44 | } 45 | 46 | .flex-item { 47 | flex: 0 0 auto; 48 | } 49 | 50 | .flex-item-expand { 51 | flex-grow: 1; 52 | } 53 | 54 | .clickable { 55 | cursor: pointer; 56 | } 57 | 58 | .icon-btn { 59 | @extend .clickable; 60 | line-height: 100%; 61 | } 62 | 63 | .ellipsis { 64 | white-space: nowrap; /* 禁止文本换行 */ 65 | overflow: hidden; /* 隐藏超出容器的文本 */ 66 | text-overflow: ellipsis; /* 使用省略号表示被截断的文本 */ 67 | } 68 | 69 | .unit-item { 70 | margin-left: 10px; 71 | } 72 | 73 | .fill-height { 74 | height: 100%; 75 | } 76 | 77 | .text-block { 78 | white-space: pre-line; 79 | } 80 | 81 | .content-wrapper { 82 | height: 100%; 83 | flex-grow: 1; 84 | overflow: hidden; 85 | gap: 5px; 86 | padding-top: 5px; 87 | //padding: 5px; 88 | box-sizing: border-box; 89 | position: relative; 90 | 91 | .tb2 { 92 | gap: 5px; 93 | justify-content: flex-end; 94 | align-items: center; 95 | } 96 | 97 | .value-wrapper { 98 | //border-top: v-bind('themeVars.borderColor') 1px solid; 99 | user-select: text; 100 | //height: 100%; 101 | box-sizing: border-box; 102 | } 103 | 104 | .value-item-part { 105 | padding: 0 5px; 106 | } 107 | 108 | .value-footer { 109 | align-items: center; 110 | gap: 0; 111 | padding: 3px 10px 3px 10px; 112 | height: 30px; 113 | } 114 | } 115 | 116 | .n-dynamic-input-item { 117 | align-items: center; 118 | gap: 10px; 119 | } 120 | 121 | .n-tree-node-content__text { 122 | @extend .ellipsis; 123 | } 124 | 125 | .context-menu-item { 126 | min-width: 100px; 127 | padding-right: 10px; 128 | } 129 | 130 | .nav-pane-container { 131 | overflow: hidden; 132 | 133 | .nav-pane-bottom { 134 | align-items: center; 135 | gap: 8px; 136 | padding: 3px 10px 3px 10px; 137 | min-height: 30px; 138 | //border-top: v-bind('themeVars.borderColor') 1px solid; 139 | } 140 | } 141 | 142 | .n-modal-mask { 143 | --wails-draggable: drag; 144 | } 145 | 146 | .n-tabs .n-tabs-nav { 147 | line-height: 1.3; 148 | } 149 | -------------------------------------------------------------------------------- /backend/utils/redis/log_hook.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "log" 8 | "net" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | type execCallback func(string, int64) 14 | 15 | type LogHook struct { 16 | name string 17 | cmdExec execCallback 18 | } 19 | 20 | func NewHook(name string, cmdExec execCallback) *LogHook { 21 | return &LogHook{ 22 | name: name, 23 | cmdExec: cmdExec, 24 | } 25 | } 26 | 27 | func appendArg(b []byte, v interface{}) []byte { 28 | switch v := v.(type) { 29 | case nil: 30 | return append(b, ""...) 31 | case string: 32 | return append(b, []byte(v)...) 33 | case []byte: 34 | return append(b, v...) 35 | case int: 36 | return strconv.AppendInt(b, int64(v), 10) 37 | case int8: 38 | return strconv.AppendInt(b, int64(v), 10) 39 | case int16: 40 | return strconv.AppendInt(b, int64(v), 10) 41 | case int32: 42 | return strconv.AppendInt(b, int64(v), 10) 43 | case int64: 44 | return strconv.AppendInt(b, v, 10) 45 | case uint: 46 | return strconv.AppendUint(b, uint64(v), 10) 47 | case uint8: 48 | return strconv.AppendUint(b, uint64(v), 10) 49 | case uint16: 50 | return strconv.AppendUint(b, uint64(v), 10) 51 | case uint32: 52 | return strconv.AppendUint(b, uint64(v), 10) 53 | case uint64: 54 | return strconv.AppendUint(b, v, 10) 55 | case float32: 56 | return strconv.AppendFloat(b, float64(v), 'f', -1, 64) 57 | case float64: 58 | return strconv.AppendFloat(b, v, 'f', -1, 64) 59 | case bool: 60 | if v { 61 | return append(b, "true"...) 62 | } 63 | return append(b, "false"...) 64 | case time.Time: 65 | return v.AppendFormat(b, time.RFC3339Nano) 66 | default: 67 | return append(b, fmt.Sprint(v)...) 68 | } 69 | } 70 | 71 | func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook { 72 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 73 | return next(ctx, network, addr) 74 | } 75 | } 76 | 77 | func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 78 | return func(ctx context.Context, cmd redis.Cmder) error { 79 | t := time.Now() 80 | err := next(ctx, cmd) 81 | b := make([]byte, 0, 64) 82 | for i, arg := range cmd.Args() { 83 | if i > 0 { 84 | b = append(b, ' ') 85 | } 86 | b = appendArg(b, arg) 87 | } 88 | log.Println(string(b)) 89 | if l.cmdExec != nil { 90 | l.cmdExec(string(b), time.Since(t).Milliseconds()) 91 | } 92 | return err 93 | } 94 | } 95 | 96 | func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 97 | return func(ctx context.Context, cmds []redis.Cmder) error { 98 | t := time.Now() 99 | err := next(ctx, cmds) 100 | cost := time.Since(t).Milliseconds() 101 | b := make([]byte, 0, 64) 102 | for i, cmd := range cmds { 103 | log.Println("pipeline: ", cmd) 104 | if l.cmdExec != nil { 105 | for i, arg := range cmd.Args() { 106 | if i > 0 { 107 | b = append(b, ' ') 108 | } 109 | b = appendArg(b, arg) 110 | } 111 | if i != len(cmds) { 112 | b = append(b, '\n') 113 | } 114 | } 115 | } 116 | if l.cmdExec != nil { 117 | l.cmdExec(string(b), cost) 118 | } 119 | return err 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /frontend/src/components/common/ResizeableWrapper.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 94 | 95 | 123 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /backend/types/js_resp.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type JSResp struct { 4 | Success bool `json:"success"` 5 | Msg string `json:"msg"` 6 | Data any `json:"data,omitempty"` 7 | } 8 | 9 | type KeySummaryParam struct { 10 | Server string `json:"server"` 11 | DB int `json:"db"` 12 | Key any `json:"key"` 13 | } 14 | 15 | type KeySummary struct { 16 | Type string `json:"type"` 17 | TTL int64 `json:"ttl"` 18 | Size int64 `json:"size"` 19 | Length int64 `json:"length"` 20 | } 21 | 22 | type KeyDetailParam struct { 23 | Server string `json:"server"` 24 | DB int `json:"db"` 25 | Key any `json:"key"` 26 | Format string `json:"format,omitempty"` 27 | Decode string `json:"decode,omitempty"` 28 | MatchPattern string `json:"matchPattern,omitempty"` 29 | Reset bool `json:"reset"` 30 | Full bool `json:"full"` 31 | } 32 | 33 | type KeyDetail struct { 34 | Value any `json:"value"` 35 | Length int64 `json:"length,omitempty"` 36 | Format string `json:"format,omitempty"` 37 | Decode string `json:"decode,omitempty"` 38 | Match string `json:"match,omitempty"` 39 | Reset bool `json:"reset"` 40 | End bool `json:"end"` 41 | } 42 | 43 | type SetKeyParam struct { 44 | Server string `json:"server"` 45 | DB int `json:"db"` 46 | Key any `json:"key"` 47 | KeyType string `json:"keyType"` 48 | Value any `json:"value"` 49 | TTL int64 `json:"ttl"` 50 | Format string `json:"format,omitempty"` 51 | Decode string `json:"decode,omitempty"` 52 | } 53 | 54 | type SetListParam struct { 55 | Server string `json:"server"` 56 | DB int `json:"db"` 57 | Key any `json:"key"` 58 | Index int64 `json:"index"` 59 | Value any `json:"value"` 60 | Format string `json:"format,omitempty"` 61 | Decode string `json:"decode,omitempty"` 62 | RetFormat string `json:"retFormat,omitempty"` 63 | RetDecode string `json:"retDecode,omitempty"` 64 | } 65 | 66 | type SetHashParam struct { 67 | Server string `json:"server"` 68 | DB int `json:"db"` 69 | Key any `json:"key"` 70 | Field string `json:"field,omitempty"` 71 | NewField string `json:"newField,omitempty"` 72 | Value any `json:"value"` 73 | Format string `json:"format,omitempty"` 74 | Decode string `json:"decode,omitempty"` 75 | RetFormat string `json:"retFormat,omitempty"` 76 | RetDecode string `json:"retDecode,omitempty"` 77 | } 78 | 79 | type SetSetParam struct { 80 | Server string `json:"server"` 81 | DB int `json:"db"` 82 | Key any `json:"key"` 83 | Value any `json:"value"` 84 | NewValue any `json:"newValue"` 85 | Format string `json:"format,omitempty"` 86 | Decode string `json:"decode,omitempty"` 87 | RetFormat string `json:"retFormat,omitempty"` 88 | RetDecode string `json:"retDecode,omitempty"` 89 | } 90 | 91 | type SetZSetParam struct { 92 | Server string `json:"server"` 93 | DB int `json:"db"` 94 | Key any `json:"key"` 95 | Value any `json:"value"` 96 | NewValue any `json:"newValue"` 97 | Score float64 `json:"score"` 98 | Format string `json:"format,omitempty"` 99 | Decode string `json:"decode,omitempty"` 100 | RetFormat string `json:"retFormat,omitempty"` 101 | RetDecode string `json:"retDecode,omitempty"` 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/components/common/ToolbarControlWidget.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 74 | 75 | 110 | -------------------------------------------------------------------------------- /backend/utils/string/any_convert.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | sliceutil "tinyrdm/backend/utils/slice" 7 | ) 8 | 9 | func AnyToString(value interface{}, prefix string, layer int) (s string) { 10 | if value == nil { 11 | return 12 | } 13 | 14 | switch value.(type) { 15 | case float64: 16 | ft := value.(float64) 17 | s = strconv.FormatFloat(ft, 'f', -1, 64) 18 | case float32: 19 | ft := value.(float32) 20 | s = strconv.FormatFloat(float64(ft), 'f', -1, 64) 21 | case int: 22 | it := value.(int) 23 | s = strconv.Itoa(it) 24 | case uint: 25 | it := value.(uint) 26 | s = strconv.Itoa(int(it)) 27 | case int8: 28 | it := value.(int8) 29 | s = strconv.Itoa(int(it)) 30 | case uint8: 31 | it := value.(uint8) 32 | s = strconv.Itoa(int(it)) 33 | case int16: 34 | it := value.(int16) 35 | s = strconv.Itoa(int(it)) 36 | case uint16: 37 | it := value.(uint16) 38 | s = strconv.Itoa(int(it)) 39 | case int32: 40 | it := value.(int32) 41 | s = strconv.Itoa(int(it)) 42 | case uint32: 43 | it := value.(uint32) 44 | s = strconv.Itoa(int(it)) 45 | case int64: 46 | it := value.(int64) 47 | s = strconv.FormatInt(it, 10) 48 | case uint64: 49 | it := value.(uint64) 50 | s = strconv.FormatUint(it, 10) 51 | case string: 52 | if layer > 0 { 53 | s = "\"" + value.(string) + "\"" 54 | } else { 55 | s = value.(string) 56 | } 57 | case bool: 58 | val, _ := value.(bool) 59 | if val { 60 | s = "True" 61 | } else { 62 | s = "False" 63 | } 64 | case []byte: 65 | s = prefix + string(value.([]byte)) 66 | case []string: 67 | ss := value.([]string) 68 | anyStr := sliceutil.Map(ss, func(i int) string { 69 | str := AnyToString(ss[i], prefix, layer+1) 70 | return prefix + strconv.Itoa(i+1) + ") " + str 71 | }) 72 | s = prefix + sliceutil.JoinString(anyStr, "\r\n") 73 | case []any: 74 | as := value.([]any) 75 | anyItems := sliceutil.Map(as, func(i int) string { 76 | str := AnyToString(as[i], prefix, layer+1) 77 | return prefix + strconv.Itoa(i+1) + ") " + str 78 | }) 79 | s = sliceutil.JoinString(anyItems, "\r\n") 80 | case map[any]any: 81 | am := value.(map[any]any) 82 | var items []string 83 | index := 0 84 | for k, v := range am { 85 | kk := prefix + strconv.Itoa(index+1) + ") " + AnyToString(k, prefix, layer+1) 86 | vv := prefix + strconv.Itoa(index+2) + ") " + AnyToString(v, "\t", layer+1) 87 | if layer > 0 { 88 | indent := layer 89 | if index == 0 { 90 | indent -= 1 91 | } 92 | for i := 0; i < indent; i++ { 93 | vv = " " + vv 94 | } 95 | } 96 | index += 2 97 | items = append(items, kk, vv) 98 | } 99 | s = sliceutil.JoinString(items, "\r\n") 100 | default: 101 | b, _ := json.Marshal(value) 102 | s = prefix + string(b) 103 | } 104 | 105 | return 106 | } 107 | 108 | //func AnyToHex(val any) (string, bool) { 109 | // var src string 110 | // switch val.(type) { 111 | // case string: 112 | // src = val.(string) 113 | // case []byte: 114 | // src = string(val.([]byte)) 115 | // } 116 | // 117 | // if len(src) <= 0 { 118 | // return "", false 119 | // } 120 | // 121 | // var output strings.Builder 122 | // for i := range src { 123 | // if !utf8.ValidString(src[i : i+1]) { 124 | // output.WriteString(fmt.Sprintf("\\x%02x", src[i:i+1])) 125 | // } else { 126 | // output.WriteString(src[i : i+1]) 127 | // } 128 | // } 129 | // 130 | // return output.String(), true 131 | //} 132 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "github.com/wailsapp/wails/v2" 7 | "github.com/wailsapp/wails/v2/pkg/menu" 8 | "github.com/wailsapp/wails/v2/pkg/options" 9 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 10 | "github.com/wailsapp/wails/v2/pkg/options/linux" 11 | "github.com/wailsapp/wails/v2/pkg/options/mac" 12 | "github.com/wailsapp/wails/v2/pkg/options/windows" 13 | runtime2 "github.com/wailsapp/wails/v2/pkg/runtime" 14 | "runtime" 15 | "tinyrdm/backend/consts" 16 | "tinyrdm/backend/services" 17 | ) 18 | 19 | //go:embed all:frontend/dist 20 | var assets embed.FS 21 | 22 | //go:embed build/appicon.png 23 | var icon []byte 24 | 25 | var version = "0.0.0" 26 | var gaMeasurementID, gaSecretKey string 27 | 28 | func main() { 29 | // Create an instance of the app structure 30 | sysSvc := services.System() 31 | connSvc := services.Connection() 32 | browserSvc := services.Browser() 33 | cliSvc := services.Cli() 34 | prefSvc := services.Preferences() 35 | prefSvc.SetAppVersion(version) 36 | windowWidth, windowHeight, maximised := prefSvc.GetWindowSize() 37 | windowStartState := options.Normal 38 | if maximised { 39 | windowStartState = options.Maximised 40 | } 41 | 42 | // menu 43 | appMenu := menu.NewMenu() 44 | if runtime.GOOS == "darwin" { 45 | appMenu.Append(menu.AppMenu()) 46 | appMenu.Append(menu.EditMenu()) 47 | appMenu.Append(menu.WindowMenu()) 48 | } 49 | 50 | // Create application with options 51 | err := wails.Run(&options.App{ 52 | Title: "Tiny RDM", 53 | Width: windowWidth, 54 | Height: windowHeight, 55 | MinWidth: consts.MIN_WINDOW_WIDTH, 56 | MinHeight: consts.MIN_WINDOW_HEIGHT, 57 | WindowStartState: windowStartState, 58 | Frameless: runtime.GOOS != "darwin", 59 | Menu: appMenu, 60 | EnableDefaultContextMenu: true, 61 | AssetServer: &assetserver.Options{ 62 | Assets: assets, 63 | }, 64 | BackgroundColour: options.NewRGBA(27, 38, 54, 0), 65 | StartHidden: true, 66 | OnStartup: func(ctx context.Context) { 67 | sysSvc.Start(ctx) 68 | connSvc.Start(ctx) 69 | browserSvc.Start(ctx) 70 | cliSvc.Start(ctx) 71 | 72 | services.GA().SetSecretKey(gaMeasurementID, gaSecretKey) 73 | services.GA().Startup(version) 74 | }, 75 | OnDomReady: func(ctx context.Context) { 76 | runtime2.WindowShow(ctx) 77 | }, 78 | OnShutdown: func(ctx context.Context) { 79 | browserSvc.Stop() 80 | cliSvc.CloseAll() 81 | }, 82 | Bind: []interface{}{ 83 | sysSvc, 84 | connSvc, 85 | browserSvc, 86 | cliSvc, 87 | prefSvc, 88 | }, 89 | Mac: &mac.Options{ 90 | TitleBar: mac.TitleBarHiddenInset(), 91 | About: &mac.AboutInfo{ 92 | Title: "Tiny RDM " + version, 93 | Message: "A modern lightweight cross-platform Redis desktop client.\n\nCopyright © 2023", 94 | Icon: icon, 95 | }, 96 | WebviewIsTransparent: false, 97 | WindowIsTranslucent: true, 98 | }, 99 | Windows: &windows.Options{ 100 | WebviewIsTransparent: true, 101 | WindowIsTranslucent: true, 102 | DisableFramelessWindowDecorations: true, 103 | }, 104 | Linux: &linux.Options{ 105 | Icon: icon, 106 | WebviewGpuPolicy: linux.WebviewGpuPolicyOnDemand, 107 | WindowIsTranslucent: true, 108 | }, 109 | }) 110 | 111 | if err != nil { 112 | println("Error:", err.Error()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /backend/storage/preferences.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v3" 6 | "log" 7 | "reflect" 8 | "strings" 9 | "sync" 10 | "tinyrdm/backend/consts" 11 | "tinyrdm/backend/types" 12 | ) 13 | 14 | type PreferencesStorage struct { 15 | storage *localStorage 16 | mutex sync.Mutex 17 | } 18 | 19 | func NewPreferences() *PreferencesStorage { 20 | return &PreferencesStorage{ 21 | storage: NewLocalStore("preferences.yaml"), 22 | } 23 | } 24 | 25 | func (p *PreferencesStorage) DefaultPreferences() types.Preferences { 26 | return types.NewPreferences() 27 | } 28 | 29 | func (p *PreferencesStorage) getPreferences() (ret types.Preferences) { 30 | b, err := p.storage.Load() 31 | if err != nil { 32 | ret = p.DefaultPreferences() 33 | return 34 | } 35 | 36 | if err = yaml.Unmarshal(b, &ret); err != nil { 37 | ret = p.DefaultPreferences() 38 | return 39 | } 40 | return 41 | } 42 | 43 | // GetPreferences Get preferences from local 44 | func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) { 45 | p.mutex.Lock() 46 | defer p.mutex.Unlock() 47 | 48 | ret = p.getPreferences() 49 | if ret.General.ScanSize <= 0 { 50 | ret.General.ScanSize = consts.DEFAULT_SCAN_SIZE 51 | } 52 | ret.Behavior.AsideWidth = max(ret.Behavior.AsideWidth, consts.DEFAULT_ASIDE_WIDTH) 53 | ret.Behavior.WindowWidth = max(ret.Behavior.WindowWidth, consts.MIN_WINDOW_WIDTH) 54 | ret.Behavior.WindowHeight = max(ret.Behavior.WindowHeight, consts.MIN_WINDOW_HEIGHT) 55 | return 56 | } 57 | 58 | func (p *PreferencesStorage) setPreferences(pf *types.Preferences, key string, value any) error { 59 | parts := strings.Split(key, ".") 60 | if len(parts) > 0 { 61 | var reflectValue reflect.Value 62 | if reflect.TypeOf(pf).Kind() == reflect.Ptr { 63 | reflectValue = reflect.ValueOf(pf).Elem() 64 | } else { 65 | reflectValue = reflect.ValueOf(pf) 66 | } 67 | for i, part := range parts { 68 | part = strings.ToUpper(part[:1]) + part[1:] 69 | reflectValue = reflectValue.FieldByName(part) 70 | if reflectValue.IsValid() { 71 | if i == len(parts)-1 { 72 | reflectValue.Set(reflect.ValueOf(value)) 73 | return nil 74 | } 75 | } else { 76 | break 77 | } 78 | } 79 | } 80 | 81 | return fmt.Errorf("invalid key path(%s)", key) 82 | } 83 | 84 | func (p *PreferencesStorage) savePreferences(pf *types.Preferences) error { 85 | b, err := yaml.Marshal(pf) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | if err = p.storage.Store(b); err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | // SetPreferences replace preferences 97 | func (p *PreferencesStorage) SetPreferences(pf *types.Preferences) error { 98 | p.mutex.Lock() 99 | defer p.mutex.Unlock() 100 | 101 | return p.savePreferences(pf) 102 | } 103 | 104 | // UpdatePreferences update values by key paths, the key path use "." to indicate multiple level 105 | func (p *PreferencesStorage) UpdatePreferences(values map[string]any) error { 106 | p.mutex.Lock() 107 | defer p.mutex.Unlock() 108 | 109 | pf := p.getPreferences() 110 | for path, v := range values { 111 | if err := p.setPreferences(&pf, path, v); err != nil { 112 | return err 113 | } 114 | } 115 | log.Println("after save", pf) 116 | 117 | return p.savePreferences(&pf) 118 | } 119 | 120 | func (p *PreferencesStorage) RestoreDefault() types.Preferences { 121 | p.mutex.Lock() 122 | defer p.mutex.Unlock() 123 | 124 | pf := p.DefaultPreferences() 125 | p.savePreferences(&pf) 126 | return pf 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

English | 简体中文

2 |
3 | 4 |
5 |

Tiny RDM

6 |
7 | 8 | [![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE) 9 | [![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases) 10 | [![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers) 11 | [![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork) 12 | [![Discord](https://img.shields.io/discord/1170373259133456434?label=Discord&color=5865F2)](https://discord.gg/VTFbBMGjWh) 13 | [![X](https://img.shields.io/badge/Twitter-black?logo=x&logoColor=white)](https://twitter.com/Lykin53448) 14 | 15 | Tiny RDM is a modern lightweight cross-platform Redis desktop manager available for Mac, Windows, and Linux. 16 |
17 | 18 | 19 | 20 | 21 | screenshot 22 | 23 | 24 | ## Feature 25 | 26 | * Super lightweight, built on Webview2, without embedded browsers (Thanks to [Wails](https://github.com/wailsapp/wails)). 27 | * More elegant UI, frameless, offering light and dark themes (Thanks to [Naive UI](https://github.com/tusen-ai/naive-ui) 28 | and [IconPark](https://iconpark.oceanengine.com)). 29 | * Multi-language support ([Need more languages ? Click here to contribute](.github/CONTRIBUTING.md)). 30 | * Better connection management: supports SSH Tunnel/SSL/Sentinel Mode/Cluster Mode. 31 | * Visualize key value operations, CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams. 32 | * Support multiple data viewing format and decode/decompression methods. 33 | * Use SCAN for segmented loading, making it easy to list millions of keys. 34 | * Operation command execution logs. 35 | * Provides command-line operations. 36 | * Provides slow logs. 37 | * Segmented loading and querying for List/Hash/Set/Sorted Set. 38 | * Decode/decompression display for value of List/Hash/Set/Sorted Set 39 | 40 | ## Roadmap 41 | - [ ] Real-time commands monitoring 42 | - [ ] Pub/Sub operations 43 | - [ ] Embedding Monaco Editor 44 | - [ ] Import/export connection profile 45 | - [ ] Import/export data 46 | 47 | ## Installation 48 | 49 | Available to download for free from [here](https://github.com/tiny-craft/tiny-rdm/releases). 50 | 51 | > If you can't open it after installation on macOS, exec the following command then reopen: 52 | > ``` shell 53 | > sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app 54 | > ``` 55 | 56 | ## Build Guidelines 57 | ### Prerequisites 58 | * Go (latest version) 59 | * Node.js >= 16 60 | * NPM >= 9 61 | 62 | ### Install wails 63 | ```bash 64 | go install github.com/wailsapp/wails/v2/cmd/wails@latest 65 | ``` 66 | 67 | ### Clone the code 68 | ```bash 69 | git clone https://github.com/tiny-craft/tiny-rdm --depth=1 70 | ``` 71 | 72 | ### Build frontend 73 | ```bash 74 | npm install --prefix ./frontend 75 | ``` 76 | 77 | ### Compile and run 78 | ```bash 79 | wails dev 80 | ``` 81 | 82 | ## License 83 | 84 | Tiny RDM is licensed under [GNU General Public](/LICENSE) license. 85 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/FlushDbDialog.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /backend/types/connection.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ConnectionCategory int 4 | 5 | type ConnectionConfig struct { 6 | Name string `json:"name" yaml:"name"` 7 | Group string `json:"group,omitempty" yaml:"-"` 8 | Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` 9 | Port int `json:"port,omitempty" yaml:"port,omitempty"` 10 | Username string `json:"username,omitempty" yaml:"username,omitempty"` 11 | Password string `json:"password,omitempty" yaml:"password,omitempty"` 12 | DefaultFilter string `json:"defaultFilter,omitempty" yaml:"default_filter,omitempty"` 13 | KeySeparator string `json:"keySeparator,omitempty" yaml:"key_separator,omitempty"` 14 | ConnTimeout int `json:"connTimeout,omitempty" yaml:"conn_timeout,omitempty"` 15 | ExecTimeout int `json:"execTimeout,omitempty" yaml:"exec_timeout,omitempty"` 16 | DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` 17 | DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` 18 | KeyView int `json:"keyView,omitempty" yaml:"key_view,omitempty"` 19 | LoadSize int `json:"loadSize,omitempty" yaml:"load_size,omitempty"` 20 | MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` 21 | SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` 22 | SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` 23 | Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"` 24 | Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"` 25 | } 26 | 27 | type Connection struct { 28 | ConnectionConfig `json:",inline" yaml:",inline"` 29 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 30 | Connections []Connection `json:"connections,omitempty" yaml:"connections,omitempty"` 31 | } 32 | 33 | type Connections []Connection 34 | 35 | type ConnectionDB struct { 36 | Name string `json:"name"` 37 | Index int `json:"index"` 38 | Keys int `json:"keys"` 39 | Expires int `json:"expires,omitempty"` 40 | AvgTTL int `json:"avgTtl,omitempty"` 41 | } 42 | 43 | type ConnectionSSL struct { 44 | Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` 45 | KeyFile string `json:"keyFile,omitempty" yaml:"keyFile,omitempty"` 46 | CertFile string `json:"certFile,omitempty" yaml:"certFile,omitempty"` 47 | CAFile string `json:"caFile,omitempty" yaml:"caFile,omitempty"` 48 | } 49 | 50 | type ConnectionSSH struct { 51 | Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` 52 | Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` 53 | Port int `json:"port,omitempty" yaml:"port,omitempty"` 54 | LoginType string `json:"loginType,omitempty" yaml:"login_type"` 55 | Username string `json:"username,omitempty" yaml:"username,omitempty"` 56 | Password string `json:"password,omitempty" yaml:"password,omitempty"` 57 | PKFile string `json:"pkFile,omitempty" yaml:"pk_file,omitempty"` 58 | Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"` 59 | } 60 | 61 | type ConnectionSentinel struct { 62 | Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` 63 | Master string `json:"master,omitempty" yaml:"master,omitempty"` 64 | Username string `json:"username,omitempty" yaml:"username,omitempty"` 65 | Password string `json:"password,omitempty" yaml:"password,omitempty"` 66 | } 67 | 68 | type ConnectionCluster struct { 69 | Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/SetTtlDialog.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/GroupDialog.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/KeyFilterDialog.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /frontend/src/components/sidebar/BrowserPane.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 100 | 101 | 107 | --------------------------------------------------------------------------------