master 4c2b7a5f1074 cached
121 files
983.6 KB
288.7k tokens
92 symbols
1 requests
Download .txt
Showing preview only (1,027K chars total). Download the full file or copy to clipboard to get everything.
Repository: qishibo/AnotherRedisDesktopManager
Branch: master
Commit: 4c2b7a5f1074
Files: 121
Total size: 983.6 KB

Directory structure:
gitextract_k6_5a8j7/

├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE.md
│   └── workflows/
│       ├── build_linux.yml
│       ├── build_mac.yml
│       ├── build_windows.yml
│       ├── codeql-analysis.yml.bak
│       ├── gen_sponsors.yaml
│       └── publish_winget.yml
├── .gitignore
├── .jshintrc
├── .postcssrc.js
├── LICENSE
├── PRIVACY.md
├── README.md
├── README.zh-CN.md
├── SECURITY.md
├── babel.config.json
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── element-variables.scss
├── index.html
├── pack/
│   ├── electron/
│   │   ├── electron-main.js
│   │   ├── font-manager.js
│   │   ├── icons/
│   │   │   └── icon.icns
│   │   ├── package.json
│   │   ├── update.js
│   │   └── win-state.js
│   └── scripts/
│       └── notarize.js
├── package.json
├── src/
│   ├── App.vue
│   ├── Aside.vue
│   ├── addon.js
│   ├── bus.js
│   ├── commands.js
│   ├── components/
│   │   ├── CliContent.vue
│   │   ├── CliTab.vue
│   │   ├── CommandLog.vue
│   │   ├── ConnectionMenu.vue
│   │   ├── ConnectionWrapper.vue
│   │   ├── Connections.vue
│   │   ├── CustomFormatter.vue
│   │   ├── DeleteBatch.vue
│   │   ├── FileInput.vue
│   │   ├── FormatViewer.vue
│   │   ├── HotKeys.vue
│   │   ├── InputBinary.vue
│   │   ├── InputPassword.vue
│   │   ├── JsonEditor.vue
│   │   ├── KeyDetail.vue
│   │   ├── KeyHeader.vue
│   │   ├── KeyList.vue
│   │   ├── KeyListNormal.vue
│   │   ├── KeyListVirtualTree.vue
│   │   ├── LanguageSelector.vue
│   │   ├── MemoryAnalysis.vue
│   │   ├── NewConnectionDialog.vue
│   │   ├── OperateItem.vue
│   │   ├── PaginationTable.vue
│   │   ├── RightClickMenu.vue
│   │   ├── ScrollToTop.vue
│   │   ├── Setting.vue
│   │   ├── SlowLog.vue
│   │   ├── Status.vue
│   │   ├── Tabs.vue
│   │   ├── UpdateCheck.vue
│   │   ├── contents/
│   │   │   ├── KeyContentHash.vue
│   │   │   ├── KeyContentList.vue
│   │   │   ├── KeyContentReJson.vue
│   │   │   ├── KeyContentSet.vue
│   │   │   ├── KeyContentStream.vue
│   │   │   ├── KeyContentString.vue
│   │   │   └── KeyContentZset.vue
│   │   └── viewers/
│   │       ├── ViewerBinary.vue
│   │       ├── ViewerBrotli.vue
│   │       ├── ViewerCustom.vue
│   │       ├── ViewerDeflate.vue
│   │       ├── ViewerDeflateRaw.vue
│   │       ├── ViewerGzip.vue
│   │       ├── ViewerHex.vue
│   │       ├── ViewerJavaSerialize.vue
│   │       ├── ViewerJson.vue
│   │       ├── ViewerMsgpack.vue
│   │       ├── ViewerOverSize.vue
│   │       ├── ViewerPHPSerialize.vue
│   │       ├── ViewerPickle.vue
│   │       ├── ViewerProtobuf.vue
│   │       └── ViewerText.vue
│   ├── i18n/
│   │   ├── i18n.js
│   │   └── langs/
│   │       ├── cn.js
│   │       ├── de.js
│   │       ├── en.js
│   │       ├── es.js
│   │       ├── fr.js
│   │       ├── it.js
│   │       ├── ko.js
│   │       ├── pt.js
│   │       ├── ru.js
│   │       ├── tr.js
│   │       ├── tw.js
│   │       ├── ua.js
│   │       └── vi.js
│   ├── main.js
│   ├── redisClient.js
│   ├── router/
│   │   └── index.js
│   ├── shortcut.js
│   ├── storage.js
│   └── util.js
└── static/
    ├── .gitkeep
    └── theme/
        ├── dark/
        │   └── index.css
        └── light/
            └── index.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.js]
quote_type = single

[*.{html,less,css,json}]
quote_type = double

================================================
FILE: .eslintrc.json
================================================
{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": ["eslint:recommended", "airbnb-base", "plugin:vue/essential"],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018
    },
    "plugins": [
        "vue"
    ],
    "rules": {
    }
}

================================================
FILE: .gitattributes
================================================
*.vue linguist-language=JavaScript


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: qishibo
patreon: # Replace with a single Patreon username
open_collective: AnotherRedisDesktopManager
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://cdn.jsdelivr.net/gh/qishibo/img/wechat.jpeg


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## OS

Windows or Linux or Mac

## VERSION

Version in settings


## ISSUE DESCRIPTION

Bug reproduction process and configuration screenshot if possible


================================================
FILE: .github/workflows/build_linux.yml
================================================
name: build_linux

on:
  release:
    types: [published]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run pack:prepare
    - run: npm run pack:linux:publish
      env:
        GH_TOKEN: ${{secrets.GH_TOKEN}}


================================================
FILE: .github/workflows/build_mac.yml
================================================
name: build_mac

on:
  release:
    types: [published]

jobs:
  build:

    runs-on: macos-latest

    env:
      GH_TOKEN: ${{secrets.GH_TOKEN}}
      CSC_LINK: ${{secrets.CSC_LINK}}
      CSC_KEY_PASSWORD: ${{secrets.CSC_KEY_PASSWORD}}
      APPLEID: ${{secrets.APPLEID}}
      APPLEID_PASSWORD: ${{secrets.APPLEID_PASSWORD}}

    steps:
    - name: Import signing keychain
      uses: apple-actions/import-codesign-certs@v2
      with:
        keychain: signing_temp
        p12-file-base64: ${{secrets.CSC_LINK}}
        p12-password: ${{secrets.CSC_KEY_PASSWORD}}

    - uses: actions/checkout@v4
    - name: Use Node.js
      uses: actions/setup-node@v4
      with:
        node-version: 16
    - run: npm ci
    - run: npm run pack:prepare
    # - run: npm run pack:macm1:publish
    - run: npm run pack:mac:publish

    - name: Cleanup keychain
      if: always()
      shell: bash
      run: |
        # Don't fail if the keychain doesn't exist.
        security delete-keychain signing_temp.keychain || true


================================================
FILE: .github/workflows/build_windows.yml
================================================
name: build_windows

on:
  release:
    types: [published]

jobs:
  build:

    runs-on: windows-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run pack:prepare
    - run: npm run pack:win:publish
      env:
        GH_TOKEN: ${{secrets.GH_TOKEN}}


================================================
FILE: .github/workflows/codeql-analysis.yml.bak
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '15 20 * * 6'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: [ 'javascript' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
        # Learn more about CodeQL language support at https://git.io/codeql-language-support

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v1
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v1

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v1


================================================
FILE: .github/workflows/gen_sponsors.yaml
================================================
name: Generate Sponsors To README
on:
  workflow_dispatch
permissions:
  contents: write
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Generate Sponsors
        uses: JamesIves/github-sponsors-readme-action@v1
        with:
          token: ${{ secrets.PAT }}
          file: 'README.md'
          active-only: false,
          template: '<a href="https://github.com/{{ login }}"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url={{ avatarUrl }}" width="60px" alt="{{ name }}" /></a>'

      - name: Generate Sponsors zh-CN
        uses: JamesIves/github-sponsors-readme-action@v1
        with:
          token: ${{ secrets.PAT }}
          file: 'README.zh-CN.md'
          active-only: false,
          template: '<a href="https://github.com/{{ login }}"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url={{ avatarUrl }}" width="60px" alt="{{ name }}" /></a>'

      - name: Commit To README
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: master
          folder: '.'


================================================
FILE: .github/workflows/publish_winget.yml
================================================
name: Publish to WinGet
on:
  release:
    types: [released]
jobs:
  publish:
    runs-on: windows-latest
    steps:
      - uses: vedantmgoyal9/winget-releaser@main
        with:
          identifier: qishibo.AnotherRedisDesktopManager
          installers-regex: '\.exe$' # Only .exe files
          token: ${{ secrets.WINGET_TOKEN }}


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
/dist/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln


================================================
FILE: .jshintrc
================================================
{
  "esversion": 6,
  "bitwise": false,
  "freeze": true
}


================================================
FILE: .postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-url": {},
    "autoprefixer": {}
  }
}


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) shibo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: PRIVACY.md
================================================
# Privacy Policy

We takes your privacy seriously. To better protect your privacy we provide this privacy policy notice explaining the way your personal information is collected and used.


## Collection of Routine Information

This app track basic information about their users. This information includes, but is not limited to, App details, timestamps, bug stacks and referring pages. None of this information can personally identify specific user to this app. The information is tracked for bug fixing and app maintenance purposes.

And please note that this routine information is stored locally by the user. Unless the user provides it voluntarily, we cannot obtain this information and we won't upload this information to the internet.


## Links to Third Party Websites

We might included links on this app for your use and reference. we are not responsible for the privacy policies on these websites. You should be aware that the privacy policies of these websites may differ from ours.


## Changes To This Privacy Policy

This Privacy Policy is effective as of 2020-01-01 and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page.

We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our app.


## Contact Information

For any questions or concerns regarding the privacy policy, please contact by shiboqi123@gmail.com.

================================================
FILE: README.md
================================================
# Another Redis Desktop Manager

<img align="right" width="110" src="https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081958294.png">

> 🚀🚀🚀 A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac. What's more, it won't crash when loading massive keys.

<br>

