├── .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 |
11 |
14 |
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 |
10 |
11 | emit('update:value', val)" />
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Filter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
19 |
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 |
8 |
9 |
{{ title }}
10 |
localhost:3306
11 |
12 |
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 |
11 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Key.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Binary.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/src/components/common/EditableTableRow.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ props.value }}
25 |
26 |
27 |
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 |
11 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Edit.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/More.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
22 |
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 |
11 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/AddGroup.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Layer.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/WindowClose.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/common/FileOpenInput.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 | ...
28 |
29 |
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 |
11 |
25 |
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 |
15 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Add.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
31 |
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 |
11 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Save.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Record.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Monitor.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Search.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Delete.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/FullScreen.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/OffScreen.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Sort.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Pin.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
30 |
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 |
27 |
28 |
29 |
30 | remove(index)" />
31 | create(index)" />
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Copy.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Update.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/src/components/new_value/NewSetValue.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
29 |
30 | remove(index)" />
31 | create(index)" />
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/components/content/ContentServerPane.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ $t('interface.new_conn') }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
46 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Code.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
37 |
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 |
19 |
43 |
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 |
29 |
35 | {{ props.type }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
52 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Help.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
30 |
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 |
11 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/frontend/src/components/common/EditableTableColumn.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 | {{ $t('dialogue.remove_tip', { name: props.bindKey }) }}
33 |
34 |
35 |
36 |
37 |
44 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Pub.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Timer.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Log.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
48 |
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 |
15 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/LoadList.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/frontend/src/components/new_value/NewHashValue.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
47 |
48 | remove(index)" />
49 | create(index)" />
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Conversion.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
49 |
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 |
15 |
35 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Clear.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/frontend/src/components/new_value/AddListValue.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 | emit('update:type', val)">
37 |
38 |
39 |
40 |
41 |
42 |
43 | remove(index)" />
44 | create(index)" />
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/ConnectionPane.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Structure.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/ListView.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Config.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/TreeView.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Connect.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
51 |
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 |
43 |
44 |
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 | {{ props.tTooltips ? $t(props.tTooltips[i]) : '' }}
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Server.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/frontend/src/components/new_value/NewStreamValue.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
53 |
54 | remove(index)" />
55 | create(index)" />
56 |
57 |
58 |
59 |
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 |
11 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Folder.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
34 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/frontend/src/components/content_value/FormatSelector.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 | onFormatChanged(props.decode, f)" />
48 |
49 | onFormatChanged(d, props.format)" />
57 |
58 |
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 |
44 |
45 |
46 |
47 |
52 |
56 |
57 |
58 | remove(index)" />
59 | create(index)" />
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/frontend/src/components/dialogs/AboutDialog.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 |
32 | Tiny RDM
33 | {{ version }}
34 |
35 | {{ $t('dialogue.about.source') }}
36 |
37 | {{ $t('dialogue.about.website') }}
38 |
39 |
40 | Copyright © 2023 Tinycraft.cc All rights reserved
41 |
42 |
43 |
44 |
45 |
46 |
65 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/CopyLink.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/frontend/src/components/icons/Detail.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
64 |
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 |
19 |
29 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/frontend/src/components/new_value/AddHashValue.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 | emit('update:type', val)">
50 |
51 |
52 |
53 |
56 |
62 |
63 | remove(index)" />
64 | create(index)" />
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/frontend/src/components/common/IconButton.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
57 |
58 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
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 |
15 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/frontend/src/components/content_value/ContentSearchInput.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
67 |
68 |
69 |
70 |
71 | {{ inputData.match }}
72 |
73 |
74 | {{ $t('interface.full_search_result', { pattern: inputData.match }) }}
75 |
76 |
77 |
78 |
79 | {{ $t('interface.full_search') }}
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/frontend/src/components/dialogs/RenameKeyDialog.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
70 |
76 |
77 |
78 |
79 |
80 |
81 |
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 |
2 |
3 |

4 |
5 | Tiny RDM
6 |
7 |
8 | [](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
9 | [](https://github.com/tiny-craft/tiny-rdm/releases)
10 | [](https://github.com/tiny-craft/tiny-rdm/stargazers)
11 | [](https://github.com/tiny-craft/tiny-rdm/fork)
12 | [](https://discord.gg/VTFbBMGjWh)
13 | [](https://twitter.com/Lykin53448)
14 |
15 | 一个现代化轻量级的跨平台Redis桌面客户端,支持Mac、Windows和Linux
16 |
17 |
18 |
19 |
20 |
21 |
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 |
56 |
57 | emit('update:type', val)">
58 |
59 |
60 |
61 |
64 |
65 |
66 |
71 | :
72 |
76 |
77 |
78 | remove(index)" />
79 | create(index)" />
80 |
81 |
82 |
83 |
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 |
71 |
81 |
82 | {{ props.tooltip }}
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {{ buttonText }}
91 |
92 |
93 |
94 |
95 |
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 |
79 |
93 |
94 |
95 |
123 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
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 |
39 |
40 |
41 | {{ $t('menu.minimise') }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ $t('menu.restore') }}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {{ $t('menu.maximise') }}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {{ $t('menu.close') }}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
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 | [](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
9 | [](https://github.com/tiny-craft/tiny-rdm/releases)
10 | [](https://github.com/tiny-craft/tiny-rdm/stargazers)
11 | [](https://github.com/tiny-craft/tiny-rdm/fork)
12 | [](https://discord.gg/VTFbBMGjWh)
13 | [](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 |
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 |
56 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {{ $t('dialogue.key.async_delete_title') }}
76 |
77 |
78 |
79 |
80 | {{ $t('dialogue.key.confirm_flush') }}
81 |
82 |
83 |
84 |
85 |
86 |
87 | {{ $t('common.cancel') }}
88 |
94 | {{ $t('dialogue.key.confirm_flush_db') }}
95 |
96 |
97 |
98 |
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 |
72 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
95 |
96 | {{ $t('common.second') }}
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | {{ $t('dialogue.key.persist_key') }}
105 |
106 |
107 | {{ $t('common.cancel') }}
108 | {{ $t('common.save') }}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/frontend/src/components/dialogs/GroupDialog.vue:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
107 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/frontend/src/components/dialogs/KeyFilterDialog.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
71 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 | {{ $t('dialogue.filter.filter_pattern_tip') }}
96 |
97 |
98 | {{ $t('preferences.restore_defaults') }}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/BrowserPane.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
98 |
99 |
100 |
101 |
107 |
--------------------------------------------------------------------------------