[![LICENSE](https://img.shields.io/github/license/qishibo/AnotherRedisDesktopManager)](LICENSE)
[![Release](https://img.shields.io/github/release/qishibo/AnotherRedisDesktopManager.svg)](https://github.com/qishibo/AnotherRedisDesktopManager/releases)
[![Download](https://img.shields.io/github/downloads/qishibo/AnotherRedisDesktopManager/total)](https://github.com/qishibo/AnotherRedisDesktopManager/releases)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fqishibo%2FAnotherRedisDesktopManager.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fqishibo%2FAnotherRedisDesktopManager?ref=badge_shield)
<a href="https://www.producthunt.com/posts/another-redis-desktop-manager?utm_source=badge-featured"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=340552&theme=dark" height="20" width="93" /></a>
[![STARS](https://img.shields.io/github/stars/qishibo/AnotherRedisDesktopManager)](https://github.com/qishibo/AnotherRedisDesktopManager/)


[简体中文](README.zh-CN.md)


## Windows

- Download latest [exe](https://github.com/qishibo/AnotherRedisDesktopManager/releases) package from release \[or [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases) in China\], double click to install.
- Or by **chocolatey**: `choco install another-redis-desktop-manager`
- Or by **winget**: `winget install qishibo.AnotherRedisDesktopManager`
- Or **sponsor** by win store, It's not free, and I will be very grateful to you.
<br/><a href="https://apps.microsoft.com/store/detail/9MTD84X0JFHZ?launch=true&cid=github&mode=mini"><img src="https://cdn.jsdelivr.net/gh/qishibo/img/microsoft-store.png" height="58" width="180" alt="get from microsoft store"></a>

## Linux

- Download latest [AppImage](https://github.com/qishibo/AnotherRedisDesktopManager/releases) package from release \[or [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases) in China\], `chmod +x`, double click to run.
- Or by **snap**: `sudo snap install another-redis-desktop-manager`
 **Tips**: If permission denied when selecting private key, run `sudo snap connect another-redis-desktop-manager:ssh-keys` to give access to ~/.ssh folder.
<br/>[![Get it from the Snap Store](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411080845423.svg)](https://snapcraft.io/another-redis-desktop-manager)


## Mac

> If you can't open it after installation by brew or dmg, exec the following command then reopen:<br>`sudo xattr -rd com.apple.quarantine /Applications/Another\ Redis\ Desktop\ Manager.app`

- Download latest [dmg](https://github.com/qishibo/AnotherRedisDesktopManager/releases) package from release \[or [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases) in China\], double click to install.
- Or by **brew**: `brew install --cask another-redis-desktop-manager`
- Or **sponsor** by app store, It's not free, and I will be very grateful to you.
<br/>[![get from app store](https://cdn.jsdelivr.net/gh/qishibo/img/avail_app_store180.svg)](https://apps.apple.com/app/id1516451072)


## Enjoy!

![redis status dark](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318491.png)

![redis key dark](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318490.png)

![redis exec log](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318492.png)


## Contributors

This project exists thanks to all the people who contribute.
[![contributors](https://opencollective.com/AnotherRedisDesktopManager/contributors.svg?width=890&button=false)](https://github.com/qishibo/AnotherRedisDesktopManager/graphs/contributors)
[![backers](https://opencollective.com/AnotherRedisDesktopManager/backers.svg)](https://opencollective.com/AnotherRedisDesktopManager)

<!-- sponsors --><a href="https://github.com/brunoksato"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;2257501?u&#x3D;62e7db432487ab19a9e11db051198d91fb42fe95&amp;v&#x3D;4" width="60px" alt="Bruno Sato" /></a><a href="https://github.com/overtrue"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;1472352?u&#x3D;72f261973db954faf4a64987ee3f7e7baf423ded&amp;v&#x3D;4" width="60px" alt="安正超" /></a><a href="https://github.com/wehnertb"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;6706492?u&#x3D;23b3c316d88684d8cbce4947d9a9564c05fa48e6&amp;v&#x3D;4" width="60px" alt="Bill" /></a><a href="https://github.com/RobinTao"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;3703152?v&#x3D;4" width="60px" alt="" /></a><a href="https://github.com/hfoxy"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;1254033?u&#x3D;630787b85016572cb1f9d87a7fbd77314b084bb9&amp;v&#x3D;4" width="60px" alt="Harry Fox" /></a><a href="https://github.com/maniappstudios"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;91392014?v&#x3D;4" width="60px" alt="Mani App Studios" /></a><a href="https://github.com/BWICompanies"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;28601356?v&#x3D;4" width="60px" alt="BWI Companies, Inc." /></a><a href="https://github.com/dragonflydb"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;104819355?v&#x3D;4" width="60px" alt="DragonflyDB" /></a><a href="https://github.com/gauravn00b"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;70876227?v&#x3D;4" width="60px" alt="" /></a><a href="https://github.com/roostinghawk"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;5466611?u&#x3D;6c5bcb3a5e4cd3bc128052dcce3a58ba00e742f9&amp;v&#x3D;4" width="60px" alt="liuwei" /></a><a href="https://github.com/status2xx"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;40201780?u&#x3D;ba9c0f10211ecf2af64ec335a5df13637e313060&amp;v&#x3D;4" width="60px" alt="小新" /></a><a href="https://github.com/mikeallisonJS"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;838371?u&#x3D;6c8a7a989e12d67117976a5ec155e022a5d83499&amp;v&#x3D;4" width="60px" alt="Mike Allison" /></a><!-- sponsors -->


## Sponsor

- Give me a star ⭐ or upvote on [Producthunt](https://www.producthunt.com/posts/another-redis-desktop-manager)
- Through [OpenCollective](https://opencollective.com/AnotherRedisDesktopManager) or [Github Sponsor](https://github.com/sponsors/qishibo)
- If you are a Mac user, you can purchase this software from the [app store](https://apps.apple.com/app/id1516451072) to sponsor, and then let the app store automatically update it for you.
<br>[![app store](https://cdn.jsdelivr.net/gh/qishibo/img/avail_app_store180.svg)](https://apps.apple.com/app/id1516451072)
- If you are a Windows user, you can purchase this software from the [win store](https://www.microsoft.com/store/apps/9MTD84X0JFHZ) to sponsor.
<br>[![windows store](https://cdn.jsdelivr.net/gh/qishibo/img/windows-store-icon182-56.png)](https://www.microsoft.com/store/apps/9MTD84X0JFHZ)
- Wechat sponsor code [Sponsor me a cup of coffee ☕]

  <img width="150px" src="https://cdn.jsdelivr.net/gh/qishibo/img/202109031655807.jpeg" />


## Feature Log

- 2025-10-01: Long time no see! New features will coming soon!
- 2024-11-03: Import batch commands from files
- 2024-10-07: Hash field TTL support(Redis>=7.4)
- 2024-06-06: Search connections support
- 2024-04-10: DB custom name support
- 2024-02-21: Java/Pickle viewers support
- 2024-02-15: Groups/Consumers in STREAM view
- 2024-01-31: Hey, long time! Command line(CLI) args support
- 2023-06-22: Export\Import keys support
- 2023-05-26: Search support in Stream && Slow log support
- 2023-04-01: Search support in List && Deflate raw support
- 2022-10-07: Arrow Keys support in key list && Memory Analysis in folder
- 2022-08-05: Clone Connection && Tabs Contextmenu\Mousewheel Support
- 2022-04-01: Protobuf Support && Memory Analysis
- 2022-03-03: Readonly Mode && Mointor Support
- 2022-01-24: Command Dump Support
- 2022-01-05: Support To Load All Keys
- 2022-01-01: Brotli\Gzip\Deflate Support && RedisJSON Support
- 2021-11-26: JSON Editable && Subscribe Support
- 2021-08-30: Execution log Support && Add Hot Keys
- 2021-08-16: Custom Formatter View Support!
- 2021-06-30: Sentinel Support!!
- 2021-06-24: ACL Support
- 2021-05-03: Stream Support && Cli Command Tips Support
- 2021-02-28: Connection Color Tag && Search History Support
- 2021-02-03: Multiple Select\Delete && Msgpack Viewer Support
- 2020-12-30: Tree View Support!!!
- 2020-11-03: Binary View Support && SSH Passparse\Timeout Support
- 2020-09-04: SSH Cluster Support && Extension Commands Support
- 2020-06-18: SSL/TLS Support!!!
- 2020-04-28: Page Zoom && Big Key Loads With Scan && Auto Json
- 2020-04-18: Unvisible Key\Value Format Support
- 2020-04-04: Cluster Support!!!
- 2020-03-13: Dark Mode Support!!! && JsonView In Other Place
- 2020-02-16: SSH Private Key Support
- 2020-02-13: Open Cli Console In Tabs
- 2019-06-14: Custom Font-Family Support
- 2019-05-28: Key List Resizable
- 2019-05-09: Search Support In Hash List Set Zset
- 2019-04-26: Auto Updater
- 2019-04-09: SSH Tunnel Connection Support
- 2019-04-01: Extract Search Support
- 2019-02-22: Single Connection Support
- 2019-01-08: Project Start


## Dev Build

### Linux Or Mac

```bash
# clone code
git clone https://github.com/qishibo/AnotherRedisDesktopManager.git --depth=1
cd AnotherRedisDesktopManager

# install dependencies
npm install

# if download electron failed during installing, use this command
# ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" npm install

# serve with hot reload at localhost:9988
npm start


# after the previous step is completed to 100%, open another tab, build up a desktop client
npm run electron
```

If linux errors like this:

```bash
# if error like this
../src/FontManagerLinux.cc:1:35: fatal error: fontconfig/fontconfig.h: No such file or directory

# then try this
sudo apt install libfontconfig1-dev
```


### Windows

``` bash
# clone code
git clone https://github.com/qishibo/AnotherRedisDesktopManager.git --depth=1
cd AnotherRedisDesktopManager

# install dependencies, 32-bit or 64-bit all use win32
npm install --platform=win32

# if download electron failed during installing, use this command
# npm config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/
# npm install --platform=win32

# serve with hot reload at localhost:9988
npm start


# after the previous step is completed to 100%, open another tab, build up a desktop client
npm run electron
```

### Build Package

```bash
# prepare before package
npm run pack:prepare

# build package on respective platforms
# on windows build 64bit package
npm run pack:win
# on windows build 32bit package
npm run pack:win32

# on mac
npm run pack:mac

# on linux
npm run pack:linux
```


## Custom Viewer

> When the default viewer does not meet the needs, you can format your content via customize script.
<br>Method: Pull down the viewer list to the bottom, click "Custom -> Add", and then refer to the instructions below
<br>Note: The script needs to output formatted content through `print` `console.log` `echo` etc., which can be any string or JSON string

| Config | Description |
| ------ | ------ |
| `Name` | Custom name |
| `Command` | Executable commands, such as `xxx.py` `xxx.js` `xxx.class` etc. The file needs `x` permission, which can be executed in the form of `./xxx.py`; It can also be set to `/bin/node` `/bin/bash` or other system commands, and the script path needs to be placed in Params |
| `Params` | Parameters spliced after `Command`, such as "--key `{KEY}` --value `{VALUE}`", where `{KEY}` and `{VALUE}` will be replaced with the corresponding Redis key and value. Note that if the content is invisible such as binary, you can use `{HEX}` instead of `{VALUE}`, and `{HEX}` will be replaced with the hexadecimal string. If HEX is too long (>8000 chars), it will be written to a temporary file. You can use `{HEX_FILE}` to obtain the file path, and read by yourself in the script |

### Configuration example:
> Add env to the first line of the script, the final executed command is: `./home/qii/pickle_decoder.py {HEX}`, the script can receive parameters via `argv[1]`, ref [#978](https://github.com/qishibo/AnotherRedisDesktopManager/issues/987#issuecomment-1294844707)

| Command | Params |
| ------ | ------ |
| `/home/qii/pickle_decoder.py` | `{HEX}` |
| `/home/qii/shell_decoder.sh` | `{VALUE}` |

### Without execute permission `x`:
> The final executed command is: `/bin/node /home/qii/node_decoder.js {HEX} --key={KEY}`, the script can receive parameters via `argv[1]`

| Command | Params |
| ------ | ------ |
| `/bin/bash` | `/home/qii/shell_decoder.sh {VALUE}` |
| `/bin/node` | `/home/qii/node_decoder.js {HEX} --key={KEY}` |



## Start From Command Line(CLI)

> If you want to start from command line(CLI), you can pass args to the App.

### Examples

```bash
# Linux
# ./Another Redis Desktop Manager.AppImage

# Mac
# open /Applications/Another\ Redis\ Desktop\ Manager.app --args

# Windows
"D:\xxxx\Another Redis Desktop Manager.exe"

# COMMON
--host 127.0.0.1 --port 6379 --auth 123
--name tmp_connection

# CLUSTER
--cluster

# SSH
--ssh-host 192.168.0.110
--ssh-username root --ssh-password 123

# SENTINEL
--sentinel-master-name mymaster
--sentinel-node-password 123

# save connection
--save
# readonly mode
--readonly
```

### Parameter Description

#### Common

| Args | Description | Args | Description |
| ------ | ------ | ------ | ------ |
| --host | Redis host* | --port | Redis port|
| --auth | Password | --name | Custom name|
| --separator | Key separator | --readonly | Enable readonly mode|
| --username | Username(Redis6 ACL)| --save| Enable saving, one-time link by default|

#### SSH

| Args | Description | Args | Description |
| ------ | ------ | ------ | ------ |
| --ssh-host | SSH host* | --ssh-port | SSH port(default:22)|
| --ssh-username | Username* | --ssh-password | Password|
| --ssh-private-key | Path of private key | --ssh-passphrase | Password of private key|
| --ssh-timeout | SSH timeout(s) | | &nbsp;|

#### CLUSTER

| Args | Description |
| ------ | ------ |
| --cluster | Enable CLUSTER mode |

#### SSL

| Args | Description | Args | Description |
| ------ | ------ | ------ | ------ |
| --ssl | Enable SSL* | --ssl-key | SSL Private Key Pem|
| --ssl-ca | SSL Certificate Authority | --ssl-cert | SSL Public Key Pem|

#### SENTINEL

| Args | Description |
| ------ | ------ |
| --sentinel-master-name | Name of master group*,like 'mymaster' |
| --sentinel-node-password | Password of Redis node |



## FAQ

#### 1. How to connect to Redis Cluster in internal network (such as Docker, LAN, AWS)?
   
   Answer: Connect via `SSH+Cluster` (SSH to the internal network and then connecting to Cluster with internal IP such as `127.0.0.1`, `192.168.x.x`), you need to fill Redis Host with the internal IP.
   
   How to get Redis internal IP? Connect through SSH, uncheck Cluster option, and then open the console to execute `CLUSTER NODES`, select any IP in the result.

#### 2. Do I need to fill in the 'Username' in the Redis configuration?
   
   Answer: The access control list (ACL) is only supported in `Redis>=6.0`, so do not fill it unless you need a special user.


## License

[MIT](LICENSE)


## Support

[goanother.com](https://goanother.com/) &nbsp; [Producthunt](https://www.producthunt.com/posts/another-redis-desktop-manager) &nbsp; [Twitter@shibo](https://twitter.com/qii404) &nbsp; [Download Analysis](https://qii404.me/github-release-statistics/?repo=/qishibo/AnotherRedisDesktopManager/)



================================================
FILE: README.zh-CN.md
================================================
# Another Redis Desktop Manager

<img align="right" width="110" src="https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081958294.png">

> 🚀🚀🚀 更快、更好、更稳定的Redis桌面(GUI)管理客户端,兼容Windows、Mac、Linux,性能出众,轻松加载海量键值

<br>

[![LICENSE](https://img.shields.io/github/license/qishibo/AnotherRedisDesktopManager)](LICENSE)
[![Release](https://img.shields.io/github/release/qishibo/AnotherRedisDesktopManager.svg)](https://github.com/qishibo/AnotherRedisDesktopManager/releases)
[![Download](https://img.shields.io/github/downloads/qishibo/AnotherRedisDesktopManager/total)](https://github.com/qishibo/AnotherRedisDesktopManager/releases)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fqishibo%2FAnotherRedisDesktopManager.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fqishibo%2FAnotherRedisDesktopManager?ref=badge_shield)
<a href="https://www.producthunt.com/posts/another-redis-desktop-manager?utm_source=badge-featured"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=340552&theme=dark" height="20" width="93" /></a>
[![STARS](https://img.shields.io/github/stars/qishibo/AnotherRedisDesktopManager)](https://github.com/qishibo/AnotherRedisDesktopManager/)

## Windows

- 可以在[github](https://github.com/qishibo/AnotherRedisDesktopManager/releases) 或者 [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases)下载`exe`安装包
- 或者通过**chocolatey**: `choco install another-redis-desktop-manager`
- 或者通过**winget**: `winget install qishibo.AnotherRedisDesktopManager`
- 或者通过Win Store**赞助**,然后让Win Store帮你自动更新版本
<br/><a href="https://www.microsoft.com/store/apps/9MTD84X0JFHZ?cid=storebadge&ocid=badge"><img src="https://cdn.jsdelivr.net/gh/qishibo/img/microsoft-store.png" height="58" width="180" alt="get from microsoft store"></a>

## Linux

- 可以在[github](https://github.com/qishibo/AnotherRedisDesktopManager/releases) 或者 [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases)下载`AppImage`包,`chmod +x`, 双击运行
- 或者通过**snap**: `sudo snap install another-redis-desktop-manager`
**Tips**: 如果选择私钥时提示权限不足,执行`sudo snap connect another-redis-desktop-manager:ssh-keys`来获取对~/.ssh文件夹的权限
<br/>[![Get it from the Snap Store](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411080845423.svg)](https://snapcraft.io/another-redis-desktop-manager)

## Mac

> 如果通过brew或者dmg安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:<br>`sudo xattr -rd com.apple.quarantine /Applications/Another\ Redis\ Desktop\ Manager.app`

- 可以在[github](https://github.com/qishibo/AnotherRedisDesktopManager/releases) 或者 [gitee](https://gitee.com/qishibo/AnotherRedisDesktopManager/releases)下载`dmg`安装包
- 通过 **brew**: `brew install --cask another-redis-desktop-manager`
- 或者通过App Store**赞助**, 然后让App Store帮你自动更新版本
<br/>[![app store](https://cdn.jsdelivr.net/gh/qishibo/img/avail_app_store180.svg)](https://apps.apple.com/app/id1516451072)


## 起飞!

![redis status dark](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318491.png)

![redis key dark](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318490.png)

![redis exec log](https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411081318492.png)


## 贡献者

在这里感谢所有为此项目做出贡献的人.
[![contributors](https://opencollective.com/AnotherRedisDesktopManager/contributors.svg?width=890&button=false)](https://github.com/qishibo/AnotherRedisDesktopManager/graphs/contributors)
[![backers](https://opencollective.com/AnotherRedisDesktopManager/backers.svg)](https://opencollective.com/AnotherRedisDesktopManager)

<!-- sponsors --><a href="https://github.com/brunoksato"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;2257501?u&#x3D;62e7db432487ab19a9e11db051198d91fb42fe95&amp;v&#x3D;4" width="60px" alt="Bruno Sato" /></a><a href="https://github.com/overtrue"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;1472352?u&#x3D;72f261973db954faf4a64987ee3f7e7baf423ded&amp;v&#x3D;4" width="60px" alt="安正超" /></a><a href="https://github.com/wehnertb"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;6706492?u&#x3D;23b3c316d88684d8cbce4947d9a9564c05fa48e6&amp;v&#x3D;4" width="60px" alt="Bill" /></a><a href="https://github.com/RobinTao"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;3703152?v&#x3D;4" width="60px" alt="" /></a><a href="https://github.com/hfoxy"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;1254033?u&#x3D;630787b85016572cb1f9d87a7fbd77314b084bb9&amp;v&#x3D;4" width="60px" alt="Harry Fox" /></a><a href="https://github.com/maniappstudios"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;91392014?v&#x3D;4" width="60px" alt="Mani App Studios" /></a><a href="https://github.com/BWICompanies"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;28601356?v&#x3D;4" width="60px" alt="BWI Companies, Inc." /></a><a href="https://github.com/dragonflydb"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;104819355?v&#x3D;4" width="60px" alt="DragonflyDB" /></a><a href="https://github.com/gauravn00b"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;70876227?v&#x3D;4" width="60px" alt="" /></a><a href="https://github.com/roostinghawk"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;5466611?u&#x3D;6c5bcb3a5e4cd3bc128052dcce3a58ba00e742f9&amp;v&#x3D;4" width="60px" alt="liuwei" /></a><a href="https://github.com/status2xx"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;40201780?u&#x3D;ba9c0f10211ecf2af64ec335a5df13637e313060&amp;v&#x3D;4" width="60px" alt="小新" /></a><a href="https://github.com/mikeallisonJS"><img src="https://images.weserv.nl/?h=120&w=120&mask=circle&url=https:&#x2F;&#x2F;avatars.githubusercontent.com&#x2F;u&#x2F;838371?u&#x3D;6c8a7a989e12d67117976a5ec155e022a5d83499&amp;v&#x3D;4" width="60px" alt="Mike Allison" /></a><!-- sponsors -->


## 赞助

- 给我点个Star⭐⭐ 或者 [Producthunt](https://www.producthunt.com/posts/another-redis-desktop-manager)点个赞
- 通过 [OpenCollective](https://opencollective.com/AnotherRedisDesktopManager)或者[Github Sponsor](https://github.com/sponsors/qishibo)
- 如果你是Mac用户, 也可以通过从[App Store](https://apps.apple.com/app/id1516451072)购买来赞助, 然后应用商店会为你自动更新
<br>[![app store](https://cdn.jsdelivr.net/gh/qishibo/img/avail_app_store180.svg)](https://apps.apple.com/app/id1516451072)
- 如果你是Windows用户,还可以通过从[Win Store](https://www.microsoft.com/store/apps/9MTD84X0JFHZ)购买来赞助,商店就会帮你自动更新
<br>[![windows store](https://cdn.jsdelivr.net/gh/qishibo/img/windows-store-icon182-56.png)](https://www.microsoft.com/store/apps/9MTD84X0JFHZ)
- 微信赞助码 [觉得好用,赞助一些大白兔🐇奶糖!]

  <img width="150px" src="https://cdn.jsdelivr.net/gh/qishibo/img/202109031655807.jpeg" />


## 里程碑

- 2024-11-03: 支持从文件批量导入命令并执行
- 2024-10-07: Hash键值支持TTL(Redis>=7.4)
- 2024-06-06: 搜索链接支持
- 2024-04-10: DB自定义名称支持
- 2024-02-21: Java/Pickle解码视图支持
- 2024-02-15: STEAM支持查看群组和消费者
- 2024-01-31: 好久不见! 命令行参数启动支持
- 2023-06-22: 不同db\数据库之间支持导入导出key
- 2023-05-26: Stream类型搜索支持 && 支持慢日志查询
- 2023-04-01: List类型搜索支持 && Deflate raw 支持
- 2022-10-07: Key列表方向键 && 内存分析支持指定文件夹
- 2022-08-05: 克隆连接 && Tabs右键和滚轮支持
- 2022-04-01: Protobuf 支持 && 内存占用分析
- 2022-03-03: 只读模式 && Mointor 支持
- 2022-01-01: Brotli\Gzip\Deflate 解压缩支持 && RedisJSON 支持
- 2021-11-26: JSON可编辑 && Subscribe支持
- 2021-08-30: 命令执行日志 && 快捷键
- 2021-08-16: 自定义文本视图
- 2021-06-30: 哨兵支持
- 2021-06-24: Redis>=6.0的ACL支持
- 2021-05-03: Stream 视图支持 && Cli命令行提示
- 2021-02-28: 链接颜色标记 && 搜索历史提示
- 2021-02-03: 多选支持 && Msgpack视图支持
- 2020-12-30: 树状列表
- 2020-11-03: Binary视图 && SSH Passparse\Timeout 支持
- 2020-09-04: SSH 集群支持
- 2020-06-18: SSL/TLS 支持
- 2020-04-28: 页面缩放 && 大键值Scan操作 && 自动Json
- 2020-04-18: 不可见键值对支持
- 2020-04-04: 集群支持
- 2020-03-13: 暗黑模式
- 2020-02-16: SSH 私钥支持
- 2020-02-13: Cli新Tab打开
- 2019-06-14: 自定义字体支持
- 2019-05-28: Key列表调节宽度
- 2019-05-09: Hash List Set Zset搜索支持
- 2019-04-26: 自动更新
- 2019-04-09: SSH 通道支持
- 2019-04-01: 精确搜索
- 2019-02-22: 单链接支持
- 2019-01-08: 项目孵化


## Dev Build

> Tips: 此为开发环境,用于运行完整项目,**普通用户**直接从前面下载安装包即可


### Linux Or Mac

```bash
# clone code
git clone https://github.com/qishibo/AnotherRedisDesktopManager.git --depth=1
cd AnotherRedisDesktopManager

# install dependencies
npm install

# if download electron failed during installing, use this command
# ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" npm install

# serve with hot reload at localhost:9988
npm start


# after the previous step is completed to 100%, open another tab, build up a desktop client
npm run electron
```

If linux errors like this:

```bash
# if error like this
../src/FontManagerLinux.cc:1:35: fatal error: fontconfig/fontconfig.h: No such file or directory

# then try this
sudo apt install libfontconfig1-dev
```


### Windows

``` bash
# clone code
git clone https://github.com/qishibo/AnotherRedisDesktopManager.git --depth=1
cd AnotherRedisDesktopManager

# install dependencies, 32-bit or 64-bit all use win32
npm install --platform=win32

# if download electron failed during installing, use this command
# npm config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/
# npm install --platform=win32

# serve with hot reload at localhost:9988
npm start


# after the previous step is completed to 100%, open another tab, build up a desktop client
npm run electron
```

### Build Package

```bash
# prepare before package
npm run pack:prepare

# build package on respective platforms
# on windows build 64bit package
npm run pack:win
# on windows build 32bit package
npm run pack:win32

# on mac
npm run pack:mac

# on linux
npm run pack:linux
```


## 自定义格式化

> 当默认可视化方式不满足需求时,可以使用自定义脚本来格式化你的内容。
<br>方式:可视化列表下拉到底部,点击"自定义->新增",然后参考下面说明。
<br>注意:脚本需要通过`print` `console.log` `echo`等输出格式化好的内容,可以是任意字符串或者JSON字符串


| 配置项 | 参数说明 |
| ------ | ------ |
| `Name` | 自定义名称 |
| `Command` | 可执行命令,如`xxx.py` `xxx.js` `xxx.class`等,该文件需要具有可执行的`x`权限,可以通过形如`./xxx.py`方式执行;也可以直接用系统命令`/bin/node` `/bin/bash`等,此时需要把脚本路径放到Params里 |
| `Params` | 拼接在`Command`后的参数,如"--key `{KEY}` --value `{VALUE}`",其中`{KEY}`和`{VALUE}`在执行时会被替换成对应的Redis key和value。注意如果内容为二进制等不可见字符时,可以使用`{HEX}`,`{HEX}`会被替换成对应value的16进制即hex编码。如果HEX过长(>8000字符)时会被写入到临时文件,可以用`{HEX_FILE}`获取文件路径,脚本中自行读取即可 |

### 配置样例:
> 脚本文件首行一般要增加env说明,最终执行的命令如: `./home/qii/pickle_decoder.py {HEX}`, 脚本中可以使用`argv[1]`接收参数,参考 [#978](https://github.com/qishibo/AnotherRedisDesktopManager/issues/987#issuecomment-1294844707)

| Command | Params |
| ------ | ------ |
| `/home/qii/pickle_decoder.py` | `{HEX}` |
| `/home/qii/shell_decoder.sh` | `{VALUE}` |

### 脚本文件无执行权限时:
> 最终执行的命令如: `/bin/node /home/qii/node_decoder.js {HEX}`, 脚本中可以使用`argv[1]`接收参数

| Command | Params |
| ------ | ------ |
| `/bin/bash` | `/home/qii/shell_decoder.sh {VALUE}` |
| `/bin/node` | `/home/qii/node_decoder.js {HEX} --key={KEY}` |



## 命令行启动

> 如果你有需求从命令行启动程序,可以通过如下方式,自定义不同的连接参数。

### 示例

```bash
# Linux
# ./Another Redis Desktop Manager.AppImage

# Mac
# open /Applications/Another\ Redis\ Desktop\ Manager.app --args

# Windows
"D:\xxxx\Another Redis Desktop Manager.exe"

# COMMON
--host 127.0.0.1 --port 6379 --auth 123
--name tmp_connection

# CLUSTER
--cluster

# SSH
--ssh-host 192.168.0.110
--ssh-username root --ssh-password 123

# SENTINEL
--sentinel-master-name mymaster
--sentinel-node-password 123

# save connection
--save
# readonly mode
--readonly
```

### 参数说明

#### 通用

| 参数 | 说明 | 参数 | 说明 |
| ------ | ------ | ------ | ------ |
| --host | 地址* | --port | 端口|
| --auth | 密码 | --name | 自定义名称|
| --separator | 分隔符 | --readonly | 开启只读模式|
| --username | 用户名(Redis6 ACL)| --save| 保存连接(默认不保存)|

#### SSH

| 参数 | 说明 | 参数 | 说明 |
| ------ | ------ | ------ | ------ |
| --ssh-host | 地址* | --ssh-port | 端口(默认22)|
| --ssh-username | 用户名* | --ssh-password | 密码|
| --ssh-private-key | 私钥路径 | --ssh-passphrase | 私钥密码|
| --ssh-timeout | 超时(秒) | | &nbsp;|

#### CLUSTER

| 参数 | 说明 |
| ------ | ------ |
| --cluster | 开启集群模式 |

#### SSL

| 参数 | 说明 | 参数 | 说明 |
| ------ | ------ | ------ | ------ |
| --ssl | 开启SSL模式* | --ssl-key | SSL私钥路径|
| --ssl-ca | SSL证书机构 | --ssl-cert | SSL公钥路径|

#### SENTINEL

| 参数 | 说明 |
| ------ | ------ |
| --sentinel-master-name | Master组名称*,如mymaster |
| --sentinel-node-password | Redis节点密码 |



## FAQ

#### 1. 内网中的Redis集群如何连接(如Docker内,局域网内,AWS内)?
   
   答:使用`SSH+Cluster`的方式连接(等价于先SSH到内网,再使用内网ip连接Cluster),Redis的Host填写Redis**内网ip**地址如`127.0.0.1` `192.168.x.x`。
   
   Redis内网地址如何获得?直接以SSH的方式连接,不勾选Cluster,然后打开命令行,直接执行`CLUSTER NODES`, 在结果中选一ip即可。

#### 2. Redis配置中的`Username`用户名是否需要填写?
   
   答:用户名为`Redis>=6.0`才支持的访问控制列表(`ACL`),默认不需要填写(为default),指定特殊用户时才填写。


## License

[MIT](LICENSE)


## Support

[goanother.com](https://goanother.com/) &nbsp; [Producthunt](https://www.producthunt.com/posts/another-redis-desktop-manager) &nbsp; [Download Analysis](https://qii404.me/github-release-statistics/?repo=/qishibo/AnotherRedisDesktopManager/)




================================================
FILE: SECURITY.md
================================================
# Security Policy

## Reporting a Vulnerability

If there are any vulnerabilities in **Another Redis Desktop Manager**, don't hesitate to _report them_.

1. Mail to `shiboqi123@gmail.com`
2. Describe the vulnerability.

   If you have a fix, that is most welcome -- please attach or summarize it in your message!

3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.

   Please **do not disclose the vulnerability publicly** until a fix is released!

4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.

5. Thx!


================================================
FILE: babel.config.json
================================================
{
  "presets": [
    "@babel/env",
    "@vue/babel-preset-jsx"
  ],
  "sourceType": "unambiguous",
  "plugins": [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}


================================================
FILE: build/build.js
================================================
'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})


================================================
FILE: build/check-versions.js
================================================
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')

function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  }
]

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}


================================================
FILE: build/utils.js
================================================
'use strict'
const path = require('path')
const config = require('../config')
// const ExtractTextPlugin = require('extract-text-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const packageConfig = require('../package.json')

exports.assetsPath = function (_path) {
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory

  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}

  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      // return ExtractTextPlugin.extract({
      //   use: loaders,
      //   fallback: 'vue-style-loader',
      //   // for font path import by css files
      //   publicPath:"../../",
      // })
      return [MiniCssExtractPlugin.loader].concat(loaders)
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

exports.createNotifierCallback = () => {
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return

    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}


================================================
FILE: build/vue-loader.conf.js
================================================
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}


================================================
FILE: build/webpack.base.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}



module.exports = {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  target: 'electron-renderer',
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
  module: {
    rules: [
      {
        test: /\.node$/,
        loader: "node-loader",
        options: {
          // map sourceMap
          name(resourcePath, resourceQuery) {
            if (process.env.NODE_ENV === "development") {
              return "[path][name].[ext]";
            }

            return "[contenthash].[ext]";
          },
        },
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig,
        // include: [
        //   resolve('src'), resolve('test'),
        //   // resolve('node_modules/@qii404/vue-easy-tree/src/')
        // ],
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [
          // resolve('src'),
          // resolve('test'),
          // resolve('node_modules/webpack-dev-server/client'),
          resolve('node_modules/pickleparser')
        ]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
          // this is vital important for fonts loads, added before 'static/fonts'
          publicPath: '../../'
        }
      },

    ]
  },
  node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}


================================================
FILE: build/webpack.dev.conf.js
================================================
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const VueLoaderPlugin = require('vue-loader/lib/plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  mode: 'development',
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    new VueLoaderPlugin(),
    new MonacoWebpackPlugin({languages: ['json'], features: []}),
    // new webpack.DefinePlugin({
    //   'process.env': require('../config/dev.env')
    // }),
    new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    // new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})


================================================
FILE: build/webpack.prod.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// const ExtractTextPlugin = require('extract-text-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const VueLoaderPlugin = require('vue-loader/lib/plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

const env = require('../config/prod.env')

const webpackConfig = merge(baseWebpackConfig, {
  mode: 'production',
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  performance: {
    hints: false
  },
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // show bundle analysis if need
    // new BundleAnalyzerPlugin(),
    new VueLoaderPlugin(),
    new MonacoWebpackPlugin({languages: ['json'], features: []}),
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    // new webpack.DefinePlugin({
    //   'process.env': env
    // }),
    // new UglifyJsPlugin({
    //   uglifyOptions: {
    //     compress: {
    //       warnings: false,
    //       drop_debugger: true,
    //       drop_console: true,
    //     }
    //   },
    //   sourceMap: config.build.productionSourceMap,
    //   parallel: true
    // }),
    // extract css into its own file
    // new ExtractTextPlugin({
    //   filename: utils.assetsPath('css/[name].[contenthash].css'),
    //   // Setting the following option to `false` will not extract CSS from codesplit chunks.
    //   // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
    //   // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
    //   // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
    //   allChunks: true,
    // }),
    new MiniCssExtractPlugin({
      filename: utils.assetsPath('css/[name].css'),
      chunkFilename: utils.assetsPath('css/[name].[contenthash].css')
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      // chunksSortMode: 'dependency'
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    // new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'vendor',
    //   minChunks (module) {
    //     // any required modules inside node_modules are extracted to vendor
    //     return (
    //       module.resource &&
    //       /\.js$/.test(module.resource) &&
    //       module.resource.indexOf(
    //         path.join(__dirname, '../node_modules')
    //       ) === 0
    //     )
    //   }
    // }),
    // // extract webpack runtime and module manifest to its own file in order to
    // // prevent vendor hash from being updated whenever app bundle is updated
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'manifest',
    //   minChunks: Infinity
    // }),
    // // This instance extracts shared chunks from code splitted chunks and bundles them
    // // in a separate chunk, similar to the vendor chunk
    // // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'app',
    //   async: 'vendor-async',
    //   children: true,
    //   minChunks: 3
    // }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ],
  optimization: {
    runtimeChunk: {
      name: 'manifest'
    },
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: config.build.productionSourceMap,
        uglifyOptions: {
          warnings: false
        },
      }),
      new OptimizeCSSPlugin({
        cssProcessorOptions: config.build.productionSourceMap
          ? { safe: true, map: { inline: false } }
          : { safe: true }
      }),
    ],
    splitChunks: {
      chunks: 'all',
      // cacheGroups: {
      //   commons: {
      //     test: /[\\/]node_modules[\\/]/,
      //     name: 'vendors',
      //     chunks: 'all',
      //   },
      // }
    }
  },
})

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig


================================================
FILE: config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})


================================================
FILE: config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 9988, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-


    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    // when not web conditon, such as index.html as main, relative path
    assetsPublicPath: './',

    /**
     * Source Maps
     */

    productionSourceMap: false,
    // https://webpack.js.org/configuration/devtool/#production
    // devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}


================================================
FILE: config/prod.env.js
================================================
'use strict'
module.exports = {
  NODE_ENV: '"production"'
}


================================================
FILE: element-variables.scss
================================================
/* Element Chalk Variables */


// Special comment for theme configurator
// type|skipAutoTranslation|Category|Order
// skipAutoTranslation 1

/* Transition
-------------------------- */
$--all-transition: all .3s cubic-bezier(.645,.045,.355,1) !default;
$--fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--fade-linear-transition: opacity 200ms linear !default;
$--md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--border-transition-base: border-color .2s cubic-bezier(.645,.045,.355,1) !default;
$--color-transition-base: color .2s cubic-bezier(.645,.045,.355,1) !default;

/* Color
-------------------------- */
/// color|1|Brand Color|0
$--color-primary: #52a6fd !default;
/// color|1|Background Color|4
$--color-white: #263238 !default;
/// color|1|Background Color|4
$--color-black: #000000 !default;
$--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */
$--color-primary-light-2: mix($--color-white, $--color-primary, 20%) !default; /* 66b1ff */
$--color-primary-light-3: mix($--color-white, $--color-primary, 30%) !default; /* 79bbff */
$--color-primary-light-4: mix($--color-white, $--color-primary, 40%) !default; /* 8cc5ff */
$--color-primary-light-5: mix($--color-white, $--color-primary, 50%) !default; /* a0cfff */
$--color-primary-light-6: mix($--color-white, $--color-primary, 60%) !default; /* b3d8ff */
$--color-primary-light-7: mix($--color-white, $--color-primary, 70%) !default; /* c6e2ff */
$--color-primary-light-8: mix($--color-white, $--color-primary, 80%) !default; /* d9ecff */
$--color-primary-light-9: mix($--color-white, $--color-primary, 90%) !default; /* ecf5ff */
/// color|1|Functional Color|1
$--color-success: #67C23A !default;
/// color|1|Functional Color|1
$--color-warning: #E6A23C !default;
/// color|1|Functional Color|1
$--color-danger: #F56C6C !default;
/// color|1|Functional Color|1
$--color-info: #a3a6ad !default;

$--color-success-light: mix($--color-white, $--color-success, 80%) !default;
$--color-warning-light: mix($--color-white, $--color-warning, 80%) !default;
$--color-danger-light: mix($--color-white, $--color-danger, 80%) !default;
$--color-info-light: mix($--color-white, $--color-info, 80%) !default;

$--color-success-lighter: mix($--color-white, $--color-success, 90%) !default;
$--color-warning-lighter: mix($--color-white, $--color-warning, 90%) !default;
$--color-danger-lighter: mix($--color-white, $--color-danger, 90%) !default;
$--color-info-lighter: mix($--color-white, $--color-info, 90%) !default;
/// color|1|Font Color|2
$--color-text-primary: #F3F3F4 !default;
/// color|1|Font Color|2
$--color-text-regular: #F3F3F4 !default;
/// color|1|Font Color|2
$--color-text-secondary: #F3F3F4 !default;
/// color|1|Font Color|2
$--color-text-placeholder: #507fa9 !default;
/// color|1|Border Color|3
$--border-color-base: #7f8ea5 !default;
/// color|1|Border Color|3
$--border-color-light: #7f8ea5 !default;
/// color|1|Border Color|3
$--border-color-lighter: #7f8ea5 !default;
/// color|1|Border Color|3
$--border-color-extra-light: #7f8ea5 !default;

// Background
/// color|1|Background Color|4
$--background-color-base: #3b4b54 !default;

/*custom styles*/
body {
  background: $--color-white;
}
/*custom styles end*/

/* Link
-------------------------- */
$--link-color: $--color-primary-light-2 !default;
$--link-hover-color: $--color-primary !default;

/* Border
-------------------------- */
$--border-width-base: 1px !default;
$--border-style-base: solid !default;
$--border-color-hover: $--color-text-placeholder !default;
$--border-base: $--border-width-base $--border-style-base $--border-color-base !default;
/// borderRadius|1|Radius|0
$--border-radius-base: 4px !default;
/// borderRadius|1|Radius|0
$--border-radius-small: 2px !default;
/// borderRadius|1|Radius|0
$--border-radius-circle: 100% !default;
/// borderRadius|1|Radius|0
$--border-radius-zero: 0 !default;

// Box-shadow
/// boxShadow|1|Shadow|1
$--box-shadow-base: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04) !default;
// boxShadow|1|Shadow|1
$--box-shadow-dark: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .12) !default;
/// boxShadow|1|Shadow|1
$--box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !default;

/* Fill
-------------------------- */
$--fill-base: $--color-white !default;

/* Typography
-------------------------- */
$--font-path: 'fonts' !default;
$--font-display: 'auto' !default;
/// fontSize|1|Font Size|0
$--font-size-extra-large: 20px !default;
/// fontSize|1|Font Size|0
$--font-size-large: 18px !default;
/// fontSize|1|Font Size|0
$--font-size-medium: 16px !default;
/// fontSize|1|Font Size|0
$--font-size-base: 14px !default;
/// fontSize|1|Font Size|0
$--font-size-small: 13px !default;
/// fontSize|1|Font Size|0
$--font-size-extra-small: 12px !default;
/// fontWeight|1|Font Weight|1
$--font-weight-primary: 500 !default;
/// fontWeight|1|Font Weight|1
$--font-weight-secondary: 100 !default;
/// fontLineHeight|1|Line Height|2
$--font-line-height-primary: 24px !default;
/// fontLineHeight|1|Line Height|2
$--font-line-height-secondary: 16px !default;
$--font-color-disabled-base: #bbb !default;
/* Size
-------------------------- */
$--size-base: 14px !default;

/* z-index
-------------------------- */
$--index-normal: 1 !default;
$--index-top: 1000 !default;
$--index-popper: 2000 !default;

/* Disable base
-------------------------- */
$--disabled-fill-base: $--background-color-base !default;
$--disabled-color-base: $--color-text-placeholder !default;
$--disabled-border-base: $--border-color-light !default;

/* Icon
-------------------------- */
$--icon-color: #666 !default;
$--icon-color-base: $--color-info !default;

/* Checkbox
-------------------------- */
/// fontSize||Font|1
$--checkbox-font-size: 14px !default;
/// fontWeight||Font|1
$--checkbox-font-weight: $--font-weight-primary !default;
/// color||Color|0
$--checkbox-font-color: $--color-text-regular !default;
$--checkbox-input-height: 14px !default;
$--checkbox-input-width: 14px !default;
/// borderRadius||Border|2
$--checkbox-border-radius: $--border-radius-small !default;
/// color||Color|0
$--checkbox-background-color: $--color-white !default;
$--checkbox-input-border: $--border-base !default;

/// color||Color|0
$--checkbox-disabled-border-color: $--border-color-base !default;
$--checkbox-disabled-input-fill: #edf2fc !default;
$--checkbox-disabled-icon-color: $--color-text-placeholder !default;

$--checkbox-disabled-checked-input-fill: $--border-color-extra-light !default;
$--checkbox-disabled-checked-input-border-color: $--border-color-base !default;
$--checkbox-disabled-checked-icon-color: $--color-text-placeholder !default;

/// color||Color|0
$--checkbox-checked-font-color: $--color-primary !default;
$--checkbox-checked-input-border-color: $--color-primary !default;
/// color||Color|0
$--checkbox-checked-background-color: $--color-primary !default;
$--checkbox-checked-icon-color: $--fill-base !default;

$--checkbox-input-border-color-hover: $--color-primary !default;
/// height||Other|4
$--checkbox-bordered-height: 40px !default;
/// padding||Spacing|3
$--checkbox-bordered-padding: 9px 20px 9px 10px !default;
/// padding||Spacing|3
$--checkbox-bordered-medium-padding: 7px 20px 7px 10px !default;
/// padding||Spacing|3
$--checkbox-bordered-small-padding: 5px 15px 5px 10px !default;
/// padding||Spacing|3
$--checkbox-bordered-mini-padding: 3px 15px 3px 10px !default;
$--checkbox-bordered-medium-input-height: 14px !default;
$--checkbox-bordered-medium-input-width: 14px !default;
/// height||Other|4
$--checkbox-bordered-medium-height: 36px !default;
$--checkbox-bordered-small-input-height: 12px !default;
$--checkbox-bordered-small-input-width: 12px !default;
/// height||Other|4
$--checkbox-bordered-small-height: 32px !default;
$--checkbox-bordered-mini-input-height: 12px !default;
$--checkbox-bordered-mini-input-width: 12px !default;
/// height||Other|4
$--checkbox-bordered-mini-height: 28px !default;

/// color||Color|0
$--checkbox-button-checked-background-color: $--color-primary !default;
/// color||Color|0
$--checkbox-button-checked-font-color: $--color-white !default;
/// color||Color|0
$--checkbox-button-checked-border-color: $--color-primary !default;



/* Radio
-------------------------- */
/// fontSize||Font|1
$--radio-font-size: $--font-size-base !default;
/// fontWeight||Font|1
$--radio-font-weight: $--font-weight-primary !default;
/// color||Color|0
$--radio-font-color: $--color-text-regular !default;
$--radio-input-height: 14px !default;
$--radio-input-width: 14px !default;
/// borderRadius||Border|2
$--radio-input-border-radius: $--border-radius-circle !default;
/// color||Color|0
$--radio-input-background-color: $--color-white !default;
$--radio-input-border: $--border-base !default;
/// color||Color|0
$--radio-input-border-color: $--border-color-base !default;
/// color||Color|0
$--radio-icon-color: $--color-white !default;

$--radio-disabled-input-border-color: $--disabled-border-base !default;
$--radio-disabled-input-fill: $--disabled-fill-base !default;
$--radio-disabled-icon-color: $--disabled-fill-base !default;

$--radio-disabled-checked-input-border-color: $--disabled-border-base !default;
$--radio-disabled-checked-input-fill: $--disabled-fill-base !default;
$--radio-disabled-checked-icon-color: $--color-text-placeholder !default;

/// color||Color|0
$--radio-checked-font-color: $--color-primary !default;
/// color||Color|0
$--radio-checked-input-border-color: $--color-primary !default;
/// color||Color|0
$--radio-checked-input-background-color: $--color-white !default;
/// color||Color|0
$--radio-checked-icon-color: $--color-primary !default;

$--radio-input-border-color-hover: $--color-primary !default;

$--radio-bordered-height: 40px !default;
$--radio-bordered-padding: 12px 20px 0 10px !default;
$--radio-bordered-medium-padding: 10px 20px 0 10px !default;
$--radio-bordered-small-padding: 8px 15px 0 10px !default;
$--radio-bordered-mini-padding: 6px 15px 0 10px !default;
$--radio-bordered-medium-input-height: 14px !default;
$--radio-bordered-medium-input-width: 14px !default;
$--radio-bordered-medium-height: 36px !default;
$--radio-bordered-small-input-height: 12px !default;
$--radio-bordered-small-input-width: 12px !default;
$--radio-bordered-small-height: 32px !default;
$--radio-bordered-mini-input-height: 12px !default;
$--radio-bordered-mini-input-width: 12px !default;
$--radio-bordered-mini-height: 28px !default;

/// fontSize||Font|1
$--radio-button-font-size: $--font-size-base !default;
/// color||Color|0
$--radio-button-checked-background-color: $--color-primary !default;
/// color||Color|0
$--radio-button-checked-font-color: $--color-white !default;
/// color||Color|0
$--radio-button-checked-border-color: $--color-primary !default;
$--radio-button-disabled-checked-fill: $--border-color-extra-light !default;

/* Select
-------------------------- */
$--select-border-color-hover: $--border-color-hover !default;
$--select-disabled-border: $--disabled-border-base !default;
/// fontSize||Font|1
$--select-font-size: $--font-size-base !default;
$--select-close-hover-color: $--color-text-secondary !default;

$--select-input-color: $--color-text-placeholder !default;
$--select-multiple-input-color: #666 !default;
/// color||Color|0
$--select-input-focus-border-color: $--color-primary !default;
/// fontSize||Font|1
$--select-input-font-size: 14px !default;

$--select-option-color: $--color-text-regular !default;
$--select-option-disabled-color: $--color-text-placeholder !default;
$--select-option-disabled-background: $--color-white !default;
/// height||Other|4
$--select-option-height: 34px !default;
$--select-option-hover-background: $--background-color-base !default;
/// color||Color|0
$--select-option-selected-font-color: $--color-primary !default;
$--select-option-selected-hover: $--background-color-base !default;

$--select-group-color: $--color-info !default;
$--select-group-height: 30px !default;
$--select-group-font-size: 12px !default;

$--select-dropdown-background: $--color-white !default;
$--select-dropdown-shadow: $--box-shadow-light !default;
$--select-dropdown-empty-color: #999 !default;
/// height||Other|4
$--select-dropdown-max-height: 274px !default;
$--select-dropdown-padding: 6px 0 !default;
$--select-dropdown-empty-padding: 10px 0 !default;
$--select-dropdown-border: solid 1px $--border-color-light !default;

/* Alert
-------------------------- */
$--alert-padding: 8px 16px !default;
/// borderRadius||Border|2
$--alert-border-radius: $--border-radius-base !default;
/// fontSize||Font|1
$--alert-title-font-size: 13px !default;
/// fontSize||Font|1
$--alert-description-font-size: 12px !default;
/// fontSize||Font|1
$--alert-close-font-size: 12px !default;
/// fontSize||Font|1
$--alert-close-customed-font-size: 13px !default;

$--alert-success-color: $--color-success-lighter !default;
$--alert-info-color: $--color-info-lighter !default;
$--alert-warning-color: $--color-warning-lighter !default;
$--alert-danger-color: $--color-danger-lighter !default;

/// height||Other|4
$--alert-icon-size: 16px !default;
/// height||Other|4
$--alert-icon-large-size: 28px !default;

/* MessageBox
-------------------------- */
/// color||Color|0
$--messagebox-title-color: $--color-text-primary !default;
$--msgbox-width: 420px !default;
$--msgbox-border-radius: 4px !default;
/// fontSize||Font|1
$--messagebox-font-size: $--font-size-large !default;
/// fontSize||Font|1
$--messagebox-content-font-size: $--font-size-base !default;
/// color||Color|0
$--messagebox-content-color: $--color-text-regular !default;
/// fontSize||Font|1
$--messagebox-error-font-size: 12px !default;
$--msgbox-padding-primary: 15px !default;
/// color||Color|0
$--messagebox-success-color: $--color-success !default;
/// color||Color|0
$--messagebox-info-color: $--color-info !default;
/// color||Color|0
$--messagebox-warning-color: $--color-warning !default;
/// color||Color|0
$--messagebox-danger-color: $--color-danger !default;

/*fix dark mode message box icon top error*/
.el-message-box__status {
  top: 48.5%;
}

/* Message
-------------------------- */
$--message-shadow: $--box-shadow-base !default;
$--message-min-width: 380px !default;
$--message-background-color: #edf2fc !default;
$--message-padding: 15px 15px 15px 20px !default;
/// color||Color|0
$--message-close-icon-color: $--color-text-placeholder !default;
/// height||Other|4
$--message-close-size: 16px !default;
/// color||Color|0
$--message-close-hover-color: $--color-text-secondary !default;

/// color||Color|0
$--message-success-font-color: $--color-success !default;
/// color||Color|0
$--message-info-font-color: $--color-info !default;
/// color||Color|0
$--message-warning-font-color: $--color-warning !default;
/// color||Color|0
$--message-danger-font-color: $--color-danger !default;

/* Notification
-------------------------- */
$--notification-width: 330px !default;
/// padding||Spacing|3
$--notification-padding: 14px 26px 14px 13px !default;
$--notification-radius: 8px !default;
$--notification-shadow: $--box-shadow-light !default;
/// color||Color|0
$--notification-border-color: $--border-color-lighter !default;
$--notification-icon-size: 24px !default;
$--notification-close-font-size: $--message-close-size !default;
$--notification-group-margin-left: 13px !default;
$--notification-group-margin-right: 8px !default;
/// fontSize||Font|1
$--notification-content-font-size: $--font-size-base !default;
/// color||Color|0
$--notification-content-color: $--color-text-regular !default;
/// fontSize||Font|1
$--notification-title-font-size: 16px !default;
/// color||Color|0
$--notification-title-color: $--color-text-primary !default;

/// color||Color|0
$--notification-close-color: $--color-text-secondary !default;
/// color||Color|0
$--notification-close-hover-color: $--color-text-regular !default;

/// color||Color|0
$--notification-success-icon-color: $--color-success !default;
/// color||Color|0
$--notification-info-icon-color: $--color-info !default;
/// color||Color|0
$--notification-warning-icon-color: $--color-warning !default;
/// color||Color|0
$--notification-danger-icon-color: $--color-danger !default;

/* Input
-------------------------- */
$--input-font-size: $--font-size-base !default;
/// color||Color|0
$--input-font-color: $--color-text-regular !default;
/// height||Other|4
$--input-width: 140px !default;
/// height||Other|4
$--input-height: 40px !default;
$--input-border: $--border-base !default;
$--input-border-color: $--border-color-base !default;
/// borderRadius||Border|2
$--input-border-radius: $--border-radius-base !default;
$--input-border-color-hover: $--border-color-hover !default;
/// color||Color|0
$--input-background-color: $--color-white !default;
$--input-fill-disabled: $--disabled-fill-base !default;
$--input-color-disabled: $--font-color-disabled-base !default;
/// color||Color|0
$--input-icon-color: $--color-text-placeholder !default;
/// color||Color|0
$--input-placeholder-color: $--color-text-placeholder !default;
$--input-max-width: 314px !default;

$--input-hover-border: $--border-color-hover !default;
$--input-clear-hover-color: $--color-text-secondary !default;

$--input-focus-border: $--color-primary !default;
$--input-focus-fill: $--color-white !default;

$--input-disabled-fill: $--disabled-fill-base !default;
$--input-disabled-border: $--disabled-border-base !default;
$--input-disabled-color: $--disabled-color-base !default;
$--input-disabled-placeholder-color: $--color-text-placeholder !default;

/// fontSize||Font|1
$--input-medium-font-size: 14px !default;
/// height||Other|4
$--input-medium-height: 36px !default;
/// fontSize||Font|1
$--input-small-font-size: 13px !default;
/// height||Other|4
$--input-small-height: 32px !default;
/// fontSize||Font|1
$--input-mini-font-size: 12px !default;
/// height||Other|4
$--input-mini-height: 28px !default;

/* Cascader
-------------------------- */
/// color||Color|0
$--cascader-menu-font-color: $--color-text-regular !default;
/// color||Color|0
$--cascader-menu-selected-font-color: $--color-primary !default;
$--cascader-menu-fill: $--fill-base !default;
$--cascader-menu-font-size: $--font-size-base !default;
$--cascader-menu-radius: $--border-radius-base !default;
$--cascader-menu-border: solid 1px $--border-color-light !default;
$--cascader-menu-shadow: $--box-shadow-light !default;
$--cascader-node-background-hover: $--background-color-base !default;
$--cascader-node-color-disabled:$--color-text-placeholder !default;
$--cascader-color-empty:$--color-text-placeholder !default;
$--cascader-tag-background: #f0f2f5;

/* Group
-------------------------- */
$--group-option-flex: 0 0 (1/5) * 100% !default;
$--group-option-offset-bottom: 12px !default;
$--group-option-fill-hover: rgba($--color-black, 0.06) !default;
$--group-title-color: $--color-black !default;
$--group-title-font-size: $--font-size-base !default;
$--group-title-width: 66px !default;

/* Tab
-------------------------- */
$--tab-font-size: $--font-size-base !default;
$--tab-border-line: 1px solid #e4e4e4 !default;
$--tab-header-color-active: $--color-text-secondary !default;
$--tab-header-color-hover: $--color-text-regular !default;
$--tab-header-color: $--color-text-regular !default;
$--tab-header-fill-active: rgba($--color-black, 0.06) !default;
$--tab-header-fill-hover: rgba($--color-black, 0.06) !default;
$--tab-vertical-header-width: 90px !default;
$--tab-vertical-header-count-color: $--color-white !default;
$--tab-vertical-header-count-fill: $--color-text-secondary !default;

/* Button
-------------------------- */
/// fontSize||Font|1
$--button-font-size: $--font-size-base !default;
/// fontWeight||Font|1
$--button-font-weight: $--font-weight-primary !default;
/// borderRadius||Border|2
$--button-border-radius: $--border-radius-base !default;
/// padding||Spacing|3
$--button-padding-vertical: 12px !default;
/// padding||Spacing|3
$--button-padding-horizontal: 20px !default;

/// fontSize||Font|1
$--button-medium-font-size: $--font-size-base !default;
/// borderRadius||Border|2
$--button-medium-border-radius: $--border-radius-base !default;
/// padding||Spacing|3
$--button-medium-padding-vertical: 10px !default;
/// padding||Spacing|3
$--button-medium-padding-horizontal: 20px !default;

/// fontSize||Font|1
$--button-small-font-size: 12px !default;
$--button-small-border-radius: #{$--border-radius-base - 1} !default;
/// padding||Spacing|3
$--button-small-padding-vertical: 9px !default;
/// padding||Spacing|3
$--button-small-padding-horizontal: 15px !default;
/// fontSize||Font|1
$--button-mini-font-size: 12px !default;
$--button-mini-border-radius: #{$--border-radius-base - 1} !default;
/// padding||Spacing|3
$--button-mini-padding-vertical: 7px !default;
/// padding||Spacing|3
$--button-mini-padding-horizontal: 15px !default;

/// color||Color|0
$--button-default-font-color: $--color-text-regular !default;
/// color||Color|0
$--button-default-background-color: $--color-white !default;
/// color||Color|0
$--button-default-border-color: $--border-color-base !default;

/// color||Color|0
$--button-disabled-font-color: $--color-text-placeholder !default;
/// color||Color|0
$--button-disabled-background-color: $--color-white !default;
/// color||Color|0
$--button-disabled-border-color: $--border-color-lighter !default;

/// color||Color|0
$--button-primary-border-color: $--color-primary !default;
/// color||Color|0
$--button-primary-font-color: $--color-white !default;
/// color||Color|0
$--button-primary-background-color: $--color-primary !default;
/// color||Color|0
$--button-success-border-color: $--color-success !default;
/// color||Color|0
$--button-success-font-color: $--color-white !default;
/// color||Color|0
$--button-success-background-color: $--color-success !default;
/// color||Color|0
$--button-warning-border-color: $--color-warning !default;
/// color||Color|0
$--button-warning-font-color: $--color-white !default;
/// color||Color|0
$--button-warning-background-color: $--color-warning !default;
/// color||Color|0
$--button-danger-border-color: $--color-danger !default;
/// color||Color|0
$--button-danger-font-color: $--color-white !default;
/// color||Color|0
$--button-danger-background-color: $--color-danger !default;
/// color||Color|0
$--button-info-border-color: $--color-info !default;
/// color||Color|0
$--button-info-font-color: $--color-white !default;
/// color||Color|0
$--button-info-background-color: $--color-info !default;

$--button-hover-tint-percent: 20% !default;
$--button-active-shade-percent: 10% !default;


/* cascader
-------------------------- */
$--cascader-height: 200px !default;

/* Switch
-------------------------- */
/// color||Color|0
$--switch-on-color: $--color-primary !default;
/// color||Color|0
$--switch-off-color: $--border-color-base !default;
/// fontSize||Font|1
$--switch-font-size: $--font-size-base !default;
$--switch-core-border-radius: 10px !default;
// height||Other|4 TODO: width 代码写死的40px 所以下面这三个属性都没意义
$--switch-width: 40px !default;
// height||Other|4
$--switch-height: 20px !default;
// height||Other|4
$--switch-button-size: 16px !default;

/* Dialog
-------------------------- */
$--dialog-background-color: $--color-white !default;
$--dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !default;
/// fontSize||Font|1
$--dialog-title-font-size: $--font-size-large !default;
/// fontSize||Font|1
$--dialog-content-font-size: 14px !default;
/// fontLineHeight||LineHeight|2
$--dialog-font-line-height: $--font-line-height-primary !default;
/// padding||Spacing|3
$--dialog-padding-primary: 20px !default;

/* Table
-------------------------- */
/// color||Color|0
$--table-border-color: $--border-color-lighter !default;
$--table-border: 1px solid $--table-border-color !default;
/// color||Color|0
$--table-font-color: $--color-text-regular !default;
/// color||Color|0
$--table-header-font-color: $--color-text-secondary !default;
/// color||Color|0
$--table-row-hover-background-color: $--background-color-base !default;
$--table-current-row-background-color: $--color-primary-light-9 !default;
/// color||Color|0
$--table-header-background-color: $--color-white !default;
$--table-fixed-box-shadow: 0 0 10px rgba(0, 0, 0, .12) !default;

.el-table__row--striped {
  td {
    background: $--background-color-base !important;
  }
}

/* Pagination
-------------------------- */
/// fontSize||Font|1
$--pagination-font-size: 13px !default;
/// color||Color|0
$--pagination-background-color: $--color-white !default;
/// color||Color|0
$--pagination-font-color: $--color-text-primary !default;
$--pagination-border-radius: 3px !default;
/// color||Color|0
$--pagination-button-color: $--color-text-primary !default;
/// height||Other|4
$--pagination-button-width: 35.5px !default;
/// height||Other|4
$--pagination-button-height: 28px !default;
/// color||Color|0
$--pagination-button-disabled-color: $--color-text-placeholder !default;
/// color||Color|0
$--pagination-button-disabled-background-color: $--color-white !default;
/// color||Color|0
$--pagination-hover-color: $--color-primary !default;

/* Popup
-------------------------- */
/// color||Color|0
$--popup-modal-background-color: $--color-black !default;
/// opacity||Other|1
$--popup-modal-opacity: 0.5 !default;

/* Popover
-------------------------- */
/// color||Color|0
$--popover-background-color: $--color-white !default;
/// fontSize||Font|1
$--popover-font-size: $--font-size-base !default;
/// color||Color|0
$--popover-border-color: $--border-color-lighter !default;
$--popover-arrow-size: 6px !default;
/// padding||Spacing|3
$--popover-padding: 12px !default;
$--popover-padding-large: 18px 20px !default;
/// fontSize||Font|1
$--popover-title-font-size: 16px !default;
/// color||Color|0
$--popover-title-font-color: $--color-text-primary !default;

/* Tooltip
-------------------------- */
/// color|1|Color|0
$--tooltip-fill: $--color-text-primary !default;
/// color|1|Color|0
$--tooltip-color: $--color-white !default;
/// fontSize||Font|1
$--tooltip-font-size: 12px !default;
/// color||Color|0
$--tooltip-border-color: $--color-text-primary !default;
$--tooltip-arrow-size: 6px !default;
/// padding||Spacing|3
$--tooltip-padding: 10px !default;

/* Tag
-------------------------- */
/// color||Color|0
$--tag-info-color: $--color-info !default;
/// color||Color|0
$--tag-primary-color: $--color-primary !default;
/// color||Color|0
$--tag-success-color: $--color-success !default;
/// color||Color|0
$--tag-warning-color: $--color-warning !default;
/// color||Color|0
$--tag-danger-color: $--color-danger !default;
/// fontSize||Font|1
$--tag-font-size: 12px !default;
$--tag-border-radius: 4px !default;
$--tag-padding: 0 10px !default;

/* Tree
-------------------------- */
/// color||Color|0
$--tree-node-hover-background-color: $--background-color-base !default;
/// color||Color|0
$--tree-font-color: $--color-text-regular !default;
/// color||Color|0
$--tree-expand-icon-color: $--color-text-placeholder !default;

/* Dropdown
-------------------------- */
$--dropdown-menu-box-shadow: $--box-shadow-light !default;
$--dropdown-menuItem-hover-fill: $--color-primary-light-9 !default;
$--dropdown-menuItem-hover-color: $--link-color !default;

/* Badge
-------------------------- */
/// color||Color|0
$--badge-background-color: $--color-danger !default;
$--badge-radius: 10px !default;
/// fontSize||Font|1
$--badge-font-size: 12px !default;
/// padding||Spacing|3
$--badge-padding: 6px !default;
/// height||Other|4
$--badge-size: 18px !default;

/* Card
--------------------------*/
/// color||Color|0
$--card-border-color: $--border-color-lighter !default;
$--card-border-radius: 4px !default;
/// padding||Spacing|3
$--card-padding: 20px !default;

/* Slider
--------------------------*/
/// color||Color|0
$--slider-main-background-color: $--color-primary !default;
/// color||Color|0
$--slider-runway-background-color: $--border-color-light !default;
$--slider-button-hover-color: mix($--color-primary, black, 97%) !default;
$--slider-stop-background-color: $--color-white !default;
$--slider-disable-color: $--color-text-placeholder !default;
$--slider-margin: 16px 0 !default;
$--slider-border-radius: 3px !default;
/// height|1|Other|4
$--slider-height: 6px !default;
/// height||Other|4
$--slider-button-size: 16px !default;
$--slider-button-wrapper-size: 36px !default;
$--slider-button-wrapper-offset: -15px !default;

/* Steps
--------------------------*/
$--steps-border-color: $--disabled-border-base !default;
$--steps-border-radius: 4px !default;
$--steps-padding: 20px !default;

/* Menu
--------------------------*/
/// fontSize||Font|1
$--menu-item-font-size: $--font-size-base !default;
/// color||Color|0
$--menu-item-font-color: $--color-text-primary !default;
/// color||Color|0
$--menu-background-color: $--color-white !default;
$--menu-item-hover-fill: $--color-primary-light-9 !default;

/* Rate
--------------------------*/
$--rate-height: 20px !default;
/// fontSize||Font|1
$--rate-font-size: $--font-size-base !default;
/// height||Other|3
$--rate-icon-size: 18px !default;
/// margin||Spacing|2
$--rate-icon-margin: 6px !default;
$--rate-icon-color: $--color-text-placeholder !default;

/* DatePicker
--------------------------*/
$--datepicker-font-color: $--color-text-regular !default;
/// color|1|Color|0
$--datepicker-off-font-color: $--color-text-placeholder !default;
/// color||Color|0
$--datepicker-header-font-color: $--color-text-regular !default;
$--datepicker-icon-color: $--color-text-primary !default;
$--datepicker-border-color: $--disabled-border-base !default;
$--datepicker-inner-border-color: #e4e4e4 !default;
/// color||Color|0
$--datepicker-inrange-background-color: $--border-color-extra-light !default;
/// color||Color|0
$--datepicker-inrange-hover-background-color: $--border-color-extra-light !default;
/// color||Color|0
$--datepicker-active-color: $--color-primary !default;
/// color||Color|0
$--datepicker-hover-font-color: $--color-primary !default;
$--datepicker-cell-hover-color: #fff !default;

/* Loading
--------------------------*/
/// height||Other|4
$--loading-spinner-size: 42px !default;
/// height||Other|4
$--loading-fullscreen-spinner-size: 50px !default;

/* Scrollbar
--------------------------*/
$--scrollbar-background-color: rgba($--color-text-secondary, .3) !default;
$--scrollbar-hover-background-color: rgba($--color-text-secondary, .5) !default;

/* Carousel
--------------------------*/
/// fontSize||Font|1
$--carousel-arrow-font-size: 12px !default;
$--carousel-arrow-size: 36px !default;
$--carousel-arrow-background: rgba(31, 45, 61, 0.11) !default;
$--carousel-arrow-hover-background: rgba(31, 45, 61, 0.23) !default;
/// width||Other|4
$--carousel-indicator-width: 30px !default;
/// height||Other|4
$--carousel-indicator-height: 2px !default;
$--carousel-indicator-padding-horizontal: 4px !default;
$--carousel-indicator-padding-vertical: 12px !default;
$--carousel-indicator-out-color: $--border-color-hover !default;

/* Collapse
--------------------------*/
/// color||Color|0
$--collapse-border-color: $--border-color-lighter !default;
/// height||Other|4
$--collapse-header-height: 48px !default;
/// color||Color|0
$--collapse-header-background-color: $--color-white !default;
/// color||Color|0
$--collapse-header-font-color: $--color-text-primary !default;
/// fontSize||Font|1
$--collapse-header-font-size: 13px !default;
/// color||Color|0
$--collapse-content-background-color: $--color-white !default;
/// fontSize||Font|1
$--collapse-content-font-size: 13px !default;
/// color||Color|0
$--collapse-content-font-color: $--color-text-primary !default;

/* Transfer
--------------------------*/
$--transfer-border-color: $--border-color-lighter !default;
$--transfer-border-radius: $--border-radius-base !default;
/// height||Other|4
$--transfer-panel-width: 200px !default;
/// height||Other|4
$--transfer-panel-header-height: 40px !default;
/// color||Color|0
$--transfer-panel-header-background-color: $--background-color-base !default;
/// height||Other|4
$--transfer-panel-footer-height: 40px !default;
/// height||Other|4
$--transfer-panel-body-height: 246px !default;
/// height||Other|4
$--transfer-item-height: 30px !default;
/// height||Other|4
$--transfer-filter-height: 32px !default;

/* Header
  --------------------------*/
$--header-padding: 0 20px !default;

/* Footer
--------------------------*/
$--footer-padding: 0 20px !default;

/* Main
--------------------------*/
$--main-padding: 20px !default;

/* Timeline
--------------------------*/
$--timeline-node-size-normal: 12px !default;
$--timeline-node-size-large: 14px !default;
$--timeline-node-color: $--border-color-light !default;

/* Backtop
--------------------------*/
/// color||Color|0
$--backtop-background-color: $--color-white !default;
/// color||Color|0
$--backtop-font-color: $--color-primary !default;
/// color||Color|0
$--backtop-hover-background-color: $--border-color-extra-light !default;

/* Link
--------------------------*/
/// fontSize||Font|1
$--link-font-size: $--font-size-base !default;
/// fontWeight||Font|1
$--link-font-weight: $--font-weight-primary !default;
/// color||Color|0
$--link-default-font-color: $--color-text-regular !default;
/// color||Color|0
$--link-default-active-color: $--color-primary !default;
/// color||Color|0
$--link-disabled-font-color: $--color-text-placeholder !default;
/// color||Color|0
$--link-primary-font-color: $--color-primary !default;
/// color||Color|0
$--link-success-font-color: $--color-success !default;
/// color||Color|0
$--link-warning-font-color: $--color-warning !default;
/// color||Color|0
$--link-danger-font-color: $--color-danger !default;
/// color||Color|0
$--link-info-font-color: $--color-info !default;
/* Calendar
--------------------------*/
/// border||Other|4
$--calendar-border: $--table-border !default;
/// color||Other|4
$--calendar-selected-background-color: #F2F8FE !default;
$--calendar-cell-width: 85px !default;

/* Form
-------------------------- */
/// fontSize||Font|1
$--form-label-font-size: $--font-size-base !default;

/* Avatar
--------------------------*/
/// color||Color|0
$--avatar-font-color: #fff !default;
/// color||Color|0
$--avatar-background-color: #C0C4CC !default;
/// fontSize||Font Size|1
$--avatar-text-font-size: 14px !default;
/// fontSize||Font Size|1
$--avatar-icon-font-size: 18px !default;
/// borderRadius||Border|2
$--avatar-border-radius: $--border-radius-base !default;
/// size|1|Avatar Size|3
$--avatar-large-size: 40px !default;
/// size|1|Avatar Size|3
$--avatar-medium-size: 36px !default;
/// size|1|Avatar Size|3
$--avatar-small-size: 28px !default;

/* Break-point
--------------------------*/
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

$--breakpoints: (
  'xs' : (max-width: $--sm - 1),
  'sm' : (min-width: $--sm),
  'md' : (min-width: $--md),
  'lg' : (min-width: $--lg),
  'xl' : (min-width: $--xl)
);

$--breakpoints-spec: (
  'xs-only' : (max-width: $--sm - 1),
  'sm-and-up' : (min-width: $--sm),
  'sm-only': "(min-width: #{$--sm}) and (max-width: #{$--md - 1})",
  'sm-and-down': (max-width: $--md - 1),
  'md-and-up' : (min-width: $--md),
  'md-only': "(min-width: #{$--md}) and (max-width: #{$--lg - 1})",
  'md-and-down': (max-width: $--lg - 1),
  'lg-and-up' : (min-width: $--lg),
  'lg-only': "(min-width: #{$--lg}) and (max-width: #{$--xl - 1})",
  'lg-and-down': (max-width: $--xl - 1),
  'xl-only' : (min-width: $--xl),
);


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="stylesheet" type="text/css" id="theme-link">
    <title>Another Redis Desktop Manager</title>
  </head>
  <body>
    <!-- this script must be placed here after body -->
    <script type="text/javascript">
      const ipcRenderer = require('electron').ipcRenderer;

      function changeCSS(theme = 'light') {
        const themeHref = `static/theme/${theme}/index.css`;
        document.getElementById('theme-link').href = themeHref;
        theme == 'dark' ? document.body.classList.add('dark-mode') :
                          document.body.classList.remove('dark-mode');
      }

      function globalChangeTheme(theme) {
        ipcRenderer.invoke('changeTheme', theme).then(shouldUseDarkColors => {
          // delay to avoid stuck
          setTimeout(() => {
            changeCSS(shouldUseDarkColors ? 'dark' : 'light');
          }, 100);
        });
      }

      // triggered by OS theme changed
      ipcRenderer.on('os-theme-updated', (event, arg) => {
        // auto change only when theme set to 'system'
        if (arg.themeSource != 'system') {
          return;
        }

        setTimeout(() => {
          changeCSS(arg.shouldUseDarkColors ? 'dark' : 'light');
        }, 100);
      });

      // theme init at startup
      (() => {
        let theme = localStorage.theme;

        if (!['system', 'light', 'dark'].includes(theme)) {
          theme = 'system';
        }

        // follow system OS
        if (theme == 'system') {
          const dark = (new URL(window.location.href)).searchParams.get('dark');
          changeCSS(dark === 'true' ? 'dark' : 'light');
        }
        // setted light or dark
        else {
          changeCSS(theme);
          ipcRenderer.invoke('changeTheme', theme);
        }
      })();
    </script>

    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: pack/electron/electron-main.js
================================================
// Modules to control application life and create native browser window
const {
  app, BrowserWindow, Menu, ipcMain, dialog, nativeTheme,
} = require('electron');
const url = require('url');
const path = require('path');
const fontManager = require('./font-manager');
const winState = require('./win-state');

// disable GPU for some white screen issues
// app.disableHardwareAcceleration();
// app.commandLine.appendSwitch('disable-gpu');

global.APP_ENV = (process.env.ARDM_ENV === 'development') ? 'development' : 'production';

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

// handle uncaught exception
process.on('uncaughtException', (err, origin) => {
  if (!err) {
    return;
  }

  dialog.showMessageBoxSync(mainWindow, {
    type: 'error',
    title: 'Whoops! Uncaught Exception',
    message: err.stack,
    detail: '\nDon\'t worry, I will fix it! 😎😎\n\n'
            + 'Submit issue to: \nhttps://github.com/qishibo/AnotherRedisDesktopManager/',
  });

  process.exit();
});

// auto update
if (APP_ENV === 'production') {
  require('./update')();
}

function createWindow() {
  // get last win stage
  const lastWinStage = winState.getLastState();

  // Create the browser window.
  mainWindow = new BrowserWindow({
    x: lastWinStage.x,
    y: lastWinStage.y,
    width: lastWinStage.width,
    height: lastWinStage.height,
    icon: `${__dirname}/icons/icon.png`,
    autoHideMenuBar: true,
    webPreferences: {
      nodeIntegration: true,
      // add this to keep 'remote' module avaiable. Tips: it will be removed in electron 14
      enableRemoteModule: true,
      contextIsolation: false,
    },
  });

  if (lastWinStage.maximized) {
    mainWindow.maximize();
  }

  winState.watchClose(mainWindow);

  // and load the index.html of the app.
  if (APP_ENV === 'production') {
    // mainWindow.loadFile('index.html');
    mainWindow.loadURL(url.format({
      protocol: 'file',
      pathname: path.join(__dirname, 'index.html'),
      query: {version: app.getVersion(), dark: nativeTheme.shouldUseDarkColors},
    }));
  } else {
    mainWindow.loadURL(url.format({
      protocol: 'http',
      host: 'localhost:9988',
      query: {version: app.getVersion(), dark: nativeTheme.shouldUseDarkColors},
    }));
  }

  // Open the DevTools.
  // mainWindow.webContents.openDevTools();

  mainWindow.on('close', () => {
    mainWindow.webContents.send('closingWindow');
  });

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });

  // const contents = mainWindow.webContents;
  // // contents.openFindWindow();
  // contents.findInPage('133');
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  app.quit();
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  // if (process.platform !== 'darwin') {
  //   app.quit();
  // }
});

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  }
});

// hide window
ipcMain.on('hideWindow', () => {
  mainWindow && mainWindow.hide();
});
// minimize window
ipcMain.on('minimizeWindow', () => {
  mainWindow && mainWindow.minimize();
});
// toggle maximize
ipcMain.on('toggleMaximize', () => {
  if (mainWindow) {
    // restore failed on MacOS, use unmaximize instead
    mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
  }
});

ipcMain.handle('getMainArgs', (event, arg) => ({
  argv: process.argv,
  version: app.getVersion(),
}));

ipcMain.handle('changeTheme', (event, theme) => {
  nativeTheme.themeSource = theme;
  return nativeTheme.shouldUseDarkColors;
});

// OS theme changed
nativeTheme.on('updated', () => {
  // delay send to prevent webcontent stuck
  setTimeout(() => {
    mainWindow.webContents.send('os-theme-updated', {
      shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
      themeSource: nativeTheme.themeSource
    });
  }, 50);
});

ipcMain.handle('getTempPath', (event, arg) => app.getPath('temp'));

// for mac copy paset shortcut
if (process.platform === 'darwin') {
  const template = [
    // { role: 'appMenu' },
    {
      label: app.name,
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideothers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' },
      ],
    },
    { role: 'editMenu' },
    // { role: 'viewMenu' },
    {
      label: 'View',
      submenu: [
        ...(
          (APP_ENV === 'production') ? [] : [{ role: 'toggledevtools' }]
        ),
        { role: 'togglefullscreen' },
      ],
    },
    // { role: 'windowMenu' },
    {
      role: 'window',
      submenu: [
        { role: 'minimize' },
        { role: 'zoom' },
        { type: 'separator' },
        { role: 'front' },
        { type: 'separator' },
        // { role: 'window' }
      ],
    },
    {
      role: 'help',
      submenu: [
        {
          label: 'Learn More',
          click: async () => {
            const { shell } = require('electron');
            await shell.openExternal('https://github.com/qishibo/AnotherRedisDesktopManager');
          },
        },
      ],
    },
  ];

  menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.


================================================
FILE: pack/electron/font-manager.js
================================================
const { ipcMain } = require('electron');

ipcMain.on('get-all-fonts', (event, arg) => {
  try {
    require('font-list').getFonts().then((fonts) => {
      if (!fonts || !fonts.length) {
        fonts = [];
      }

      fonts = fonts.map(font => font.replace('"', '').replace('"', ''));

      event.sender.send('send-all-fonts', fonts);
    }).catch(e => {
      event.sender.send('send-all-fonts', ['Default Initial']);
    });
  } catch (e) {
    event.sender.send('send-all-fonts', ['Default Initial']);
  }
});


================================================
FILE: pack/electron/package.json
================================================
{
  "name": "another-redis-desktop-manager",
  "version": "1.7.1",
  "description": "A faster, better and more stable redis desktop manager.",
  "author": "Another",
  "private": true,
  "main": "electron-main.js",
  "dependencies": {
    "electron-updater": "4.6.5",
    "font-list": "^1.4.5"
  },
  "repository": "github:qishibo/AnotherRedisDesktopManager",
  "build": {
    "appId": "me.qii404.another-redis-desktop-manager",
    "productName": "Another Redis Desktop Manager",
    "artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
    "copyright": "Copyright © 2024 qii404",
    "asar": true,
    "directories": {
      "output": "build-apps",
      "buildResources": "./"
    },
    "electronVersion": "12.2.3",
    "files": [
      "!static/js/*.map",
      "!static/css/*.map",
      "!*.map"
    ],
    "publish": [{
      "provider": "github",
      "owner": "qishibo",
      "repo": "AnotherRedisDesktopManager",
      "releaseType": "prerelease"
    }],
    "win": {
      "icon": "icons/icon.ico",
      "target": [
        {"target": "nsis", "arch": ["x64", "arm64"]},
        {"target": "zip", "arch": ["x64"]}
      ]
    },
    "nsis": {
      "allowToChangeInstallationDirectory": true,
      "oneClick": false,
      "menuCategory": true,
      "allowElevation": true
    },
    "linux": {
      "icon": "icons/icon.png",
      "category": "Utility",
      "target": [
        {"target": "AppImage", "arch": ["x64", "arm64"]}
      ]
    },
    "snap": {
      "plugs": ["default", "ssh-keys"]
    },
    "mac": {
      "icon": "icons/icon.icns",
      "type": "development",
      "category": "public.app-category.developer-tools",
      "target": [
        {"target": "dmg", "arch": ["x64", "arm64"]}
      ],
      "extendInfo": {
        "ElectronTeamID": "68JN8DV835"
      }
    },
    "afterSign": "pack/scripts/notarize.js"
  }
}


================================================
FILE: pack/electron/update.js
================================================
const { session, ipcMain, net } = require('electron');
const { autoUpdater } = require('electron-updater');

// disable auto download
autoUpdater.autoDownload = false;

let mainEvent;

const update = () => {
  bindMainListener();

  ipcMain.on('update-check', (event, arg) => {
    mainEvent = event;
    autoUpdater.checkForUpdates()
      .then(() => {})
      .catch((err) => {
        // mainEvent.sender.send('update-error', err);
      });
  });

  ipcMain.on('continue-update', (event, arg) => {
    autoUpdater.downloadUpdate()
      .then(() => {})
      .catch((err) => {
        // mainEvent.sender.send('update-error', err);
      });
  });
};

function bindMainListener() {
  autoUpdater.on('checking-for-update', () => {});

  autoUpdater.on('update-available', (info) => {
    mainEvent.sender.send('update-available', info);
  });

  autoUpdater.on('update-not-available', (info) => {
    mainEvent.sender.send('update-not-available', info);
  });

  autoUpdater.on('error', (err) => {
    mainEvent.sender.send('update-error', err);
  });

  autoUpdater.on('download-progress', (progressObj) => {
    mainEvent.sender.send('download-progress', progressObj);
  });

  autoUpdater.on('update-downloaded', (info) => {
    mainEvent.sender.send('update-downloaded', info);
  });
}

module.exports = update;


================================================
FILE: pack/electron/win-state.js
================================================
const { app, screen } = require('electron');
const path = require('path');
const fs = require('fs');

const winState = {
  // {x, y, width, height, maximized}
  getLastState() {
    let data = '{}';

    try {
      data = fs.readFileSync(this.getStateFile());
    } catch (err) {}

    const lastWinStage = this.parseJson(data);
    const lastX = lastWinStage.x;
    const lastY = lastWinStage.y;

    const primary = screen.getPrimaryDisplay();

    // recovery position only when app in primary screen
    // if in external screens, reset position for uncaught display issues
    if (
      lastX < 0 || lastY < 0
      || lastX > primary.workAreaSize.width || lastY > primary.workAreaSize.height
    ) {
      lastWinStage.x = null;
      lastWinStage.y = null;
    }

    // adjust extremely small window
    (lastWinStage.width < 250) && (lastWinStage.width = 1100);
    (lastWinStage.height < 250) && (lastWinStage.height = 728);

    return lastWinStage;

    // // there is some uncaught display issues when display in external screens
    // // such as windows disappears even x < width
    // let screenCanDisplay = false;
    // const displays = screen.getAllDisplays()

    // for (const display of displays) {
    //   const bounds = display.workArea;
    //   // check if there is a screen can display this position
    //   if (bounds.x * lastX > 0 && bounds.y * lastY > 0) {
    //     if (bounds.width > Math.abs(lastX) && bounds.height > Math.abs(lastY)) {
    //       screenCanDisplay = true;
    //       break;
    //     }
    //   }
    // }

    // let state = {...lastWinStage, x: null, y: null};

    // // recovery to last position
    // if (screenCanDisplay) {
    //   state.x = lastX;
    //   state.y = lastY;
    // }

    // return state;
  },

  watchClose(win) {
    win.on('close', () => {
      const winState = this.getWinState(win);

      if (!winState) {
        return;
      }

      this.saveStateToStorage(winState);
    });
  },

  getWinState(win) {
    try {
      const winBounds = win.getBounds();

      const state = {
        x: winBounds.x,
        y: winBounds.y,
        width: winBounds.width,
        height: winBounds.height,
        maximized: win.isMaximized(),
      };

      return state;
    } catch (err) {
      return false;
    }
  },

  saveStateToStorage(winState) {
    fs.writeFile(this.getStateFile(), JSON.stringify(winState), (err) => {});
  },

  getStateFile() {
    const userPath = app.getPath('userData');
    const fileName = 'ardm-win-state.json';

    return path.join(userPath, fileName);
  },

  parseJson(str) {
    let json = false;

    try {
      json = JSON.parse(str);
    } catch (err) {}

    return json;
  },
};

module.exports = winState;


================================================
FILE: pack/scripts/notarize.js
================================================
const { notarize } = require('@electron/notarize');

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== 'darwin') {
    return;
  }

  const appName = context.packager.appInfo.productFilename;

  return await notarize({
    appBundleId: 'me.qii404.another-redis-desktop-manager',
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLEID,
    appleIdPassword: process.env.APPLEID_PASSWORD,
    teamId: '68JN8DV835',
  });
};


================================================
FILE: package.json
================================================
{
  "name": "another-redis-desktop-manager",
  "version": "1.1.1",
  "description": "A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac",
  "author": "qii404",
  "private": true,
  "scripts": {
    "dev": "webpack serve --mode development --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "build": "node build/build.js",
    "electron": "cross-env ARDM_ENV=development electron pack/electron",
    "lint": "eslint --ext js,vue src --fix",
    "lint:init": "eslint --init",
    "asar": "asar pack dist dist/app.asar --unpack-dir \"{app.asar,build-apps,app.asar.unpacked}\"",
    "pack:prepare": "npm run build && cp -r pack/electron/* dist/",
    "pack:win": "electron-builder --project=dist -w -p never",
    "pack:win32": "electron-builder --project=dist -w --ia32 -p never",
    "pack:mac": "electron-builder --project=dist -m -p never",
    "pack:mas": "CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack:mac",
    "pack:linux": "electron-builder --project=dist -l -p never",
    "pack:win:publish": "electron-builder --project=dist -w -p always",
    "pack:mac:publish": "electron-builder --project=dist -m -p always",
    "pack:macm1:publish": "electron-builder --project=dist -m --arm64 -p always -c.artifactName='${productName} M1 ${arch} ${version}.${ext}'",
    "pack:linux:publish": "electron-builder --project=dist -l -p always",
    "et": "et -m",
    "sign": "cd pack/mas; bash sign.sh"
  },
  "dependencies": {
    "@qii404/json-bigint": "^1.0.0",
    "@qii404/redis-splitargs": "^1.0.1",
    "@qii404/vue-easy-tree": "^1.0.10",
    "algo-msgpack-with-bigint": "^2.1.1",
    "element-ui": "^2.4.11",
    "font-awesome": "^4.7.0",
    "getopts": "^2.3.0",
    "ioredis": "^5.3.2",
    "java-object-serialization": "^0.1.1",
    "keymaster": "^1.6.2",
    "monaco-editor": "^0.30.1",
    "node-version-compare": "^1.0.3",
    "php-serialize": "^4.0.2",
    "pickleparser": "^0.2.1",
    "protobufjs": "^6.11.2",
    "rawproto": "^0.7.6",
    "sortablejs": "^1.14.0",
    "tunnel-ssh": "^5.1.2",
    "vue": "^2.6.11",
    "vue-i18n": "^8.7.0",
    "vxe-table": "^3.9.0-rc.23"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-proposal-decorators": "^7.0.0",
    "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
    "@babel/plugin-proposal-function-sent": "^7.0.0",
    "@babel/plugin-proposal-json-strings": "^7.0.0",
    "@babel/plugin-proposal-numeric-separator": "^7.0.0",
    "@babel/plugin-proposal-throw-expressions": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/plugin-syntax-import-meta": "^7.0.0",
    "@babel/plugin-syntax-jsx": "^7.0.0",
    "@babel/plugin-transform-runtime": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@electron/notarize": "^2.3.0",
    "@vue/babel-preset-jsx": "^1.2.4",
    "asar": "^1.0.0",
    "autoprefixer": "^7.1.2",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^8.0.0",
    "babel-plugin-component": "^1.1.1",
    "chalk": "^2.0.1",
    "copy-webpack-plugin": "^4.6.0",
    "cross-env": "^5.2.0",
    "css-loader": "^0.28.0",
    "electron": "^12.2.3",
    "electron-builder": "^23.0.2",
    "element-theme-chalk": "^2.13.0",
    "eslint": "^5.14.1",
    "eslint-config-airbnb-base": "^13.1.0",
    "eslint-plugin-import": "^2.16.0",
    "eslint-plugin-vue": "^5.2.2",
    "file-loader": "^1.1.4",
    "font-list": "^1.4.5",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^4.5.2",
    "js-yaml": ">=3.13.1",
    "mini-css-extract-plugin": "^0.11.3",
    "monaco-editor-webpack-plugin": "^6.0.0",
    "node-loader": "^1.0.3",
    "node-notifier": "^5.1.2",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "postcss-url": "^7.2.1",
    "rimraf": "^2.6.0",
    "semver": "^5.3.0",
    "shelljs": "^0.8.4",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "url-loader": "^0.5.8",
    "vue-loader": "^15.9.8",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.46.0",
    "webpack-bundle-analyzer": "^4.6.1",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^4.2.2"
  },
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}


================================================
FILE: src/App.vue
================================================
<template>
  <el-container class="wrap-container" spellcheck="false">
    <!-- left aside draggable container -->
    <div class="aside-drag-container" :style="{width: sideWidth + 'px'}">
      <!-- connections -->
      <el-aside class="aside-connection">
        <Aside></Aside>
      </el-aside>

      <!-- drag area -->
      <div id="drag-resize-container">
        <div id="drag-resize-pointer"></div>
      </div>
    </div>

    <!-- right main container -->
    <el-container class='right-main-container'>
      <!-- tab container -->
      <el-main class='main-tabs-container'>
        <Tabs></Tabs>
      </el-main>
    </el-container>

    <UpdateCheck></UpdateCheck>
  </el-container>
</template>

<script>
import Aside from '@/Aside';
import Tabs from '@/components/Tabs';
import UpdateCheck from '@/components/UpdateCheck';
import addon from './addon';

export default {
  name: 'App',
  data() {
    return {
      sideWidth: 265,
    };
  },
  created() {
    this.$bus.$on('reloadSettings', () => {
      addon.reloadSettings();
    });

    // restore side bar width
    this.restoreSideBarWidth();
  },
  components: { Aside, Tabs, UpdateCheck },
  methods: {
    bindSideBarDrag() {
      const that = this;
      const dragPointer = document.getElementById('drag-resize-pointer');

      function mousemove(e) {
        const mouseX = e.x;
        const dragSideWidth = mouseX - 17;

        if ((dragSideWidth > 200) && (dragSideWidth < 1500)) {
          that.sideWidth = dragSideWidth;
        }
      }

      function mouseup(e) {
        document.documentElement.removeEventListener('mousemove', mousemove);
        document.documentElement.removeEventListener('mouseup', mouseup);

        // store side bar with
        localStorage.sideWidth = that.sideWidth;
      }

      dragPointer.addEventListener('mousedown', (e) => {
        e.preventDefault();

        document.documentElement.addEventListener('mousemove', mousemove);
        document.documentElement.addEventListener('mouseup', mouseup);
      });
    },
    restoreSideBarWidth() {
      const { sideWidth } = localStorage;
      sideWidth && (this.sideWidth = sideWidth);
    },
  },
  mounted() {
    setTimeout(() => {
      this.$bus.$emit('update-check');
    }, 2000);

    this.bindSideBarDrag();
    // addon init setup
    addon.setup();
  },
};
</script>

<style type="text/css">
html {
  height: 100%;
}
body {
  height: 100%;
  padding: 8px;
  margin: 0;
  box-sizing: border-box;
  -webkit-font-smoothing: antialiased;

  /*fix body scroll-y caused by tooltip in table*/
  overflow: hidden;
}

button, input, textarea, .vjs__tree {
  font-family: inherit !important;
}
a {
  color: #8e8d8d;
}


/*fix el-select bottom scroll bar*/
.el-scrollbar__wrap {
  overflow-x: hidden;
}

/*scrollbar style start*/
::-webkit-scrollbar {
  width: 9px;
}
/*track*/
::-webkit-scrollbar-track {
  background: #eaeaea;
  border-radius: 4px;
}
.dark-mode ::-webkit-scrollbar-track {
  background: #425057;
}
/*track hover*/
::-webkit-scrollbar-track:hover {
  background: #e0e0dd;
}
.dark-mode ::-webkit-scrollbar-track:hover {
  background: #495961;
}
/*thumb*/
::-webkit-scrollbar-thumb {
  border-radius: 8px;
  background: #c1c1c1;
}
.dark-mode ::-webkit-scrollbar-thumb {
  background: #5a6f7a;
}
/*thumb hover*/
::-webkit-scrollbar-thumb:hover {
  background: #7f7f7f;
}
.dark-mode ::-webkit-scrollbar-thumb:hover {
  background: #6a838f;
}
/*scrollbar style end*/

/*list index*/
li .list-index {
  color: #828282;
  /*font-size: 80%;*/
  user-select: none;
  margin-right: 10px;
  min-width: 28px;
}
.dark-mode li .list-index {
  color: #adacac;
}

.wrap-container {
  height: 100%;
}
.aside-drag-container {
  position: relative;
  user-select: none;
  /*max-width: 50%;*/
}
.aside-connection {
  height: 100%;
  width: 100% !important;
  border-right: 1px solid #e4e0e0;
  overflow: hidden;
}
/*fix right container imdraggable*/
.right-main-container {
  width: 10%;
}
.right-main-container .main-tabs-container {
  overflow-y: hidden;
  padding-top: 0px;
  padding-right: 4px;
}

.el-message-box .el-message-box__message {
  word-break: break-all;
  overflow-y: auto;
  max-height: 80vh;
}

#drag-resize-container {
  position: absolute;
  /*height: 100%;*/
  width: 10px;
  right: -12px;
  top: 0px;
}
#drag-resize-pointer {
  position: fixed;
  height: 100%;
  width: 10px;
  cursor: col-resize;
}
#drag-resize-pointer::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 20px;
  border-left: 1px solid #adabab;
  border-right: 1px solid #adabab;

  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}
.dark-mode #drag-resize-pointer::after {
  border-left: 1px solid #b9b8b8;
  border-right: 1px solid #b9b8b8;
}

@keyframes rotate {
  to{ transform: rotate(360deg); }
}

/*vxe-table dark-mode color*/
html .dark-mode {
  --vxe-ui-table-header-background-color: #273239 !important;
  --vxe-ui-layout-background-color: #273239 !important;
  --vxe-ui-table-row-striped-background-color: #3b4b54 !important;
  --vxe-ui-table-row-hover-background-color: #3b4b54 !important;
  --vxe-ui-table-row-hover-striped-background-color: #50646f !important;
  /*border color*/
  --vxe-ui-table-border-color: #7f8ea5 !important;
  /*font color*/
  --vxe-ui-font-color: #f3f3f4 !important;
  --vxe-ui-table-header-font-color: #f3f3f4 !important;
}
</style>


================================================
FILE: src/Aside.vue
================================================
<template>
  <div class="aside-outer-container">
    <div>
      <!-- new connection button -->
      <div class="aside-top-container">
        <el-button class='aside-setting-btn' type="primary" icon="el-icon-time" @click="$refs.commandLogDialog.show()" :title='$t("message.command_log")+" Ctrl+g"' plain></el-button>
        <el-button class='aside-setting-btn' type="primary" icon="el-icon-setting" @click="$refs.settingDialog.show()" :title='$t("message.settings")+" Ctrl+,"' plain></el-button>

        <div class="aside-new-connection-container">
          <el-button class="aside-new-connection-btn" type="info" @click="addNewConnection" icon="el-icon-circle-plus" :title='$t("message.new_connection")+" Ctrl+n"'>{{ $t('message.new_connection') }}</el-button>
        </div>
      </div>

      <!-- new connection dialog -->
      <NewConnectionDialog
        @editConnectionFinished="editConnectionFinished"
        ref="newConnectionDialog">
      </NewConnectionDialog>

      <!-- user settings -->
      <Setting ref="settingDialog"></Setting>

      <!-- redis command logs -->
      <CommandLog ref='commandLogDialog'></CommandLog>
      <!-- hot key tips dialog -->
      <HotKeys ref='hotKeysDialog'></HotKeys>
      <!-- custom shell formatter -->
      <CustomFormatter></CustomFormatter>
    </div>

    <!-- connection list -->
    <Connections ref="connections"></Connections>
  </div>
</template>

<script type="text/javascript">
import Setting from '@/components/Setting';
import Connections from '@/components/Connections';
import NewConnectionDialog from '@/components/NewConnectionDialog';
import CommandLog from '@/components/CommandLog';
import HotKeys from '@/components/HotKeys';
import CustomFormatter from '@/components/CustomFormatter';

export default {
  data() {
    return {};
  },
  components: {
    Connections, NewConnectionDialog, Setting, CommandLog, HotKeys, CustomFormatter,
  },
  methods: {
    editConnectionFinished() {
      this.$refs.connections.initConnections();
    },
    addNewConnection() {
      this.$refs.newConnectionDialog.show();
    },
    initShortcut() {
      // new connection
      this.$shortcut.bind('ctrl+n, ⌘+n', () => {
        this.$refs.newConnectionDialog.show();
        return false;
      });
      // settings
      this.$shortcut.bind('ctrl+,', () => {
        this.$refs.settingDialog.show();
        return false;
      });
      this.$shortcut.bind('⌘+,', () => {
        this.$refs.settingDialog.show();
        return false;
      });
      // logs
      this.$shortcut.bind('ctrl+g, ⌘+g', () => {
        this.$refs.commandLogDialog.show();
        return false;
      });
    },
  },
  mounted() {
    this.initShortcut();
  },
};
</script>

<style type="text/css">
  .aside-top-container {
    margin-right: 8px;
  }
  .aside-top-container .aside-new-connection-container {
    margin-right: 109px;
  }
  .aside-new-connection-container .aside-new-connection-btn {
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .aside-top-container .aside-setting-btn {
    float: right;
    width: 44px;
    margin-right: 5px;
  }

  .dark-mode .aside-top-container .el-button--info {
    color: #52a6fd;
    background: inherit;
  }
</style>


================================================
FILE: src/addon.js
================================================
import getopts from 'getopts';
import { ipcRenderer } from 'electron';
import bus from './bus';
import storage from './storage';

export default {
  setup() {
    // reload settings when init
    this.reloadSettings();
    // init args start from cli
    this.bindCliArgs();
    // bing href click
    this.openHrefInBrowser();
  },
  reloadSettings() {
    this.initFont();
    this.initZoom();
  },
  initFont() {
    const fontFamily = storage.getFontFamily();
    document.body.style.fontFamily = fontFamily;
    // tell monaco editor
    bus.$emit('fontInited', fontFamily);
  },
  initZoom() {
    let zoomFactor = storage.getSetting('zoomFactor');
    zoomFactor = zoomFactor || 1.0;

    const { webFrame } = require('electron');
    webFrame.setZoomFactor(zoomFactor);
  },
  openHrefInBrowser() {
    const { shell } = require('electron');

    document.addEventListener('click', (event) => {
      const ele = event.target;

      if (ele && (ele.nodeName.toLowerCase() === 'a') && ele.href.startsWith('http')) {
        event.preventDefault();
        shell.openExternal(ele.href);
      }
    });
  },
  bindCliArgs() {
    ipcRenderer.invoke('getMainArgs').then((result) => {
      if (!result.argv) {
        return;
      }
      const mainArgs = getopts(result.argv);

      if (!mainArgs.host) {
        return;
      }

      // common args
      const connection = {
        host: mainArgs.host,
        port: mainArgs.port ? mainArgs.port : 6379,
        auth: mainArgs.auth,
        username: mainArgs.username,
        name: mainArgs.name,
        separator: mainArgs.separator,
        connectionReadOnly: mainArgs.readonly,
      };

      // cluster args
      if (mainArgs.cluster) {
        connection.cluster = true;
      }

      // ssh args
      if (mainArgs['ssh-host'] && mainArgs['ssh-username']) {
        const sshOptions = {
          host: mainArgs['ssh-host'],
          port: mainArgs['ssh-port'] ? mainArgs['ssh-port'] : 22,
          username: mainArgs['ssh-username'],
          password: mainArgs['ssh-password'],
          privatekey: mainArgs['ssh-private-key'],
          passphrase: mainArgs['ssh-passphrase'],
          timeout: mainArgs['ssh-timeout'],
        };

        connection.sshOptions = sshOptions;
      }

      // sentinel args
      if (mainArgs['sentinel-master-name']) {
        const sentinelOptions = {
          masterName: mainArgs['sentinel-master-name'],
          nodePassword: mainArgs['sentinel-node-password'],
        };

        connection.sentinelOptions = sentinelOptions;
      }

      // ssl args
      if (mainArgs.ssl) {
        const sslOptions = {
          key: mainArgs['ssl-key'],
          ca: mainArgs['ssl-ca'],
          cert: mainArgs['ssl-cert'],
        };

        connection.sslOptions = sslOptions;
      }

      // add to storage
      storage.addConnection(connection);
      bus.$emit('refreshConnections');

      // open connection after added
      setTimeout(() => {
        bus.$emit('openConnection', connection.name);
        // tmp connection, delete it after opened
        if (!mainArgs.save) {
          storage.deleteConnection(connection);
        }
      }, 300);
    });
  },
};


================================================
FILE: src/bus.js
================================================
import Vue from 'vue';

const eventHub = new Vue();

export default {
  $on(...event) {
    eventHub.$on(...event);
  },
  $off(...event) {
    eventHub.$off(...event);
  },
  $once(...event) {
    eventHub.$once(...event);
  },
  $emit(...event) {
    eventHub.$emit(...event);
  },
};


================================================
FILE: src/commands.js
================================================
const adminCMD = {
  ACL: ['ACL CAT [categoryname]', 'ACL DELUSER username [username ...]', 'ACL DRYRUN username command [arg [arg ...]]', 'ACL GENPASS [bits]', 'ACL GETUSER username', 'ACL LIST', 'ACL LOAD', 'ACL LOG [count|RESET]', 'ACL SAVE', 'ACL SETUSER username [rule [rule ...]]', 'ACL USERS', 'ACL WHOAMI'],
  BGREWRITEAOF: 'BGREWRITEAOF',
  BGSAVE: 'BGSAVE',
  CLIENT: ['CLIENT LIST [TYPE normal|master|replica|pubsub]', 'CLIENT GETNAME', 'CLIENT REPLY ON|OFF|SKIP', 'CLIENT ID'],
  CLUSTER: ['CLUSTER COUNT-FAILURE-REPORTS node-id', 'CLUSTER COUNTKEYSINSLOT slot', 'CLUSTER GETKEYSINSLOT slot count', 'CLUSTER INFO', 'CLUSTER KEYSLOT key', 'CLUSTER NODES', 'CLUSTER SLAVES node-id', 'CLUSTER REPLICAS node-id', 'CLUSTER SLOTS'],
  CONFIG: ['CONFIG GET parameter', 'CONFIG SET parameter value', 'CONFIG RESETSTAT'],
  DEBUG: ['DEBUG OBJECT key', 'DEBUG SEGFAULT'],
  FAILOVER: 'FAILOVER [TO host port [FORCE]] [ABORT] [TIMEOUT milliseconds]',
  LATENCY: ['LATENCY DOCTOR', 'LATENCY GRAPH event', 'LATENCY HISTOGRAM [command [command ...]]', 'LATENCY HISTORY event', 'LATENCY LATEST', 'LATENCY RESET [event [event ...]]'],
  MODULE: ['MODULE LIST', 'MODULE LOAD path [arg [arg ...]]', 'MODULE UNLOAD name'],
  MONITOR: 'MONITOR',
  PFDEBUG: 'PFDEBUG',
  PFSELFTEST: 'PFSELFTEST',
  PSYNC: 'PSYNC replicationid offset',
  REPLCONF: 'REPLCONF',
  REPLICAOF: 'REPLICAOF host port',
  SAVE: 'SAVE',
  SHUTDOWN: 'SHUTDOWN [NOSAVE|SAVE] [NOW] [FORCE] [ABORT]',
  SLAVEOF: 'SLAVEOF host port',
  SLOWLOG: 'SLOWLOG subcommand [argument]',
  SYNC: 'SYNC',
};

const readCMD = {
  AUTH: 'AUTH password',
  BITCOUNT: 'BITCOUNT key [start] [end]',
  BITOP: 'BITOP operation destkey key [key ...]',
  BITPOS: 'BITPOS key bit [start] [end]',
  COMMAND: ['COMMAND COUNT', 'COMMAND GETKEYS', 'COMMAND INFO command-name [command-name ...]'],
  DBSIZE: 'DBSIZE',
  DISCARD: 'DISCARD',
  DUMP: 'DUMP key',
  ECHO: 'ECHO message',
  EXEC: 'EXEC',
  EXISTS: 'EXISTS key',
  GET: 'GET key',
  GETBIT: 'GETBIT key offset',
  GETRANGE: 'GETRANGE key start end',
  HEXISTS: 'HEXISTS key field',
  HGET: 'HGET key field',
  HGETALL: 'HGETALL key',
  HKEYS: 'HKEYS key',
  HLEN: 'HLEN key',
  HMGET: 'HMGET key field [field ...]',
  HSCAN: 'HSCAN key cursor [MATCH pattern] [COUNT count]',
  HVALS: 'HVALS key',
  INFO: 'INFO [section]',
  KEYS: 'KEYS pattern',
  LASTSAVE: 'LASTSAVE',
  LINDEX: 'LINDEX key index',
  LLEN: 'LLEN key',
  LRANGE: 'LRANGE key start stop',
  MGET: 'MGET key [key ...]',
  MULTI: 'MULTI',
  OBJECT: ['OBJECT ENCODING key', 'OBJECT FREQ key', 'OBJECT IDLETIME key', 'OBJECT REFCOUNT key'],
  PING: 'PING [message]',
  PUBSUB: ['PUBSUB CHANNELS [pattern]', 'PUBSUB NUMSUB [channel-1 ...]', 'PUBSUB NUMPAT'],
  PSUBSCRIBE: 'PSUBSCRIBE pattern [pattern ...]',
  PTTL: 'PTTL key',
  PUNSUBSCRIBE: 'PUNSUBSCRIBE [pattern ...]',
  RANDOMKEY: 'RANDOMKEY',
  ROLE: 'ROLE',
  SCAN: 'SCAN cursor [MATCH pattern] [COUNT count]',
  SCARD: 'SCARD key',
  SDIFF: 'SDIFF key [key ...]',
  SELECT: 'SELECT index',
  SINTER: 'SINTER key [key ...]',
  SISMEMBER: 'SISMEMBER key member',
  SMEMBERS: 'SMEMBERS key',
  SRANDMEMBER: 'SRANDMEMBER key [count]',
  SSCAN: 'SSCAN key cursor [MATCH pattern] [COUNT count]',
  STRLEN: 'STRLEN key',
  SUBSCRIBE: 'SUBSCRIBE channel [channel ...]',
  SUNION: 'SUNION key [key ...]',
  TIME: 'TIME',
  TTL: 'TTL key',
  TYPE: 'TYPE key',
  UNSUBSCRIBE: 'UNSUBSCRIBE [channel ...]',
  UNWATCH: 'UNWATCH',
  WATCH: 'WATCH key [key ...]',
  ZCARD: 'ZCARD key',
  ZCOUNT: 'ZCOUNT key min max',
  ZLEXCOUNT: 'ZLEXCOUNT key min max',
  ZRANGE: 'ZRANGE key start stop [WITHSCORES]',
  ZRANGEBYLEX: 'ZRANGEBYLEX key min max [LIMIT offset count]',
  ZRANGEBYSCORE: 'ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]',
  ZRANK: 'ZRANK key member',
  ZREVRANGE: 'ZREVRANGE key start stop [WITHSCORES]',
  ZREVRANGEBYLEX: 'ZREVRANGEBYLEX key max min [LIMIT offset count]',
  ZREVRANGEBYSCORE: 'ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]',
  ZREVRANK: 'ZREVRANK key member',
  ZSCAN: 'ZSCAN key cursor [MATCH pattern] [COUNT count]',
  ZSCORE: 'ZSCORE key member',
  GEOHASH: 'GEOHASH key member [member ...]',
  GEOPOS: 'GEOPOS key member [member ...]',
  GEODIST: 'GEODIST key member1 member2 [unit]',
  HSTRLEN: 'HSTRLEN key field',
  MEMORY: ['MEMORY DOCTOR', 'MEMORY HELP', 'MEMORY MALLOC-STATS', 'MEMORY STATS', 'MEMORY USAGE key [SAMPLES count]'],
  XINFO: 'XINFO [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]',
  XRANGE: 'XRANGE key start end [COUNT count]',
  XREVRANGE: 'XREVRANGE key end start [COUNT count]',
  XLEN: 'XLEN key',
  XREAD: 'XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]',
  XREADGROUP: 'XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]',
  XPENDING: 'XPENDING key group [start end count] [consumer]',
};

const writeCMD = {
  APPEND: 'APPEND key value',
  BLMOVE: 'BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout',
  BLPOP: 'BLPOP key [key ...] timeout',
  BRPOP: 'BRPOP key [key ...] timeout',
  BRPOPLPUSH: 'BRPOPLPUSH source destination timeout',
  BZPOPMAX: 'BZPOPMAX key [key ...] timeout',
  BZPOPMIN: 'BZPOPMIN key [key ...] timeout',
  COPY: 'COPY source destination [DB destination-db] [REPLACE]',
  DECR: 'DECR key',
  DECRBY: 'DECRBY key decrement',
  DEL: 'DEL key [key ...]',
  EVAL: 'EVAL script numkeys key [key ...] arg [arg ...]',
  EVALSHA: 'EVALSHA sha1 numkeys key [key ...] arg [arg ...]',
  EXPIRE: 'EXPIRE key seconds',
  EXPIREAT: 'EXPIREAT key timestamp',
  FLUSHALL: 'FLUSHALL',
  FLUSHDB: 'FLUSHDB',
  GEOADD: 'GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]',
  GETDEL: 'GETDEL key',
  GETSET: 'GETSET key value',
  HDEL: 'HDEL key field [field ...]',
  HINCRBY: 'HINCRBY key field increment',
  HINCRBYFLOAT: 'HINCRBYFLOAT key field increment',
  HMSET: 'HMSET key field value [field value ...]',
  HSET: 'HSET key field value',
  HSETNX: 'HSETNX key field value',
  INCR: 'INCR key',
  INCRBY: 'INCRBY key increment',
  INCRBYFLOAT: 'INCRBYFLOAT key increment',
  LINSERT: 'LINSERT key BEFORE|AFTER pivot value',
  LMOVE: 'LMOVE source destination LEFT|RIGHT LEFT|RIGHT',
  LPOP: 'LPOP key',
  LPUSH: 'LPUSH key value [value ...]',
  LPUSHX: 'LPUSHX key value',
  LREM: 'LREM key count value',
  LSET: 'LSET key index value',
  LTRIM: 'LTRIM key start stop',
  MIGRATE: 'MIGRATE host port key destination-db timeout',
  MOVE: 'MOVE key db',
  MSET: 'MSET key value [key value ...]',
  MSETNX: 'MSETNX key value [key value ...]',
  PERSIST: 'PERSIST key',
  PEXPIRE: 'PEXPIRE key milliseconds',
  PEXPIREAT: 'PEXPIREAT key milliseconds-timestamp',
  PSETEX: 'PSETEX key milliseconds value',
  PUBLISH: 'PUBLISH channel message',
  RENAME: 'RENAME key newkey',
  RENAMENX: 'RENAMENX key newkey',
  RESTORE: 'RESTORE key ttl serialized-value',
  RPOP: 'RPOP key',
  RPOPLPUSH: 'RPOPLPUSH source destination',
  RPUSH: 'RPUSH key value [value ...]',
  RPUSHX: 'RPUSHX key value',
  SADD: 'SADD key member [member ...]',
  SCRIPT: ['SCRIPT EXISTS script [script ...]', 'SCRIPT FLUSH', 'SCRIPT KILL', 'SCRIPT LOAD script'],
  SDIFFSTORE: 'SDIFFSTORE destination key [key ...]',
  SET: 'SET key value',
  SETBIT: 'SETBIT key offset value',
  SETEX: 'SETEX key seconds value',
  SETNX: 'SETNX key value',
  SETRANGE: 'SETRANGE key offset value',
  SINTERSTORE: 'SINTERSTORE destination key [key ...]',
  SMOVE: 'SMOVE source destination member',
  SORT: 'SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]',
  SPOP: 'SPOP key',
  SREM: 'SREM key member [member ...]',
  SUNIONSTORE: 'SUNIONSTORE destination key [key ...]',
  SWAPDB: 'SWAPDB index1 index2',
  UNLINK: 'UNLINK key [key ...]',
  XADD: 'XADD key ID field string [field string ...]',
  XDEL: 'XDEL key ID [ID ...]',
  XGROUP: ['XGROUP CREATE key groupname id|$ [MKSTREAM]', 'XGROUP CREATECONSUMER key groupname consumername', 'XGROUP DELCONSUMER key groupname consumername', 'XGROUP DESTROY key groupname', 'XGROUP SETID key groupname id|$'],
  XTRIM: 'XTRIM key MAXLEN [~] count',
  ZADD: 'ZADD key score member [score] [member]',
  ZDIFFSTORE: 'ZDIFFSTORE destination numkeys key [key ...]',
  ZINCRBY: 'ZINCRBY key increment member',
  ZINTERSTORE: 'ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
  ZPOPMAX: 'ZPOPMAX key [count]',
  ZPOPMIN: 'ZPOPMIN key [count]',
  ZRANGESTORE: 'ZRANGESTORE dst src min max [BYSCORE|BYLEX] [REV] [LIMIT offset count]',
  ZREM: 'ZREM key member [member ...]',
  ZREMRANGEBYLEX: 'ZREMRANGEBYLEX key min max',
  ZREMRANGEBYRANK: 'ZREMRANGEBYRANK key start stop',
  ZREMRANGEBYSCORE: 'ZREMRANGEBYSCORE key min max',
  ZUNIONSTORE: 'ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]',
};

module.exports = {
  allCMD: { ...adminCMD, ...readCMD, ...writeCMD },
  writeCMD,
};


================================================
FILE: src/components/CliContent.vue
================================================
<template>
  <div class="cli-content-container">
    <!-- monaco editor div -->
    <div class="monaco-editor-con" ref="editor"></div>
  </div>
</template>

<script type="text/javascript">
// import * as monaco from 'monaco-editor';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

export default {
  props: {
    content: { type: String, default: () => {} },
  },
  created() {
    // listen font family change and reset options
    // to avoid cursor offset
    this.$bus.$on('fontInited', this.changeFont);
  },
  watch: {
    // refresh
    content(newVal) {
      this.monacoEditor.setValue(newVal);
    },
  },
  methods: {
    changeFont(fontFamily) {
      this.monacoEditor && this.monacoEditor.updateOptions({
        fontFamily,
      });
    },
    scrollToBottom() {
      this.monacoEditor.revealLine(this.monacoEditor.getModel().getLineCount());
    },
  },

  mounted() {
    this.monacoEditor = monaco.editor.create(
      this.$refs.editor,
      {
        value: this.content,
        theme: 'vs-dark',
        language: 'plaintext',
        links: false,
        readOnly: true,
        cursorStyle: 'underline-thin',
        lineNumbers: 'off',
        contextmenu: false,
        // set fontsize and family to avoid cursor offset
        fontSize: 14,
        fontFamily: this.$storage.getFontFamily(),
        showFoldingControls: 'always',
        // auto layout, performance cost
        automaticLayout: true,
        wordWrap: 'on',
        // wordWrapColumn: 120,
        // long text indent when wrapped
        wrappingIndent: 'none',
        // cursor line highlight
        renderLineHighlight: 'none',
        // highlight word when cursor in
        occurrencesHighlight: false,
        // disable scroll one page at last line
        scrollBeyondLastLine: false,
        // hide scroll sign of current line
        hideCursorInOverviewRuler: true,
        minimap: {
          enabled: false,
        },
        // vertical line
        guides: {
          indentation: false,
          highlightActiveIndentation: false,
        },
        scrollbar: {
          useShadows: false,
          verticalScrollbarSize: '9px',
          horizontalScrollbarSize: '9px',
        },
      },
    );

    // hide tooltip in readonly mode
    const messageContribution = this.monacoEditor.getContribution('editor.contrib.messageController');
    this.monacoEditor.onDidAttemptReadOnlyEdit(() => {
      messageContribution.dispose();
    });
  },
  destroyed() {
    this.monacoEditor.dispose();
    this.$bus.$off('fontInited', this.changeFont);
  },
};
</script>

<style type="text/css">
  .cli-content-container .monaco-editor-con {
    min-height: 150px;
    height: calc(100vh - 123px);
    clear: both;
    overflow: hidden;
    background: #263238;
    border: 1px solid #e4e7ed;
    border-bottom: 0px;
    border-radius: 4px 4px 0 0;
  }
  .dark-mode .cli-content-container .monaco-editor-con {
    background: #324148;
    border-color: #7f8ea5;
  }

  /* font color*/
  .cli-content-container .monaco-editor-con .mtk1 {
    color: #d3d5d9;
  }
  .dark-mode .cli-content-container .monaco-editor-con .mtk1 {
    color: #e8e8e8;
  }

  /*hide cursor*/
  .cli-content-container .monaco-editor .cursors-layer > .cursor {
    display: none !important;
  }

  /*change default scrollbar style*/
  .cli-content-container .monaco-editor .scrollbar {
    background: #eaeaea;
    border-radius: 4px;
  }
  .dark-mode .cli-content-container .monaco-editor .scrollbar {
    background: #425057;
  }
  .cli-content-container .monaco-editor .scrollbar:hover {
    background: #e0e0dd;
  }
  .dark-mode .cli-content-container .monaco-editor .scrollbar:hover {
    background: #495961;
  }

  .cli-content-container .monaco-editor-con .monaco-editor .slider {
    border-radius: 4px;
    background: #c1c1c1;
  }
  .dark-mode .cli-content-container .monaco-editor-con .monaco-editor .slider {
    background: #5a6f7a;
  }
  .cli-content-container .monaco-editor-con .monaco-editor .slider:hover {
    background: #7f7f7f;
  }
  .dark-mode .cli-content-container .monaco-editor-con .monaco-editor .slider:hover {
    background: #6a838f;
  }

  /*remove background color*/
  .cli-content-container .monaco-editor .margin {
    background-color: inherit;
  }

  .cli-content-container .monaco-editor-con .monaco-editor,
  .cli-content-container .monaco-editor-con .monaco-editor-background,
  .cli-content-container .monaco-editor-con .monaco-editor .inputarea.ime-input {
    background-color: inherit;
  }
</style>


================================================
FILE: src/components/CliTab.vue
================================================
<template>
  <div class="tab-cli">
    <!-- result container -->
    <CliContent ref="editor" :content="contentStr"></CliContent>

    <!-- input params -->
    <el-autocomplete
      class="input-suggestion"
      autocomplete="off"
      v-model="params"
      :debounce='10'
      :disabled='subscribeMode || monitorMode'
      :fetch-suggestions="inputSuggestion"
      :placeholder="$t('message.enter_to_exec')"
      :select-when-unmatched="true"
      :trigger-on-focus="false"
      popper-class="cli-console-suggestion"
      ref="cliParams"
      @select='$refs.cliParams.focus()'
      @keyup.enter.native="consoleExec"
      @keyup.up.native="searchUp"
      @keyup.down.native="searchDown">
    </el-autocomplete>

    <!-- stop sub\monitor btn -->
    <el-button v-if='subscribeMode' @click='stopSubscribe' type='danger' class='stop-subscribe'>Stop Subscribe</el-button>
    <el-button v-else-if='monitorMode' @click='stopMonitor' type='danger' class='stop-subscribe'>Stop Monitor</el-button>
  </div>
</template>

<script type="text/javascript">
import { allCMD } from '@/commands';
import splitargs from '@qii404/redis-splitargs';
import { ipcRenderer } from 'electron';
import CliContent from '@/components/CliContent';

export default {
  data() {
    return {
      params: '',
      content: [],
      historyIndex: 0,
      inputSuggestionItems: [],
      multiQueue: null,
      subscribeMode: false,
      monitorMode: false,
      maxHistory: 2000,
    };
  },
  props: ['client', 'hotKeyScope'],
  components: { CliContent },
  computed: {
    paramsTrim() {
      return this.params.replace(/^\s+|\s+$/g, '');
    },
    paramsArr() {
      try {
        // buf array
        const paramsArr = splitargs(this.paramsTrim, true);
        // command to string
        paramsArr[0] = paramsArr[0].toString().toLowerCase();

        return paramsArr;
      } catch (e) {
        return [this.paramsTrim];
      }
    },
    contentStr() {
      if (this.content.length > this.maxHistory) {
        // this.content = this.content.slice(-this.maxHistory);
        this.content.splice(0, this.content.length - this.maxHistory);
      }

      return `${this.content.join('\n')}\n`;
    },
  },
  created() {
    this.$bus.$on('changeDb', (client, dbIndex) => {
      if (!this.anoClient || client.options.connectionName != this.anoClient.options.connectionName) {
        return;
      }

      if (this.anoClient.condition.select == dbIndex) {
        return;
      }

      this.anoClient.select(dbIndex);
    });
  },
  methods: {
    initShow() {
      if (!this.client) {
        return;
      }

      // copy to another client
      this.anoClient = this.client.duplicate();
      // bind subscribe messages
      this.bindSubscribeMessage();
      this.scrollToBottom('> connecting......');

      this.anoClient.on('ready', () => {
        !this.anoClient.cliInited && this.initCliContent();
        this.anoClient.cliInited = true;
      });

      this.$nextTick(() => {
        this.$refs.cliParams.focus();
      });
    },
    initCliContent() {
      this.scrollToBottom(`> ${this.anoClient.options.connectionName} connected!`);
    },
    tabClick() {
      this.$nextTick(() => {
        this.$refs.cliParams.focus();
      });
    },
    inputSuggestion(input, cb) {
      // tmp store cb
      this.cb = cb;

      if (!this.paramsTrim) {
        cb([]);
        return;
      }

      let items = this.inputSuggestionItems.filter(item => item.toLowerCase().indexOf(input.toLowerCase()) !== -1);

      // add cmd tips
      items = this.addCMDTips(items);

      const suggestions = [...new Set(items)].map(item => ({ value: item }));

      cb(suggestions);
    },
    addCMDTips(items = []) {
      const { paramsArr } = this;
      const paramsLen = paramsArr.length;
      const cmd = paramsArr[0].toUpperCase();

      if (!cmd) {
        return items;
      }

      for (const key in allCMD) {
        if (key.startsWith(cmd)) {
          const tip = allCMD[key];
          // single tip
          if (typeof tip === 'string') {
            items.unshift(tip);
          }

          // with sub commands, such as CONFIG SET/GET...
          else {
            items = tip.concat(items);
          }
        }
      }

      return items;
    },
    bindSubscribeMessage() {
      // bind subscribe message
      this.anoClient.on('message', (channel, message) => {
        this.scrollToBottom(`\n${channel}\n${message}`);
      });

      // bind psubscribe message
      this.anoClient.on('pmessage', (pattern, channel, message) => {
        this.scrollToBottom(`\n${pattern}\n${channel}\n${message}`);
      });
    },
    stopSubscribe() {
      this.subscribeMode = false;
      const subSet = this.anoClient.condition.subscriber.set;

      if (!subSet) {
        return;
      }

      Object.keys(subSet.subscribe).length && this.anoClient.unsubscribe();
      Object.keys(subSet.psubscribe).length && this.anoClient.punsubscribe();
    },
    stopMonitor() {
      this.monitorMode = false;
      this.monitorInstance && this.monitorInstance.disconnect();
    },
    consoleExec() {
      const params = this.paramsTrim;
      const { paramsArr } = this;

      this.params = '';
      this.content.push(`> ${params}`);

      // append to history command
      this.appendToHistory(params);

      if (paramsArr[0] == 'exit' || paramsArr[0] == 'quit') {
        return this.$bus.$emit('removePreTab');
      }

      if (paramsArr[0] == 'clear') {
        return this.content = [];
      }

      // mock help command
      if (paramsArr[0] == 'help') {
        return this.scrollToBottom('Input your command and select from tips');
      }

      // multi-exec mode
      if (paramsArr[0] == 'multi') {
        this.multiQueue = [];
        return this.scrollToBottom('OK');
      }

      // multi-discard-mode
      if (paramsArr[0] == 'discard') {
      // discard when not multi condition
        if (!Array.isArray(this.multiQueue)) {
          return this.scrollToBottom('(error) ERR DISCARD without MULTI');
        }
        this.multiQueue = null;
        return this.scrollToBottom('OK');
      }

      // multi dequeue
      if (paramsArr[0] == 'exec') {
        // exec when not multi condition
        if (!Array.isArray(this.multiQueue)) {
          return this.scrollToBottom('(error) ERR EXEC without MULTI');
        }

        this.anoClient.multi(this.multiQueue).execBuffer((err, reply) => {
          if (err) {
            this.content.push(`${err}`);
          } else {
            this.content.push(this.resolveResult(reply).trim());
          }

          this.scrollToBottom();
        });

        return this.multiQueue = null;
      }

      // multi enqueue
      if (Array.isArray(this.multiQueue)) {
        this.multiQueue.push(['callBuffer', paramsArr[0], ...paramsArr.slice(1)]);
        return this.scrollToBottom('QUEUED');
      }

      // subscribe command
      if (/subscribe/.test(paramsArr[0])) {
        this.subscribeMode = true;
      }

      // monitor command
      if (paramsArr[0] == 'monitor') {
        this.anoClient.monitor().then((monitor) => {
          this.monitorMode = true;
          this.scrollToBottom('OK');
          this.monitorInstance = monitor;
          this.monitorInstance.on('monitor', (time, args, source, database) => {
            this.scrollToBottom(`${time} [${database} ${source}] ${args.join(' ')}`);
          });
        });

        return;
      }

      // normal command
      const promise = this.anoClient.callBuffer(paramsArr[0], paramsArr.slice(1));

      // normal command promise
      promise.then((reply) => {
        this.content.push(this.resolveResult(reply).trim());
        this.execFinished(paramsArr);
        this.scrollToBottom();
      }).catch((err) => {
        this.multiQueue = null;
        this.scrollToBottom(err.message);
      });
    },
    execFinished(params) {
      const operate = params[0].toLowerCase();

      if (operate === 'select' && !isNaN(params[1])) {
        this.$bus.$emit('changeDb', this.anoClient, params[1]);
      }

      // operate may add new key, refresh left key list
      if (['hmset', 'hset', 'lpush', 'rpush', 'set', 'sadd', 'zadd', 'xadd', 'json.set'].includes(operate)) {
        this.$bus.$emit('refreshKeyList', this.client, Buffer.from(params[1]), 'add');
      }
      if (['del'].includes(operate)) {
        this.$bus.$emit('refreshKeyList', this.client, Buffer.from(params[1]), 'del');
      }
    },
    scrollToBottom(append = '') {
      append && (this.content.push(`${append}`));

      this.$nextTick(() => {
        if (this.$refs.editor) {
          return this.$refs.editor.scrollToBottom();
        }

        if (!this.$refs.cliContent) {
          return;
        }

        const textarea = this.$refs.cliContent.$el.firstChild;
        textarea.scrollTop = textarea.scrollHeight;
      });
    },
    appendToHistory(params) {
      if (!params || !params.length) {
        return;
      }

      const items = this.inputSuggestionItems;

      if (items[items.length - 1] !== params) {
        items.push(params);
      }

      this.historyIndex = items.length;
    },
    resolveResult(result) {
      let append = '';

      // list or dict
      if (typeof result === 'object' && result !== null && !Buffer.isBuffer(result)) {
        const isArray = Array.isArray(result);

        for (const i in result) {
          // list or dict
          if (typeof result[i] === 'object' && result[i] !== null && !Buffer.isBuffer(result[i])) {
            // fix ioredis pipline result such as [[null, "v1"], [null, "v2"]]
            // null is the result, and v1 is the value
            if (result[i][0] === null) {
              append += this.resolveResult(result[i][1]);
            } else {
              append += this.resolveResult(result[i]);
            }
          }
          // string buffer null
          else {
            append += `${(isArray ? '' : (`${this.$util.bufToString(i)}\n`))
                      + this.$util.bufToString(result[i])}\n`;
          }
        }
      }
      // string buffer null
      else {
        append = `${this.$util.bufToString(result)}\n`;
      }

      return append;
    },
    searchUp() {
      if (this.suggesttionShowing()) {
        return;
      }

      (--this.historyIndex < 0) && (this.historyIndex = 0);

      if (!this.inputSuggestionItems[this.historyIndex]) {
        this.params = '';
        return;
      }

      this.params = this.inputSuggestionItems[this.historyIndex];
    },
    searchDown() {
      if (this.suggesttionShowing()) {
        return;
      }

      if (++this.historyIndex > this.inputSuggestionItems.length) {
        this.historyIndex = this.inputSuggestionItems.length;
      }

      if (!this.inputSuggestionItems[this.historyIndex]) {
        this.params = '';
        return;
      }

      this.params = this.inputSuggestionItems[this.historyIndex];
    },
    suggesttionShowing() {
      const ele = document.querySelector('.cli-console-suggestion');

      if (ele && ele.style.display != 'none') {
        return true;
      }

      return false;
    },
    initShortcut() {
      // this.$shortcut.bind('ctrl+c', this.hotKeyScope, () => {
      //   this.params = '';
      //   this.scrollToBottom('> ^C');
      //   // close the tips
      //   (typeof this.cb == 'function') && this.cb([]);
      // });
      this.$shortcut.bind('ctrl+l, ⌘+l', this.hotKeyScope, () => {
        this.content = [];
      });
    },
    initHistoryTips() {
      const key = this.$storage.getStorageKeyByName('cli_tip', this.client.options.connectionName);
      const tips = localStorage.getItem(key);

      this.inputSuggestionItems = tips ? JSON.parse(tips) : [];

      ipcRenderer.on('closingWindow', (event, arg) => {
        this.storeCommandTips();
      });
    },
    storeCommandTips() {
      const key = this.$storage.getStorageKeyByName('cli_tip', this.client.options.connectionName);
      localStorage.setItem(key, JSON.stringify(this.inputSuggestionItems.slice(-200)));
    },
  },
  mounted() {
    this.initShow();
    this.initShortcut();
    this.initHistoryTips();
  },
  beforeDestroy() {
    this.anoClient && this.anoClient.quit && this.anoClient.quit();
    this.$shortcut.deleteScope(this.hotKeyScope);
    this.storeCommandTips();
  },
};
</script>

<style type="text/css">
  .tab-cli .input-suggestion {
    width: 100%;
    margin-top: 2px;
  }

  .tab-cli .input-suggestion input {
    color: #babdc1;
    background: #263238;
    border-top: 0px;
    border-radius: 0 0 4px 4px;
  }
  .dark-mode .tab-cli .input-suggestion input  {
    color: #f7f7f7;
    background: #324148;
  }

  .tab-cli .input-suggestion input::-webkit-input-placeholder {
    color: #8a8b8e;
  }

  .tab-cli .stop-subscribe {
    position: fixed;
    right: 34px;
    bottom: 68px;
  }
</style>


================================================
FILE: src/components/CommandLog.vue
================================================
<template>
<el-dialog @open='openDialog' :title="$t('message.command_log')" :visible.sync="visible" custom-class='command-log-dialog' width="90%" append-to-body>
  <!-- key list -->
  <div class="command-log-list">
    <vxe-table
      ref="commandLogList"
      size="mini" max-height="100%"
      border="none" show-overflow="title"
      :scroll-y="{enabled: true}"
      :row-config="{isHover: true, height: 24}"
      :column-config="{resizable: true}"
      :empty-text="$t('el.table.emptyText')"
      :data="logsShow">
      <vxe-column field="time" title="Time" width="90"></vxe-column>
      <vxe-column field="name" title="Connection" width="168"></vxe-column>
      <vxe-column field="cmd" title="CMD" width="130" class-name="command-cmd"></vxe-column>
      <vxe-column field="args" title="Args" min-width="90"></vxe-column>
      <vxe-column field="cost" title="Cost(ms)" width="90" class-name="command-cost"></vxe-column>
    </vxe-table>
  </div>

  <!-- filter -->
  <el-input v-model='filter' size='mini' style='max-width: 200px;' :placeholder="$t('message.key_to_search')"></el-input>&nbsp;
  <!-- show only write commands -->
  <el-checkbox v-model='showOnlyWrite'>Only Write</el-checkbox>

  <div slot="footer" class="dialog-footer">
    <el-button @click="logs=[]">{{ $t('el.colorpicker.clear') }}</el-button>
    <el-button @click="visible=false">{{ $t('el.messagebox.cancel') }}</el-button>
  </div>
</el-dialog>
</template>

<script type="text/javascript">
import { writeCMD } from '@/commands.js';
import { VxeTable, VxeColumn } from 'vxe-table';

export default {
  data() {
    return {
      visible: false,
      logs: [],
      maxLength: 5000,
      filter: '',
      showOnlyWrite: false,
    };
  },
  components: { VxeTable, VxeColumn, },
  created() {
    this.$bus.$on('commandLog', (record) => {
      // hide ping
      if (record.command.name === 'ping') {
        return;
      }

      this.logs.push({
        cmd: record.command.name,
        args: (record.command.name === 'auth') ? '***' : record.command.args.map(item => (item.length > 100 ? (`${item.slice(0, 100)}...`) : item.toString())).join(' '),
        cost: record.cost.toFixed(2),
        time: record.time.toTimeString().substr(0, 8),
        name: record.connectionName,
      });

      this.logs.length > this.maxLength && (this.logs = this.logs.slice(-this.maxLength));
      this.visible && this.scrollToBottom();
    });
  },
  computed: {
    logsShow() {
      let { logs } = this;

      if (this.showOnlyWrite) {
        logs = logs.filter(item => writeCMD[item.cmd.toUpperCase()]);
      }

      if (this.filter) {
        logs = logs.filter(item => item.cmd.includes(this.filter) || item.args.includes(this.filter));
      }

      return logs;
    },
  },
  methods: {
    show() {
      this.visible = true;
    },
    openDialog() {
      this.scrollToBottom();
    },
    scrollToBottom() {
      setTimeout(() => {
        this.$refs.commandLogList &&
          this.$refs.commandLogList.scrollTo(0, 99999999);
      }, 0);
    },
  },
};
</script>

<style type="text/css">
  .command-log-dialog.el-dialog {
    margin-top: 10vh !important;
  }
  .command-log-list {
    padding: 6px;
    min-height: 150px;
    height: calc(90vh - 307px);
    border: 1px solid grey;
    border-radius: 5px;
    margin-bottom: 12px;
  }

  .command-log-list .command-cmd {
    font-weight: bold;
    font-size: 110%;
  }
  .command-log-list .command-cost {
    color: #e59090;
  }
</style>


================================================
FILE: src/components/ConnectionMenu.vue
================================================
<template>
<div class="connection-menu-title">
  <div class="connection-opt-icons">
    <!-- right menu operate icons -->
    <i :title="$t('message.redis_status')"
      class="connection-right-icon fa fa-home"
      :style="{ color: client ? '#7cad7c' : ''}"
      @click.stop.prevent="openStatus">
    </i>
    <i :title="$t('message.redis_console')"
      class="connection-right-icon fa fa-terminal font-weight-bold"
      @click.stop.prevent="openCli">
    </i>
    <i :title="$t('message.refresh_connection')"
      class='connection-right-icon el-icon-refresh font-weight-bold'
      @click.stop.prevent="refreshConnection">
    </i>

    <!-- more operate menu -->
    <el-dropdown
      class='connection-menu-more'
      placement='bottom-start'
      :show-timeout=100
      :hide-timeout=300>
      <i class="connection-right-icon el-icon-menu" @click.stop></i>
      <el-dropdown-menu class='connection-menu-more-ul' slot="dropdown">


        <el-dropdown-item @click.native='closeConnection'>
          <span><i class='more-operate-ico fa fa-power-off'></i>&nbsp;{{ $t('message.close_connection') }}</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='showEditConnection'>
          <span><i class='more-operate-ico el-icon-edit-outline'></i>&nbsp;{{ $t('message.edit_connection') }}</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='deleteConnection'>
          <span><i class='more-operate-ico el-icon-delete'></i>&nbsp;{{ $t('message.del_connection') }}</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='duplicateConnection'>
          <span><i class='more-operate-ico fa fa-clone'></i>&nbsp;{{ $t('message.duplicate_connection') }}</span>
        </el-dropdown-item>

        <!-- menu color picker -->
        <el-tooltip placement="right" effect="light">
          <el-color-picker
            slot='content'
            v-model="menuColor"
            @change='changeColor'
            :predefine="['#f56c6c', '#F5C800', '#409EFF', '#85ce61', '#c6e2ff']">
          </el-color-picker>

          <el-dropdown-item divided>
            <span><i class='more-operate-ico fa fa-bookmark-o'></i>&nbsp;{{ $t('message.mark_color') }}</span>
          </el-dropdown-item>
        </el-tooltip>

        <el-dropdown-item @click.native='memoryAnalisys'>
          <span><i class='more-operate-ico fa fa-table'></i>&nbsp;{{ $t('message.memory_analysis') }}</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='slowLog'>
          <span><i class='more-operate-ico fa fa-hourglass-start'></i>&nbsp;{{ $t('message.slow_log') }}</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='importKeys' divided>
          <span><i class='more-operate-ico el-icon-download'></i>&nbsp;{{ $t('message.import') }} Key</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='execFileCMDS'>
          <span><i class='more-operate-ico fa fa-file-code-o'></i>&nbsp;{{ $t('message.import') }} CMD</span>
        </el-dropdown-item>
        <el-dropdown-item @click.native='flushDB'>
          <span><i class='more-operate-ico fa fa-exclamation-triangle'></i>&nbsp;{{ $t('message.flushdb') }}</span>
        </el-dropdown-item>

      </el-dropdown-menu>
    </el-dropdown>
  </div>
  <div :title="connectionTitle()" class="connection-name">{{config.connectionName}}
    <!-- <i v-if="client" style="position: absolute; left: 2px; bottom: 5px; width: 8px; height: 8px; border-radius: 4px; background-color: green;"></i> -->
  </div>

  <!-- edit connection dialog -->
  <NewConnectionDialog
    editMode='true'
    :config='config'
    @editConnectionFinished='editConnectionFinished'
    ref='editConnectionDialog'>
  </NewConnectionDialog>
</div>
</template>

<script type="text/javascript">
import storage from '@/storage.js';
import { remote } from 'electron';
import NewConnectionDialog from '@/components/NewConnectionDialog';
import splitargs from '@qii404/redis-splitargs';

export default {
  data() {
    return {
      menuColor: '#409EFF',
    };
  },
  props: ['config', 'client'],
  components: { NewConnectionDialog },
  created() {
    this.$bus.$on('duplicateConnection', (newConfig) => {
      // not self
      if (this.config.name !== newConfig.name) {
        return;
      }

      this.showEditConnection();
    });
  },
  methods: {
    connectionTitle() {
      const { config } = this;
      const sep = '-----------';
      const lines = [
        config.connectionName,
        sep,
        `${this.$t('message.host')}: ${config.host}`,
        `${this.$t('message.port')}: ${config.port}`,
      ];

      config.username && lines.push(`${this.$t('message.username')}: ${config.username}`);
      config.separator && lines.push(`${this.$t('message.separator')}: "${config.separator}"`);

      if (config.connectionReadOnly) {
        lines.push(`${sep}\nREADONLY`);
      }
      if (config.sshOptions) {
        lines.push(`${sep}\nSSH:`);
        lines.push(`  ${this.$t('message.host')}: ${config.sshOptions.host}`);
        lines.push(`  ${this.$t('message.port')}: ${config.sshOptions.port}`);
        lines.push(`  ${this.$t('message.username')}: ${config.sshOptions.username}`);
      }
      if (config.cluster) {
        lines.push(`${sep}\nCLUSTER`);
      }
      if (config.sentinelOptions) {
        lines.push(`${sep}\nSENTINEL:`);
        lines.push(`  ${this.$t('message.master_group_name')}: ${config.sentinelOptions.masterName}`);
      }

      return lines.join('\n');
    },
    refreshConnection() {
      this.$emit('refreshConnection');
    },
    showEditConnection() {
      // connection is cloesd, do not display confirm
      if (!this.client) {
        return this.$refs.editConnectionDialog.show();
      }

      this.$confirm(
        this.$t('message.close_to_edit_connection'),
        { type: 'warning' },
      ).then(() => {
        this.$bus.$emit('closeConnection', this.config.connectionName);
        this.$refs.editConnectionDialog.show();
      }).catch(() => {});
    },
    closeConnection() {
      this.$confirm(
        this.$t('message.close_to_connection'),
        { type: 'warning' },
      ).then(() => {
        this.$bus.$emit('closeConnection', this.config.connectionName);
      }).catch(() => {});
    },
    editConnectionFinished(newConfig) {
      this.$bus.$emit('refreshConnections');
    },
    duplicateConnection() {
      // empty key\order , just as a new connection
      const newConfig = {
        ...this.config,
        key: undefined,
        order: undefined,
        connectionName: undefined,
      };

      storage.addConnection(newConfig);

      this.$bus.$emit('refreshConnections');
      // 100ms after connection list is ready
      setTimeout(() => {
        this.$bus.$emit('duplicateConnection', newConfig);
      }, 100);
    },
    deleteConnection() {
      this.$confirm(
        this.$t('message.confirm_to_delete_connection'),
        { type: 'warning' },
      ).then(() => {
        storage.deleteConnection(this.config);
        this.$bus.$emit('refreshConnections');

        this.$message.success({
          message: this.$t('message.delete_success'),
          duration: 1000,
        });
      }).catch(() => {});
    },
    openStatus() {
      if (!this.client) {
        // open Connections.vue menu
        this.$parent.$parent.$parent.$refs.connectionMenu.open(this.config.connectionName);
        // open connection
        this.$parent.$parent.$parent.openConnection();
      } else {
        this.$bus.$emit('openStatus', this.client, this.config.connectionName);
      }
    },
    openCli() {
      // open cli before connection opened
      if (!this.client) {
        // open Connections.vue menu
        this.$parent.$parent.$parent.$refs.connectionMenu.open(this.config.connectionName);
        // open connection
        this.$parent.$parent.$parent.openConnection(() => {
          this.$bus.$emit('openCli', this.client, this.config.connectionName);
        });
      } else {
        this.$bus.$emit('openCli', this.client, this.config.connectionName);
      }
    },
    memoryAnalisys() {
      if (!this.client) {
        return;
      }

      this.$bus.$emit('memoryAnalysis', this.client, this.config.connectionName);
    },
    slowLog() {
      if (!this.client) {
        return;
      }

      this.$bus.$emit('slowLog', this.client, this.config.connectionName);
    },
    importKeys() {
      remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
        properties: ['openFile'],
      }).then((reply) => {
        if (reply.canceled) {
          return;
        }

        const succ = [];
        const fail = [];
        let count = 0;

        const rl = require('readline').createInterface({
          input: require('fs').createReadStream(reply.filePaths[0]),
        });

        rl.on('line', (line) => {
          let [key, content, ttl] = line.split(',');

          if (!key || !content) {
            return;
          }

          count++;

          // show notify in first time
          if (count === 1) {
            this.$notify.success({
              message: this.$createElement('p', { ref: 'importKeysNotify' }, ''),
              duration: 0,
            });
          }

          key = Buffer.from(key, 'hex');
          content = Buffer.from(content, 'hex');
          ttl = ttl > 0 ? ttl : 0;

          // fix #1213, REPLACE can be used in Redis>=3.0
          this.client.callBuffer('RESTORE', key, ttl, content, 'REPLACE').then((reply) => {
            // reply == 'OK'
            succ.push(key);
          }).catch((e) => {
            fail.push(key);
          }).finally(() => {
            this.$set(this.$refs.importKeysNotify,
              'innerHTML',
              `Succ: ${succ.length}, Fail: ${fail.length}`);
          });
        });

        rl.on('close', () => {
          if (count === 0) {
            return this.$message.error('File parse failed.');
          }

          (count > 10000) && this.$message.success({
            message: this.$t('message.import_success'),
            duration: 800,
          });

          // refresh keu list
          this.$bus.$emit('refreshKeyList', this.client);
        });
      });
    },
    execFileCMDS() {
      remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
        properties: ['openFile'],
      }).then((reply) => {
        if (reply.canceled) {
          return;
        }

        const succ = [];
        const fail = [];
        let count = 0;

        const rl = require('readline').createInterface({
          input: require('fs').createReadStream(reply.filePaths[0]),
        });

        rl.on('line', (line) => {
          const paramsArr = splitargs(line, true);

          if (!paramsArr || !paramsArr.length) {
            return;
          }

          count++;

          // show notify in first time
          if (count === 1) {
            this.$notify.success({
              message: this.$createElement('p', { ref: 'importCMDNotify' }, ''),
              duration: 0,
            });
          }

          this.client.callBuffer(...paramsArr).then((reply) => {
            succ.push(line);
          }).catch((e) => {
            fail.push(line);
          }).finally(() => {
            this.$set(this.$refs.importCMDNotify,
              'innerHTML',
              `Succ: ${succ.length}, Fail: ${fail.length}`
            );
          });
        });

        rl.on('close', () => {
          if (count === 0) {
            return this.$message.error('File parse failed.');
          }

          (count > 10000) && this.$message.success({
            message: this.$t('message.import_success'),
            duration: 800,
          });

          // refresh key list
          this.$bus.$emit('refreshKeyList', this.client);
        });
      });
    },
    flushDB() {
      if (!this.client) {
        return;
      }

      const preDB = this.client.condition ? this.client.condition.select : 0;
      const inputTxt = 'y';
      const placeholder = this.$t('message.flushdb_prompt', { txt: inputTxt });

      this.$prompt(this.$t('message.confirm_flush_db', { db: preDB }), {
        inputValidator: value => ((value == inputTxt) ? true : placeholder),
        inputPlaceholder: placeholder,
      })
        .then((value) => {
          this.client.flushdb().then((reply) => {
            if (reply == 'OK') {
              this.$message.success({
                message: this.$t('message.delete_success'),
                duration: 1000,
              });

              this.refreshConnection();
            }
          }).catch((e) => { this.$message.error(e.message); });
        })
        .catch((e) => {});
    },
    changeColor(color) {
      this.$emit('changeColor', color);
    },
  },
};
</script>

<style type="text/css">
  .connection-menu-title {
    margin-left: -20px;
  }

  .connection-menu .connection-name {
    margin-right: 115px;
    padding-right: 6px;
    word-break: keep-all;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: bold;
    font-size: 1.04em;
  }
  .connection-menu .connection-opt-icons {
    /*width: 30px;*/
    /*float: right;
    margin-right: 28px;*/
    position: absolute;
    right: 25px;
    top: -2px;
  }
  .connection-menu .connection-right-icon {
    display: inline-block;
    font-size: 1.16em;
    /*font-weight: bold;*/
    padding: 3px;
    margin-right: -4px;
    transition: background 0.2s;
  }
  .connection-menu .connection-right-icon:hover {
    /*color: #85878a;*/
    background: #dcdee0;
    border-radius: 3px;
  }
  .dark-mode .connection-menu .connection-right-icon:hover {
    background: #58707b;
  }

  /*fix more operation btn icon vertical-center*/
  .connection-menu-more {
    vertical-align: baseline;
  }
  /*more operation ul>ico*/
  .connection-menu-more-ul .more-operate-ico {
    width: 13px;
    text-align: center;
  }

  .font-weight-bold {
    font-weight: bold;
  }
</style>


================================================
FILE: src/components/ConnectionWrapper.vue
================================================
<template>
  <el-menu
    ref="connectionMenu"
    :collapse-transition='false'
    :id="connectionAnchor"
    @open="openConnection()"
    class="connection-menu"
    active-text-color="#ffd04b">
    <el-submenu :index="config.connectionName">
      <!-- connection menu -->
      <ConnectionMenu
        slot="title"
        :config="config"
        :client='client'
        @changeColor='setColor'
        @refreshConnection='openConnection(false, true)'>
      </ConnectionMenu>

      <!-- db search operate -->
      <OperateItem
        ref='operateItem'
        :config="config"
        :client='client'>
      </OperateItem>

      <!-- key list -->
      <KeyList
        ref='keyList'
        :config="config"
        :globalSettings='globalSettings'
        :client='client'>
      </KeyList>
    </el-submenu>
  </el-menu>
</template>

<script type="text/javascript">
import redisClient from '@/redisClient.js';
import KeyList from '@/components/KeyList';
import OperateItem from '@/components/OperateItem';
import ConnectionMenu from '@/components/ConnectionMenu';

export default {
  data() {
    return {
      client: null,
      pingTimer: null,
      pingInterval: 10000, // ms
      lastSelectedDb: 0,
    };
  },
  props: ['config', 'globalSettings', 'index'],
  components: { ConnectionMenu, OperateItem, KeyList },
  created() {
    this.$bus.$on('closeConnection', (connectionName = false) => {
      this.closeConnection(connectionName);
    });
    // open connection
    this.$bus.$on('openConnection', (connectionName) => {
      if (connectionName && (connectionName == this.config.connectionName)) {
        this.openConnection();
        this.$refs.connectionMenu.open(this.config.connectionName);
      }
    });
  },
  computed: {
    connectionAnchor() {
      return `connection-anchor-${this.config.connectionName}`;
    },
  },
  methods: {
    initShow() {
      this.$refs.operateItem.initShow();
      this.$refs.keyList.initShow();
    },
    initLastSelectedDb() {
      const db = parseInt(localStorage.getItem(`lastSelectedDb_${this.config.connectionName}`));

      if (db > 0 && this.lastSelectedDb != db) {
        this.lastSelectedDb = db;
        this.$refs.operateItem && this.$refs.operateItem.setDb(db);
      }
    },
    openConnection(callback = false, forceOpen = false) {
      // scroll to connection
      this.scrollToConnection();
      // recovery last selected db
      this.initLastSelectedDb();

      // opened, do nothing
      if (this.client) {
        return forceOpen ? this.afterOpenConnection(this.client, callback) : false;
      }

      // set searching status first
      this.$refs.operateItem.searchIcon = 'el-icon-loading';

      // create a new client
      const clientPromise = this.getRedisClient(this.config);

      clientPromise.then((realClient) => {
        this.afterOpenConnection(realClient, callback);
      }).catch((e) => {});
    },
    afterOpenConnection(client, callback = false) {
      // new connection, not ready
      if (client.status != 'ready') {
        client.on('ready', () => {
          if (client.readyInited) {
            return;
          }

          client.readyInited = true;
          // open status tab
          this.$bus.$emit('openStatus', client, this.config.connectionName);
          this.startPingInterval();

          this.initShow();
          callback && callback();
        });
      }

      // connection is ready
      else {
        this.initShow();
        callback && callback();
      }
    },
    closeConnection(connectionName) {
      // if connectionName is not passed, close all connections
      if (connectionName && (connectionName != this.config.connectionName)) {
        return;
      }

      this.$refs.connectionMenu
      && this.$refs.connectionMenu.close(this.config.connectionName);
      this.$bus.$emit('removeAllTab', connectionName);

      // clear ping interval
      clearInterval(this.pingTimer);

      // reset operateItem items
      this.$refs.operateItem && this.$refs.operateItem.resetStatus();
      // reset keyList items
      this.$refs.keyList && this.$refs.keyList.resetKeyList(true);

      this.client && this.client.quit && this.client.quit();
      this.client = null;
    },
    startPingInterval() {
      this.pingTimer = setInterval(() => {
        this.client && this.client.ping().then((reply) => {}).catch((e) => {
          // this.$message.error('Ping Error: ' + e.message);
        });
      }, this.pingInterval);
    },
    getRedisClient(config) {
      // prevent changing back to raw config, such as config.db
      const configCopy = JSON.parse(JSON.stringify(config));
      // select db
      configCopy.db = this.lastSelectedDb;

      // ssh client
      if (configCopy.sshOptions) {
        var clientPromise = redisClient.createSSHConnection(
          configCopy.sshOptions, configCopy.host, configCopy.port, configCopy.auth, configCopy,
        );
      }
      // normal client
      else {
        var clientPromise = redisClient.createConnection(
          configCopy.host, configCopy.port, configCopy.auth, configCopy,
        );
      }

      clientPromise.then((client) => {
        this.client = client;

        client.on('error', (error) => {
          this.$message.error({
            message: `Client On Error: ${error} Config right?`,
            duration: 3000,
            customClass: 'redis-on-error-message',
          });

          this.$bus.$emit('closeConnection');
        });
      }).catch((error) => {
        this.$message.error(error.message);
        this.$bus.$emit('closeConnection');
      });

      return clientPromise;
    },
    setColor(color, save = true) {
      const ulDom = this.$refs.connectionMenu.$el;
      const className = 'menu-with-custom-color';

      // save to setting
      save && this.$storage.editConnectionItem(this.config, { color });

      if (!color) {
        ulDom.classList.remove(className);
      } else {
        ulDom.classList.add(className);
        this.$el.style.setProperty('--menu-color', color);
      }
    },
    scrollToConnection() {
      this.$nextTick(() => {
        // 300ms after menu expand animination
        setTimeout(() => {
          let scrollTop = 0;
          const menus = document.querySelectorAll('.connections-wrap .connections-list>ul');

          // calc height sum of all above menus
          for (const menu of menus) {
            if (menu.id === this.connectionAnchor) {
              break;
            }
            scrollTop += (menu.clientHeight + 8);
          }

          // if connections filter input exists, scroll more
          // 32 = height('.filter-input')+margin
          const offset = document.querySelector('.connections-wrap .filter-input') ? 32 : 0;
          document.querySelector('.connections-wrap').scrollTo({
            top: scrollTop + offset,
            behavior: 'smooth',
          });
        }, 320);
      });
    },
  },
  mounted() {
    this.setColor(this.config.color, false);
  },
  beforeDestroy() {
    this.closeConnection(this.config.connectionName);
  },
};
</script>

<style type="text/css">
  /*menu ul*/
  .connection-menu {
    margin-bottom: 8px;
    padding-right: 6px;
    border-right: 0;
  }

  .connection-menu.menu-with-custom-color li.el-submenu {
    border-left: 5px solid var(--menu-color);
    border-radius: 4px 0 0 4px;
    padding-left: 3px;
  }

  /*this error shows first*/
  .redis-on-error-message {
    z-index:9999 !important;
  }
</style>


================================================
FILE: src/components/Connections.vue
================================================
<template>
  <div class="connections-wrap">
    <!-- search connections input -->
    <div v-if="connections.length>=filterEnableNum" class="filter-input">
      <el-input
        v-model="filterMode"
        suffix-icon="el-icon-search"
        :placeholder="$t('message.search_connection')"
        clearable
        size="mini">
      </el-input>
    </div>

    <!-- connections list -->
    <div class="connections-list">
      <ConnectionWrapper
        v-for="item, index of filteredConnections"
        :key="item.key ? item.key : item.connectionName"
        :index="index"
        :globalSettings="globalSettings"
        :config='item'>
      </ConnectionWrapper>
    </div>

    <ScrollToTop parentNum='1' :posRight='false'></ScrollToTop>
  </div>
</template>

<script type="text/javascript">
import storage from '@/storage.js';
import ConnectionWrapper from '@/components/ConnectionWrapper';
import ScrollToTop from '@/components/ScrollToTop';
import Sortable from 'sortablejs';


export default {
  data() {
    return {
      connections: [],
      globalSettings: this.$storage.getSetting(),
      filterEnableNum: 4,
      filterMode: '',
    };
  },
  components: { ConnectionWrapper, ScrollToTop },
  created() {
    this.$bus.$on('refreshConnections', () => {
      this.initConnections();
    });
    this.$bus.$on('reloadSettings', (settings) => {
      this.globalSettings = settings;
    });
  },
  computed: {
    filteredConnections() {
      if (!this.filterMode) {
        return this.connections;
      }

      return this.connections.filter(item => {
        return item.name.toLowerCase().includes(this.filterMode.toLowerCase());
      });
    },
  },
  methods: {
    initConnections() {
      const connections = storage.getConnections(true);
      const slovedConnections = [];
      // this.connections = [];

      for (const item of connections) {
        item.connectionName = storage.getConnectionName(item);
        // fix history bug, prevent db into config
        delete item.db;
        slovedConnections.push(item);
      }

      this.connections = slovedConnections;
    },
    sortOrder() {
      const dragWrapper = document.querySelector('.connections-list');
      Sortable.create(dragWrapper, {
        handle: '.el-submenu__title',
        animation: 400,
        direction: 'vertical',
        onEnd: (e) => {
          const { newIndex } = e;
          const { oldIndex } = e;
          // change in connections
          const currentRow = this.connections.splice(oldIndex, 1)[0];
          this.connections.splice(newIndex, 0, currentRow);
          // store
          this.$storage.reOrderAndStore(this.connections);
        },
      });
    },
  },
  mounted() {
    this.initConnections();
    this.sortOrder();
  },
};
</script>

<style type="text/css">
  .connections-wrap {
    height: calc(100vh - 59px);
    overflow-y: auto;
    margin-top: 11px;
  }
  .connections-wrap .filter-input {
    padding-right: 13px;
    margin-bottom: 4px;
  }
  /* set drag area min height, target to the end will be correct */
  .connections-wrap .connections-list {
    min-height: calc(100vh - 110px);
  }
</style>


================================================
FILE: src/components/CustomFormatter.vue
================================================
<template>
  <el-dialog :title="$t('message.custom_formatter')" :visible.sync="visible" append-to-body width='60%'>
    <!-- new formatter btn -->
    <el-button size="mini" @click="addDialog=true">+ {{ $t('message.new') }}</el-button>
    <!-- formatter list -->
    <el-table :data='formatters'>
      <el-table-column
        label="Name"
        prop="name"
        width="120">
      </el-table-column>
      <el-table-column
        label="Formatter">
        <template slot-scope="scope">
          {{ formatterPreview(scope.row) }}
        </template>
      </el-table-column>
      <el-table-column
        label="Operation"
        width="90">
        <template slot-scope="scope">
          <el-button icon="el-icon-delete" type="text" @click="removeFormatter(scope.$index)"></el-button>
          <el-button icon="el-icon-edit-outline" type="text" @click="showEditDialog(scope.row)"></el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- new formatter dialog -->
    <el-dialog :close-on-click-modal='false' :title="!editMode ? $t('message.new') : $t('message.edit')"
               :visible.sync="addDialog" append-to-body
               @closed='reset'>
      <el-form label-position="top" size="mini">
        <el-form-item label="Name" required>
          <el-input v-model='formatter.name'></el-input>
        </el-form-item>

        <el-form-item label="Command" required>
          <span slot="label">
            Command
            <el-popover
              placement="top-start"
              title="Command"
              trigger="hover">
              <i slot="reference" class="el-icon-question"></i>
              <p>Executable file, such as  <el-tag>/bin/bash</el-tag>, <el-tag>/bin/node</el-tag>, <el-tag>xxx.sh</el-tag>, <el-tag>xxx.php</el-tag>, make sure it is executable</p>
            </el-popover>
          </span>
          <FileInput
            :file.sync='formatter.command'
            placeholder='/bin/bash'>
          </FileInput>
        </el-form-item>

        <el-form-item label="Params">
          <span slot="label">
            Params
            <el-popover
              placement="top-start"
              title="Params"
              trigger="hover">
              <i slot="reference" class="el-icon-question"></i>
              <p>
                Command params, such as "--key
                <el-tag>{KEY}</el-tag> --value <el-tag>{VALUE}</el-tag>"<hr>
                <b>Template variables to be replaced:</b>
                <table>
                  <tr>
                    <td>[String]</td>
                    <td><el-tag>{VALUE}</el-tag></td>
                  </tr>
                  <tr>
                    <td>[Hash]</td>
                    <td><el-tag>{FIELD}</el-tag> <el-tag>{VALUE}</el-tag></td>
                  </tr>
                  <tr>
                    <td>[List]</td>
                    <td><el-tag>{VALUE}</el-tag></td>
                  </tr>
                  <tr>
                    <td>[Set]</td>
                    <td><el-tag>{VALUE}</el-tag></td>
                  </tr>
                  <tr>
                    <td>[Zset]</td>
                    <td><el-tag>{SCORE}</el-tag> <el-tag>{MEMBER}</el-tag></td>
                  </tr>
                </table>
                <hr>
                If your value is unvisible, you can pass <el-tag>{HEX}</el-tag> instead of <el-tag>{VALUE}</el-tag><br>
                then hex such as <i>68656c6c6f20776f726c64</i> will be passed
                <hr>
                If your value is too long(>8000), it will be writen to a file,<br> you can use <el-tag>{HEX_FILE}</el-tag> to get the path and read in your script,<br>
                the content in this file is same with <el-tag>{HEX}</el-tag>
              </p>
            </el-popover>
          </span>
          <el-input v-model='formatter.params' placeholder='--value "{VALUE}"'></el-input>
        </el-form-item>

        <el-form-item label="">
          <p>{{ formatterPreview(formatter) }}</p>
        </el-form-item>
      </el-form>

      <div slot="footer" class="dialog-footer">
        <el-button @click="addDialog=false">{{ $t('el.messagebox.cancel') }}</el-button>
        <el-button type="primary" @click="editFormatter">{{ $t('el.messagebox.confirm') }}</el-button>
      </div>
    </el-dialog>
  </el-dialog>
</template>

<script type="text/javascript">
import storage from '@/storage';
import FileInput from '@/components/FileInput';

export default {
  data() {
    return {
      visible: false,
      addDialog: false,
      editMode: false,
      formatter: { name: '', command: '', params: '' },
    };
  },
  components: { FileInput },
  computed: {
    formatters() {
      return storage.getCustomFormatter();
    },
  },
  created() {
    this.$bus.$on('addCustomFormatter', () => {
      this.show();
    });
  },
  methods: {
    show() {
      this.visible = true;
    },
    reset() {
      this.editMode = false;
      this.formatter = { name: '', command: '', params: '' };
    },
    formatterPreview(row) {
      return `${row.command} ${row.params}`;
    },
    showEditDialog(row) {
      this.formatter = row;
      this.addDialog = true;
      this.editMode = true;
    },
    editFormatter() {
      if (!this.formatter.name || !this.formatter.command) {
        return false;
      }

      // add mode
      if (!this.editMode) {
        this.formatters.push(this.formatter);
      }

      this.saveSetting();
      this.addDialog = false;
    },
    removeFormatter(index) {
      this.formatters.splice(index, 1);
      this.saveSetting();
    },
    saveSetting() {
      storage.saveCustomFormatters(this.formatters);
      this.$bus.$emit('refreshViewers');
    },
  },
};
</script>


================================================
FILE: src/components/DeleteBatch.vue
================================================
<template>
<div>
  <el-card class="box-card del-batch-card">
    <!-- card title -->
    <div slot="header" class="clearfix">
      <span class="del-title"><i class="fa fa-exclamation-triangle"></i> {{ $t('message.keys_to_be_deleted') }}</span>
      <i v-if="loadingScan||loadingDelete" class='el-icon-loading'></i>
      <el-tag size="mini">
        <span v-if="loadingScan">Scanning... </span>
        <span v-if="loadingDelete">Deleting... </span>
        Total: {{ allKeysList.length }}
      </el-tag>

      <!-- del btn -->
      <el-button @click="confirmDelete" :disabled="loadingScan||loadingDelete||allKeysList.length == 0" style="float: right;" type="danger">{{ $t('message.delete_all') }}</el-button>
      <!-- toggle scanning btn -->
      <el-button v-if="rule.pattern.length && !scanningEnd" @click="toggleScanning()" type="text" style="float: right;">{{loadingScan ? $t('message.pause') : $t('message.begin')}}&nbsp;</el-button>
    </div>

    <!-- scan pattern -->
    <el-tag v-if="rule.pattern && rule.pattern.length" size="mini" style="margin-left: 10px;">
      <i class="fa fa-search"></i> {{rule.pattern.join(' ')}}
    </el-tag>

    <!-- key list -->
    <RecycleScroller
      class="del-batch-key-list"
      :items="allKeysList"
      :item-size="20"
      key-field="str"
      v-slot="{ item, index }"
    >
      <li>
        <span class="list-index">{{ index + 1 }}.</span>
        <span class="key-name" :title="item.str">{{ item.str }}</span>
      </li>
    </RecycleScroller>
  </el-card>
</div>
</template>

<script type="text/javascript">
import { RecycleScroller } from 'vue-virtual-scroller';

export default {
  data() {
    return {
      loadingScan: false,
      loadingDelete: false,
      scanStreams: [],
      allKeysList: [],
      scanningEnd: false,
    };
  },
  props: ['client', 'rule', 'hotKeyScope'],
  components: { RecycleScroller },
  methods: {
    initKeys() {
      this.allKeysList = [];
      this.rule.key && this.rule.key.length && this.addToList(this.rule.key);

      if (this.rule.pattern && this.rule.pattern.length) {
        this.loadingScan = true;

        for (const pattern of this.rule.pattern) {
          this.initScanStreamsAndScan(pattern);
        }
      }
    },
    initScanStreamsAndScan(pattern) {
      const nodes = this.client.nodes ? this.client.nodes('master') : [this.client];
      this.scanningCount = nodes.length;

      nodes.map((node) => {
        const scanOption = {
          match: `${pattern}*`,
          count: 20000,
        };

        const stream = node.scanBufferStream(scanOption);
        this.scanStreams.push(stream);

        stream.on('data', (keys) => {
          this.addToList(keys.sort());

          // pause for dom rendering
          stream.pause();
          setTimeout(() => {
            this.loadingScan && stream.resume();
          }, 100);
        });

        stream.on('error', (e) => {
          this.loadingScan = false;
          this.$message.error({
            message: `Delete Batch Stream On Error: ${e.message}`,
            duration: 1500,
          });
        });

        stream.on('end', () => {
          // all nodes scan finished(cusor back to 0)
          if (--this.scanningCount <= 0) {
            this.loadingScan = false;
            this.scanningEnd = true;
          }
        });
      });
    },
    addToList(keys) {
      const list = [];
      for (const key of keys) {
        list.push({ key, str: this.$util.bufToString(key) })
Download .txt
gitextract_k6_5a8j7/

├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE.md
│   └── workflows/
│       ├── build_linux.yml
│       ├── build_mac.yml
│       ├── build_windows.yml
│       ├── codeql-analysis.yml.bak
│       ├── gen_sponsors.yaml
│       └── publish_winget.yml
├── .gitignore
├── .jshintrc
├── .postcssrc.js
├── LICENSE
├── PRIVACY.md
├── README.md
├── README.zh-CN.md
├── SECURITY.md
├── babel.config.json
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── element-variables.scss
├── index.html
├── pack/
│   ├── electron/
│   │   ├── electron-main.js
│   │   ├── font-manager.js
│   │   ├── icons/
│   │   │   └── icon.icns
│   │   ├── package.json
│   │   ├── update.js
│   │   └── win-state.js
│   └── scripts/
│       └── notarize.js
├── package.json
├── src/
│   ├── App.vue
│   ├── Aside.vue
│   ├── addon.js
│   ├── bus.js
│   ├── commands.js
│   ├── components/
│   │   ├── CliContent.vue
│   │   ├── CliTab.vue
│   │   ├── CommandLog.vue
│   │   ├── ConnectionMenu.vue
│   │   ├── ConnectionWrapper.vue
│   │   ├── Connections.vue
│   │   ├── CustomFormatter.vue
│   │   ├── DeleteBatch.vue
│   │   ├── FileInput.vue
│   │   ├── FormatViewer.vue
│   │   ├── HotKeys.vue
│   │   ├── InputBinary.vue
│   │   ├── InputPassword.vue
│   │   ├── JsonEditor.vue
│   │   ├── KeyDetail.vue
│   │   ├── KeyHeader.vue
│   │   ├── KeyList.vue
│   │   ├── KeyListNormal.vue
│   │   ├── KeyListVirtualTree.vue
│   │   ├── LanguageSelector.vue
│   │   ├── MemoryAnalysis.vue
│   │   ├── NewConnectionDialog.vue
│   │   ├── OperateItem.vue
│   │   ├── PaginationTable.vue
│   │   ├── RightClickMenu.vue
│   │   ├── ScrollToTop.vue
│   │   ├── Setting.vue
│   │   ├── SlowLog.vue
│   │   ├── Status.vue
│   │   ├── Tabs.vue
│   │   ├── UpdateCheck.vue
│   │   ├── contents/
│   │   │   ├── KeyContentHash.vue
│   │   │   ├── KeyContentList.vue
│   │   │   ├── KeyContentReJson.vue
│   │   │   ├── KeyContentSet.vue
│   │   │   ├── KeyContentStream.vue
│   │   │   ├── KeyContentString.vue
│   │   │   └── KeyContentZset.vue
│   │   └── viewers/
│   │       ├── ViewerBinary.vue
│   │       ├── ViewerBrotli.vue
│   │       ├── ViewerCustom.vue
│   │       ├── ViewerDeflate.vue
│   │       ├── ViewerDeflateRaw.vue
│   │       ├── ViewerGzip.vue
│   │       ├── ViewerHex.vue
│   │       ├── ViewerJavaSerialize.vue
│   │       ├── ViewerJson.vue
│   │       ├── ViewerMsgpack.vue
│   │       ├── ViewerOverSize.vue
│   │       ├── ViewerPHPSerialize.vue
│   │       ├── ViewerPickle.vue
│   │       ├── ViewerProtobuf.vue
│   │       └── ViewerText.vue
│   ├── i18n/
│   │   ├── i18n.js
│   │   └── langs/
│   │       ├── cn.js
│   │       ├── de.js
│   │       ├── en.js
│   │       ├── es.js
│   │       ├── fr.js
│   │       ├── it.js
│   │       ├── ko.js
│   │       ├── pt.js
│   │       ├── ru.js
│   │       ├── tr.js
│   │       ├── tw.js
│   │       ├── ua.js
│   │       └── vi.js
│   ├── main.js
│   ├── redisClient.js
│   ├── router/
│   │   └── index.js
│   ├── shortcut.js
│   ├── storage.js
│   └── util.js
└── static/
    ├── .gitkeep
    └── theme/
        ├── dark/
        │   └── index.css
        └── light/
            └── index.css
Download .txt
SYMBOL INDEX (92 symbols across 12 files)

FILE: build/check-versions.js
  function exec (line 7) | function exec (cmd) {

FILE: build/utils.js
  function generateLoaders (line 34) | function generateLoaders (loader, loaderOptions) {

FILE: build/webpack.base.conf.js
  function resolve (line 7) | function resolve (dir) {
  method name (line 41) | name(resourcePath, resourceQuery) {

FILE: build/webpack.dev.conf.js
  constant HOST (line 16) | const HOST = process.env.HOST
  constant PORT (line 17) | const PORT = process.env.PORT && Number(process.env.PORT)

FILE: pack/electron/electron-main.js
  function createWindow (line 42) | function createWindow() {

FILE: pack/electron/update.js
  function bindMainListener (line 30) | function bindMainListener() {

FILE: pack/electron/win-state.js
  method getLastState (line 7) | getLastState() {
  method watchClose (line 63) | watchClose(win) {
  method getWinState (line 75) | getWinState(win) {
  method saveStateToStorage (line 93) | saveStateToStorage(winState) {
  method getStateFile (line 97) | getStateFile() {
  method parseJson (line 104) | parseJson(str) {

FILE: src/addon.js
  method setup (line 7) | setup() {
  method reloadSettings (line 15) | reloadSettings() {
  method initFont (line 19) | initFont() {
  method initZoom (line 25) | initZoom() {
  method openHrefInBrowser (line 32) | openHrefInBrowser() {
  method bindCliArgs (line 44) | bindCliArgs() {

FILE: src/bus.js
  method $on (line 6) | $on(...event) {
  method $off (line 9) | $off(...event) {
  method $once (line 12) | $once(...event) {
  method $emit (line 15) | $emit(...event) {

FILE: src/redisClient.js
  method createConnection (line 52) | createConnection(host, port, auth, config, promise = true, forceStandalo...
  method createSSHConnection (line 90) | createSSHConnection(sshOptions, host, port, auth, config) {
  method getSSHOptions (line 166) | getSSHOptions(options, host, port) {
  method getRedisOptions (line 203) | getRedisOptions(host, port, auth, config) {
  method getSentinelOptions (line 225) | getSentinelOptions(host, port, auth, config) {
  method getClusterOptions (line 242) | getClusterOptions(redisOptions, natMap = {}) {
  method getClusterNodes (line 252) | getClusterNodes(nodes, type = 'master') {
  method createClusterSSHTunnels (line 277) | createClusterSSHTunnels(sshConfig, nodes) {
  method initNatMap (line 315) | initNatMap(tunnels) {
  method getTLSOptions (line 325) | getTLSOptions(options) {
  method retryStragety (line 344) | retryStragety(times, connection) {
  method getFileContent (line 357) | getFileContent(file, bookmark = '') {

FILE: src/storage.js
  method getSetting (line 6) | getSetting(key) {
  method saveSettings (line 12) | saveSettings(settings) {
  method getFontFamily (line 16) | getFontFamily() {
  method getCustomFormatter (line 30) | getCustomFormatter(name = '') {
  method saveCustomFormatters (line 44) | saveCustomFormatters(formatters = []) {
  method addConnection (line 47) | addConnection(connection) {
  method getConnections (line 50) | getConnections(returnList = false) {
  method editConnectionByKey (line 62) | editConnectionByKey(connection, oldKey = '') {
  method editConnectionItem (line 82) | editConnectionItem(connection, items = {}) {
  method updateConnectionName (line 94) | updateConnectionName(connection, connections) {
  method getConnectionName (line 107) | getConnectionName(connection) {
  method setConnections (line 110) | setConnections(connections) {
  method deleteConnection (line 113) | deleteConnection(connection) {
  method getConnectionKey (line 122) | getConnectionKey(connection, forceUnique = false) {
  method sortConnections (line 137) | sortConnections(connections) {
  method reOrderAndStore (line 152) | reOrderAndStore(connections = []) {
  method getStorageKeyMap (line 165) | getStorageKeyMap(type) {
  method initStorageKey (line 175) | initStorageKey(prefix, connectionName) {
  method getStorageKeyByName (line 178) | getStorageKeyByName(type = 'cli_tip', connectionName = '') {
  method hookAfterDelConnection (line 181) | hookAfterDelConnection(connection) {

FILE: src/util.js
  method get (line 3) | get(name) {
  method set (line 6) | set(name, value) {
  method bufVisible (line 9) | bufVisible(buf) {
  method bufToString (line 16) | bufToString(buf, forceHex = false) {
  method bufToQuotation (line 31) | bufToQuotation(buf) {
  method bufToHex (line 35) | bufToHex(buf) {
  method xToBuffer (line 45) | xToBuffer(str) {
  method bufToBinary (line 59) | bufToBinary(buf) {
  method binaryStringToBuffer (line 68) | binaryStringToBuffer(str) {
  method cutString (line 74) | cutString(string, maxLength = 20) {
  method isJson (line 81) | isJson(string) {
  method isPHPSerialize (line 89) | isPHPSerialize(str) {
  method isJavaSerialize (line 99) | isJavaSerialize(buf) {
  method isPickle (line 108) | isPickle(buf) {
  method isMsgpack (line 117) | isMsgpack(buf) {
  method isBrotli (line 129) | isBrotli(buf) {
  method isGzip (line 132) | isGzip(buf) {
  method isDeflate (line 135) | isDeflate(buf) {
  method isDeflateRaw (line 138) | isDeflateRaw(buf) {
  method isProtobuf (line 141) | isProtobuf(buf) {
  method zippedToString (line 164) | zippedToString(buf, type = 'unzip') {
  method base64Encode (line 184) | base64Encode(str) {
  method base64Decode (line 187) | base64Decode(str) {
  method humanFileSize (line 190) | humanFileSize(size = 0) {
  method leftTime (line 197) | leftTime(seconds) {
  method cloneObjWithBuff (line 218) | cloneObjWithBuff(object) {
  method keysToList (line 229) | keysToList(keys) {
  method keysToTree (line 240) | keysToTree(keys, separator = ':', openStatus = {}, forceCut = 20000) {
  method formatTreeData (line 268) | formatTreeData(tree, previousKey = '', openStatus = {}, separator = ':',...
  method sortKeysAndFolder (line 304) | sortKeysAndFolder(nodes) {
  method sortByTreeNodes (line 325) | sortByTreeNodes(nodes) {
  method copyToClipboard (line 345) | copyToClipboard(text) {
  method debounce (line 349) | debounce(func, wait, immediate = false, context = null) {
  method randomString (line 375) | randomString(len = 5) {
  method createAndDownloadFile (line 378) | createAndDownloadFile(fileName, content) {
  method arrayChunk (line 388) | arrayChunk(arr, size) {
Condensed preview — 121 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,037K chars).
[
  {
    "path": ".editorconfig",
    "chars": 220,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".eslintrc.json",
    "chars": 358,
    "preview": "{\n    \"env\": {\n        \"browser\": true,\n        \"es6\": true\n    },\n    \"extends\": [\"eslint:recommended\", \"airbnb-base\", "
  },
  {
    "path": ".gitattributes",
    "chars": 35,
    "preview": "*.vue linguist-language=JavaScript\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 599,
    "preview": "# These are supported funding model platforms\n\ngithub: qishibo\npatreon: # Replace with a single Patreon username\nopen_co"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 154,
    "preview": "## OS\n\nWindows or Linux or Mac\n\n## VERSION\n\nVersion in settings\n\n\n## ISSUE DESCRIPTION\n\nBug reproduction process and con"
  },
  {
    "path": ".github/workflows/build_linux.yml",
    "chars": 488,
    "preview": "name: build_linux\n\non:\n  release:\n    types: [published]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    strategy:\n    "
  },
  {
    "path": ".github/workflows/build_mac.yml",
    "chars": 1018,
    "preview": "name: build_mac\n\non:\n  release:\n    types: [published]\n\njobs:\n  build:\n\n    runs-on: macos-latest\n\n    env:\n      GH_TOK"
  },
  {
    "path": ".github/workflows/build_windows.yml",
    "chars": 489,
    "preview": "name: build_windows\n\non:\n  release:\n    types: [published]\n\njobs:\n  build:\n\n    runs-on: windows-latest\n\n    strategy:\n "
  },
  {
    "path": ".github/workflows/codeql-analysis.yml.bak",
    "chars": 2331,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/gen_sponsors.yaml",
    "chars": 1117,
    "preview": "name: Generate Sponsors To README\non:\n  workflow_dispatch\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ub"
  },
  {
    "path": ".github/workflows/publish_winget.yml",
    "chars": 337,
    "preview": "name: Publish to WinGet\non:\n  release:\n    types: [released]\njobs:\n  publish:\n    runs-on: windows-latest\n    steps:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 160,
    "preview": ".DS_Store\nnode_modules/\n/dist/\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.ide"
  },
  {
    "path": ".jshintrc",
    "chars": 59,
    "preview": "{\n  \"esversion\": 6,\n  \"bitwise\": false,\n  \"freeze\": true\n}\n"
  },
  {
    "path": ".postcssrc.js",
    "chars": 173,
    "preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    \"postcss-import\": {},"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "The MIT License (MIT)\n\nCopyright (c) shibo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "PRIVACY.md",
    "chars": 1673,
    "preview": "# Privacy Policy\n\nWe takes your privacy seriously. To better protect your privacy we provide this privacy policy notice "
  },
  {
    "path": "README.md",
    "chars": 16524,
    "preview": "# Another Redis Desktop Manager\n\n<img align=\"right\" width=\"110\" src=\"https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411"
  },
  {
    "path": "README.zh-CN.md",
    "chars": 13368,
    "preview": "# Another Redis Desktop Manager\n\n<img align=\"right\" width=\"110\" src=\"https://cdn.jsdelivr.net/gh/qishibo/img/ardm/202411"
  },
  {
    "path": "SECURITY.md",
    "chars": 743,
    "preview": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf there are any vulnerabilities in **Another Redis Desktop Manager**, "
  },
  {
    "path": "babel.config.json",
    "chars": 250,
    "preview": "{\n  \"presets\": [\n    \"@babel/env\",\n    \"@vue/babel-preset-jsx\"\n  ],\n  \"sourceType\": \"unambiguous\",\n  \"plugins\": [\n    \"@"
  },
  {
    "path": "build/build.js",
    "chars": 1198,
    "preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
  },
  {
    "path": "build/check-versions.js",
    "chars": 1290,
    "preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
  },
  {
    "path": "build/utils.js",
    "chars": 2806,
    "preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\n// const ExtractTextPlugin = require('extr"
  },
  {
    "path": "build/vue-loader.conf.js",
    "chars": 553,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_"
  },
  {
    "path": "build/webpack.base.conf.js",
    "chars": 2866,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vue"
  },
  {
    "path": "build/webpack.dev.conf.js",
    "chars": 3263,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
  },
  {
    "path": "build/webpack.prod.conf.js",
    "chars": 6548,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
  },
  {
    "path": "config/dev.env.js",
    "chars": 156,
    "preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
  },
  {
    "path": "config/index.js",
    "chars": 2051,
    "preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
  },
  {
    "path": "config/prod.env.js",
    "chars": 61,
    "preview": "'use strict'\nmodule.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "element-variables.scss",
    "chars": 35624,
    "preview": "/* Element Chalk Variables */\n\n\n// Special comment for theme configurator\n// type|skipAutoTranslation|Category|Order\n// "
  },
  {
    "path": "index.html",
    "chars": 2012,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial"
  },
  {
    "path": "pack/electron/electron-main.js",
    "chars": 6158,
    "preview": "// Modules to control application life and create native browser window\nconst {\n  app, BrowserWindow, Menu, ipcMain, dia"
  },
  {
    "path": "pack/electron/font-manager.js",
    "chars": 518,
    "preview": "const { ipcMain } = require('electron');\n\nipcMain.on('get-all-fonts', (event, arg) => {\n  try {\n    require('font-list')"
  },
  {
    "path": "pack/electron/package.json",
    "chars": 1872,
    "preview": "{\n  \"name\": \"another-redis-desktop-manager\",\n  \"version\": \"1.7.1\",\n  \"description\": \"A faster, better and more stable re"
  },
  {
    "path": "pack/electron/update.js",
    "chars": 1320,
    "preview": "const { session, ipcMain, net } = require('electron');\nconst { autoUpdater } = require('electron-updater');\n\n// disable "
  },
  {
    "path": "pack/electron/win-state.js",
    "chars": 2740,
    "preview": "const { app, screen } = require('electron');\nconst path = require('path');\nconst fs = require('fs');\n\nconst winState = {"
  },
  {
    "path": "pack/scripts/notarize.js",
    "chars": 534,
    "preview": "const { notarize } = require('@electron/notarize');\n\nexports.default = async function notarizing(context) {\n  const { el"
  },
  {
    "path": "package.json",
    "chars": 4509,
    "preview": "{\n  \"name\": \"another-redis-desktop-manager\",\n  \"version\": \"1.1.1\",\n  \"description\": \"A faster, better and more stable re"
  },
  {
    "path": "src/App.vue",
    "chars": 5387,
    "preview": "<template>\n  <el-container class=\"wrap-container\" spellcheck=\"false\">\n    <!-- left aside draggable container -->\n    <d"
  },
  {
    "path": "src/Aside.vue",
    "chars": 3247,
    "preview": "<template>\n  <div class=\"aside-outer-container\">\n    <div>\n      <!-- new connection button -->\n      <div class=\"aside-"
  },
  {
    "path": "src/addon.js",
    "chars": 3200,
    "preview": "import getopts from 'getopts';\nimport { ipcRenderer } from 'electron';\nimport bus from './bus';\nimport storage from './s"
  },
  {
    "path": "src/bus.js",
    "chars": 287,
    "preview": "import Vue from 'vue';\n\nconst eventHub = new Vue();\n\nexport default {\n  $on(...event) {\n    eventHub.$on(...event);\n  },"
  },
  {
    "path": "src/commands.js",
    "chars": 8979,
    "preview": "const adminCMD = {\n  ACL: ['ACL CAT [categoryname]', 'ACL DELUSER username [username ...]', 'ACL DRYRUN username command"
  },
  {
    "path": "src/components/CliContent.vue",
    "chars": 4555,
    "preview": "<template>\n  <div class=\"cli-content-container\">\n    <!-- monaco editor div -->\n    <div class=\"monaco-editor-con\" ref=\""
  },
  {
    "path": "src/components/CliTab.vue",
    "chars": 12924,
    "preview": "<template>\n  <div class=\"tab-cli\">\n    <!-- result container -->\n    <CliContent ref=\"editor\" :content=\"contentStr\"></Cl"
  },
  {
    "path": "src/components/CommandLog.vue",
    "chars": 3499,
    "preview": "<template>\n<el-dialog @open='openDialog' :title=\"$t('message.command_log')\" :visible.sync=\"visible\" custom-class='comman"
  },
  {
    "path": "src/components/ConnectionMenu.vue",
    "chars": 14050,
    "preview": "<template>\n<div class=\"connection-menu-title\">\n  <div class=\"connection-opt-icons\">\n    <!-- right menu operate icons --"
  },
  {
    "path": "src/components/ConnectionWrapper.vue",
    "chars": 7530,
    "preview": "<template>\n  <el-menu\n    ref=\"connectionMenu\"\n    :collapse-transition='false'\n    :id=\"connectionAnchor\"\n    @open=\"op"
  },
  {
    "path": "src/components/Connections.vue",
    "chars": 3165,
    "preview": "<template>\n  <div class=\"connections-wrap\">\n    <!-- search connections input -->\n    <div v-if=\"connections.length>=fil"
  },
  {
    "path": "src/components/CustomFormatter.vue",
    "chars": 5766,
    "preview": "<template>\n  <el-dialog :title=\"$t('message.custom_formatter')\" :visible.sync=\"visible\" append-to-body width='60%'>\n    "
  },
  {
    "path": "src/components/DeleteBatch.vue",
    "chars": 6960,
    "preview": "<template>\n<div>\n  <el-card class=\"box-card del-batch-card\">\n    <!-- card title -->\n    <div slot=\"header\" class=\"clear"
  },
  {
    "path": "src/components/FileInput.vue",
    "chars": 1205,
    "preview": "<template>\n  <el-input\n    :value='file'\n    clearable\n    @clear='clearFile'\n    @focus='focus'\n    :placeholder='place"
  },
  {
    "path": "src/components/FormatViewer.vue",
    "chars": 8423,
    "preview": "<template>\n  <div class=\"format-viewer-container\">\n    <el-select v-model=\"selectedView\" :disabled='overSize' class='for"
  },
  {
    "path": "src/components/HotKeys.vue",
    "chars": 1880,
    "preview": "<template>\n<el-dialog :title=\"$t('message.hotkey')\" :visible.sync=\"visible\" custom-class='hotkey-tips-dialog' append-to-"
  },
  {
    "path": "src/components/InputBinary.vue",
    "chars": 1121,
    "preview": "<template>\n  <div>\n    <!-- <el-tag v-if=\"!buffVisible\" class='input-binary-tag' size=\"mini\">[Hex]</el-tag> -->\n    <el-"
  },
  {
    "path": "src/components/InputPassword.vue",
    "chars": 1387,
    "preview": "<template>\n  <el-input :value=\"value\" @input=\"handleInput\" :type=\"inputType\" :placeholder=\"placeholder\">\n    <i v-if=\"!h"
  },
  {
    "path": "src/components/JsonEditor.vue",
    "chars": 6825,
    "preview": "<template>\n  <div class=\"text-formated-container\">\n    <slot name='default'></slot>\n    <!-- collapse btn -->\n    <div c"
  },
  {
    "path": "src/components/KeyDetail.vue",
    "chars": 4135,
    "preview": "<template>\n  <div>\n    <el-container direction=\"vertical\" class=\"key-tab-container\">\n      <!-- key info -->\n      <KeyH"
  },
  {
    "path": "src/components/KeyHeader.vue",
    "chars": 8917,
    "preview": "<template>\n  <div>\n    <!-- key name -->\n    <div class=\"key-header-item key-name-input\">\n      <el-input\n        ref=\"k"
  },
  {
    "path": "src/components/KeyList.vue",
    "chars": 9646,
    "preview": "<template>\n  <div>\n    <!-- key list -->\n    <component\n      :is=\"keyListType\"\n      :config=\"config\"\n      :client=\"cl"
  },
  {
    "path": "src/components/KeyListNormal.vue",
    "chars": 2451,
    "preview": "<template>\n  <div>\n    <!-- key list -->\n    <ul class='key-list'>\n      <RightClickMenu\n        :items='rightMenus'\n   "
  },
  {
    "path": "src/components/KeyListVirtualTree.vue",
    "chars": 17610,
    "preview": "<template>\n  <div ref=\"treeWrapper\" class='key-list-vtree'>\n    <!-- multi operate -->\n    <div class=\"batch-operate\">\n "
  },
  {
    "path": "src/components/LanguageSelector.vue",
    "chars": 1220,
    "preview": "<template>\n  <!-- language select -->\n  <el-select v-model=\"selectedLang\" @change=\"changeLang\" placeholder=\"Language\">\n "
  },
  {
    "path": "src/components/MemoryAnalysis.vue",
    "chars": 9049,
    "preview": "<template>\n<div class=\"memory-analysis-container\">\n  <el-card class=\"box-card\">\n    <!-- card title -->\n    <div slot=\"h"
  },
  {
    "path": "src/components/NewConnectionDialog.vue",
    "chars": 11326,
    "preview": "<template>\n  <el-dialog :title=\"dialogTitle\" :visible.sync=\"dialogVisible\" :append-to-body='true' :close-on-click-modal="
  },
  {
    "path": "src/components/OperateItem.vue",
    "chars": 14143,
    "preview": "<template>\n  <!-- operate item -->\n  <el-form class=\"connection-form\" size=\"mini\">\n    <el-form-item>\n      <el-row :gut"
  },
  {
    "path": "src/components/PaginationTable.vue",
    "chars": 1313,
    "preview": "<template>\n  <div>\n    <el-table\n      :data=\"pagedData\"\n      stripe\n      size=\"small\"\n      border\n      min-height=3"
  },
  {
    "path": "src/components/RightClickMenu.vue",
    "chars": 2286,
    "preview": "<template>\n  <div @contextmenu.prevent.stop=\"show($event)\">\n    <!-- default slot -->\n    <slot name=\"default\"></slot>\n "
  },
  {
    "path": "src/components/ScrollToTop.vue",
    "chars": 2713,
    "preview": "<template>\n  <transition name=\"bounce\">\n    <div class=\"to-top-container\" :style='style' @click=\"scrollToTop\" v-if=\"toTo"
  },
  {
    "path": "src/components/Setting.vue",
    "chars": 11272,
    "preview": "<template>\n  <!-- setting dialog -->\n  <el-dialog :title=\"$t('message.settings')\" :visible.sync=\"visible\" custom-class=\""
  },
  {
    "path": "src/components/SlowLog.vue",
    "chars": 6202,
    "preview": "<template>\n<div class=\"slowlog-container\">\n  <el-card class=\"box-card\">\n    <!-- card title -->\n    <div slot=\"header\" c"
  },
  {
    "path": "src/components/Status.vue",
    "chars": 12006,
    "preview": "<template>\n<div>\n  <!-- auto refresh row -->\n  <el-row>\n    <el-col>\n      <div style=\"float: right;\">\n        <el-tag t"
  },
  {
    "path": "src/components/Tabs.vue",
    "chars": 12669,
    "preview": "<template>\n  <div>\n    <el-tabs ref=\"tabs\" class='tabs-container' v-model=\"selectedTabName\" type=\"card\" closable @tab-re"
  },
  {
    "path": "src/components/UpdateCheck.vue",
    "chars": 4062,
    "preview": "<template>\n</template>\n\n<script type=\"text/javascript\">\nimport { ipcRenderer } from 'electron';\n\nexport default {\n  data"
  },
  {
    "path": "src/components/contents/KeyContentHash.vue",
    "chars": 10827,
    "preview": "<template>\n  <div>\n    <!-- table toolbar -->\n    <div>\n      <!-- add button -->\n      <el-button type=\"primary\" @click"
  },
  {
    "path": "src/components/contents/KeyContentList.vue",
    "chars": 9257,
    "preview": "<template>\n  <div>\n    <!-- table toolbar -->\n    <div>\n      <!-- add button -->\n      <el-button type=\"primary\" @click"
  },
  {
    "path": "src/components/contents/KeyContentReJson.vue",
    "chars": 2611,
    "preview": "<template>\n<el-form class='key-content-string'>\n  <!-- key content textarea -->\n  <el-form-item>\n    <FormatViewer\n     "
  },
  {
    "path": "src/components/contents/KeyContentSet.vue",
    "chars": 8729,
    "preview": "<template>\n  <div>\n    <!-- table toolbar -->\n    <div>\n      <!-- add button -->\n      <el-button type=\"primary\" @click"
  },
  {
    "path": "src/components/contents/KeyContentStream.vue",
    "chars": 13470,
    "preview": "<template>\n  <div class=\"key-content-stream\">\n    <!-- table toolbar -->\n    <div>\n      <el-form :inline=\"true\">\n      "
  },
  {
    "path": "src/components/contents/KeyContentString.vue",
    "chars": 2578,
    "preview": "<template>\n<el-form class='key-content-string'>\n  <!-- key content textarea -->\n  <el-form-item>\n    <FormatViewer\n     "
  },
  {
    "path": "src/components/contents/KeyContentZset.vue",
    "chars": 10425,
    "preview": "<template>\n  <div>\n    <!-- table toolbar -->\n    <div>\n      <!-- add button -->\n      <el-button type=\"primary\" @click"
  },
  {
    "path": "src/components/viewers/ViewerBinary.vue",
    "chars": 656,
    "preview": "<template>\n  <div>\n    <!-- </textarea> -->\n    <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='"
  },
  {
    "path": "src/components/viewers/ViewerBrotli.vue",
    "chars": 972,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>\n</template>\n\n<script type=\"t"
  },
  {
    "path": "src/components/viewers/ViewerCustom.vue",
    "chars": 4834,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' class='viewer-custom-editor'>\n    <p :title=\"fullCommand\" cl"
  },
  {
    "path": "src/components/viewers/ViewerDeflate.vue",
    "chars": 967,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>\n</template>\n\n<script type=\"t"
  },
  {
    "path": "src/components/viewers/ViewerDeflateRaw.vue",
    "chars": 976,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>\n</template>\n\n<script type=\"t"
  },
  {
    "path": "src/components/viewers/ViewerGzip.vue",
    "chars": 958,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>\n</template>\n\n<script type=\"t"
  },
  {
    "path": "src/components/viewers/ViewerHex.vue",
    "chars": 645,
    "preview": "<template>\n  <div>\n    <!-- </textarea> -->\n    <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='"
  },
  {
    "path": "src/components/viewers/ViewerJavaSerialize.vue",
    "chars": 975,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='true'></JsonEditor>\n</template>\n\n<script type=\"te"
  },
  {
    "path": "src/components/viewers/ViewerJson.vue",
    "chars": 1203,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='disabled||false'></JsonEditor>\n</template>\n\n<scri"
  },
  {
    "path": "src/components/viewers/ViewerMsgpack.vue",
    "chars": 1323,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false'></JsonEditor>\n</template>\n\n<script type=\"t"
  },
  {
    "path": "src/components/viewers/ViewerOverSize.vue",
    "chars": 1067,
    "preview": "<template>\n  <div class=\"size-too-large-viewer\">\n    <el-alert\n      :closable='false'\n      :title='alertTitle'\n      t"
  },
  {
    "path": "src/components/viewers/ViewerPHPSerialize.vue",
    "chars": 1288,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='isPHPClass'></JsonEditor>\n</template>\n\n<script ty"
  },
  {
    "path": "src/components/viewers/ViewerPickle.vue",
    "chars": 677,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='true'></JsonEditor>\n</template>\n\n<script type=\"te"
  },
  {
    "path": "src/components/viewers/ViewerProtobuf.vue",
    "chars": 4171,
    "preview": "<template>\n  <JsonEditor ref='editor' :content='newContent' :readOnly='false' class='protobuf-viewer'>\n    <div class=\"v"
  },
  {
    "path": "src/components/viewers/ViewerText.vue",
    "chars": 1355,
    "preview": "<template>\n  <div>\n    <!-- </textarea> -->\n    <el-input ref='textInput' :disabled='disabled' type='textarea' v-model='"
  },
  {
    "path": "src/i18n/i18n.js",
    "chars": 1935,
    "preview": "import Vue from 'vue';\nimport VueI18n from 'vue-i18n';\nimport locale from 'element-ui/lib/locale';\n\nimport enLocale from"
  },
  {
    "path": "src/i18n/langs/cn.js",
    "chars": 5933,
    "preview": "const cn = {\n  message: {\n    new_connection: '新建连接',\n    refresh_connection: '刷新',\n    edit_connection: '编辑连接',\n    dup"
  },
  {
    "path": "src/i18n/langs/de.js",
    "chars": 10241,
    "preview": "const de = {\n  message: {\n    new_connection: 'Neue Verbindung',\n    refresh_connection: 'Aktualisieren',\n    edit_conne"
  },
  {
    "path": "src/i18n/langs/en.js",
    "chars": 8902,
    "preview": "const en = {\n  message: {\n    new_connection: 'New Connection',\n    refresh_connection: 'Refresh',\n    edit_connection: "
  },
  {
    "path": "src/i18n/langs/es.js",
    "chars": 9789,
    "preview": "const es = {\n  message: {\n    new_connection: 'Nueva Conexión',\n    refresh_connection: 'Refrescar',\n    edit_connection"
  },
  {
    "path": "src/i18n/langs/fr.js",
    "chars": 10247,
    "preview": "const fr = {\n  message: {\n    new_connection: 'Nouvelle connexion',\n    refresh_connection: 'Actualiser',\n    edit_conne"
  },
  {
    "path": "src/i18n/langs/it.js",
    "chars": 10229,
    "preview": "const it = {\n  message: {\n    new_connection: 'Nuova Connessione',\n    refresh_connection: 'Ricaricare',\n    edit_connec"
  },
  {
    "path": "src/i18n/langs/ko.js",
    "chars": 6751,
    "preview": "const ko = {\n  message: {\n    new_connection: '새 연결',\n    refresh_connection: '새로고침',\n    edit_connection: '연결 편집',\n    "
  },
  {
    "path": "src/i18n/langs/pt.js",
    "chars": 9816,
    "preview": "const pt = {\n  message: {\n    new_connection: 'Nova Conexão',\n    refresh_connection: 'Atualizar',\n    edit_connection: "
  },
  {
    "path": "src/i18n/langs/ru.js",
    "chars": 10061,
    "preview": "const ru = {\n  message: {\n    new_connection: 'Новое подключение',\n    refresh_connection: 'Обновить',\n    edit_connecti"
  },
  {
    "path": "src/i18n/langs/tr.js",
    "chars": 9499,
    "preview": "const tr = {\n  message: {\n    new_connection: 'Yeni Bağlantı',\n    refresh_connection: 'Yenile',\n    edit_connection: 'B"
  },
  {
    "path": "src/i18n/langs/tw.js",
    "chars": 5963,
    "preview": "const tw = {\n  message: {\n    new_connection: '新增連線',\n    refresh_connection: '重新整理',\n    edit_connection: '編輯連線',\n    d"
  },
  {
    "path": "src/i18n/langs/ua.js",
    "chars": 9817,
    "preview": "const ua = {\n  message: {\n    new_connection: 'Нове з`єднання',\n    refresh_connection: 'Оновити',\n    edit_connection: "
  },
  {
    "path": "src/i18n/langs/vi.js",
    "chars": 9115,
    "preview": "const vi = {\n  message: {\n    new_connection: 'Kết nối mới',\n    refresh_connection: 'Làm mới',\n    edit_connection: 'Sử"
  },
  {
    "path": "src/main.js",
    "chars": 979,
    "preview": "import Vue from 'vue';\nimport ElementUI from 'element-ui';\nimport 'font-awesome/css/font-awesome.css';\nimport App from '"
  },
  {
    "path": "src/redisClient.js",
    "chars": 11541,
    "preview": "import Redis from 'ioredis';\nimport { createTunnel } from 'tunnel-ssh';\nimport vue from '@/main.js';\nimport { remote } f"
  },
  {
    "path": "src/router/index.js",
    "chars": 235,
    "preview": "import Vue from 'vue';\nimport Router from 'vue-router';\nimport Tabs from '@/components/Tabs';\n\nVue.use(Router);\n\nexport "
  },
  {
    "path": "src/shortcut.js",
    "chars": 675,
    "preview": "import keymaster from 'keymaster';\nimport { ipcRenderer } from 'electron';\n\n// enable shortcut in input, textarea, selec"
  },
  {
    "path": "src/storage.js",
    "chars": 5478,
    "preview": "import utils from './util';\n\nconst { randomString } = utils;\n\nexport default {\n  getSetting(key) {\n    let settings = lo"
  },
  {
    "path": "src/util.js",
    "chars": 9916,
    "preview": "export default {\n  data: {},\n  get(name) {\n    return this.data[name];\n  },\n  set(name, value) {\n    this.data[name] = v"
  },
  {
    "path": "static/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "static/theme/dark/index.css",
    "chars": 232946,
    "preview": "@charset \"UTF-8\";.el-pagination--small .arrow.disabled,.el-table .hidden-columns,.el-table td.is-hidden>*,.el-table th.i"
  },
  {
    "path": "static/theme/light/index.css",
    "chars": 198511,
    "preview": "@charset \"UTF-8\";.el-input__suffix,.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-pagination--sma"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the qishibo/AnotherRedisDesktopManager GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 121 files (983.6 KB), approximately 288.7k tokens, and a symbol index with 92 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!