├── LICENSE ├── README.md ├── cmd ├── mysql.go ├── redis.go └── root.go ├── go.mod ├── go.sum ├── images ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── sqltui.png ├── main.go └── pkg ├── config └── config.go └── tuiapp ├── mysql ├── dao.go ├── dashboard.go ├── input.go ├── login.go ├── mysql.go ├── query.go ├── table.go ├── textview.go └── tree.go ├── redis ├── dao.go ├── dashboard.go ├── errtextview.go ├── input.go ├── keytree.go ├── login.go ├── query.go ├── redis.go └── resutltextview.go └── tuiapp.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 LinPr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./images/sqltui.png) 2 | 3 | # SQLTUI - A terminal UI to operate sql and nosql databases 4 | 5 | sqltui provides a terminal UI to interact with your sql or nosql databases. The aim of this project is to make it easier to navigate, observe and manage your databases in the wild. 6 | 7 | # Screenshots 8 | 1. mysql login 9 | ![](./images/1.png) 10 | 11 | 2. mysql tables tree 12 | ![](./images/2.png) 13 | 14 | 3. mysql show records 15 | ![](./images/3.png) 16 | 17 | 4. mysql auto complete query 18 | ![](./images/4.png) 19 | 20 | 5. msql show error message 21 | ![](./images/5.png) 22 | 23 | 6. redis keys 24 | ![](./images/6.png) 25 | 26 | 7. redis result 27 | ![](./images/7.png) 28 | 29 | 8. redis auto complete and command tip 30 | ![](./images/8.png) 31 | 32 | # install 33 | ### 1. install with go 34 | 35 | ```shell 36 | go install github.com/LinPr/sqltui@latest 37 | ``` 38 | 39 | 40 | 41 | # quick start 42 | 43 | 44 | ### help 45 | 46 | ``` shell 47 | $ sqltui -h 48 | 49 | sqltui is a tui tool to operate sql and nosql databases 50 | 51 | Usage: 52 | sqltui [command] 53 | 54 | Available Commands: 55 | completion Generate the autocompletion script for the specified shell 56 | help Help about any command 57 | mysql start a mysql tui 58 | redis start a redis tui 59 | 60 | Flags: 61 | -h, --help help for sqltui 62 | 63 | Use "sqltui [command] --help" for more information about a command. 64 | ``` 65 | 66 | ### connect to mysql 67 | 68 | ```shel 69 | $ sqltui mysql 70 | ``` 71 | 72 | ### connect to redis 73 | 74 | ```shell 75 | $ sqltui redis 76 | ``` 77 | 78 | # Keybindings 79 | 80 | ### Login 81 | 82 | | kEY | fUNCTION | 83 | | :------- | ------------------------------------------------------- | 84 | | Enter | Confirm Login Information | 85 | | Ctrl+s | Save Login Information to file (defeault to ~/.sqltui) | 86 | | Ctrl+c | Quit | 87 | | Tab | Switch to next object | 88 | | Ctrl+Tab | Switch to previous object | 89 | 90 | 91 | 92 | ### Mysql Dashboard 93 | 94 | | kEY | fUNCTION | 95 | | ----- | ------------------- | 96 | | Enter | Select Tree Node | 97 | | Tab | Switch Widget Focus | 98 | 99 | 100 | 101 | # TODO list 102 | 1. supprt sqlight(current working on) 103 | 2. support others... 104 | 105 | # references 106 | 107 | this project used two main opensorce projects 108 | - [cobra - for building command line interface](https://github.com/spf13/cobra) 109 | - [tview - for building terminal ui interface](https://github.com/rivo/tview) -------------------------------------------------------------------------------- /cmd/mysql.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "github.com/LinPr/sqltui/pkg/tuiapp" 8 | tuimysql "github.com/LinPr/sqltui/pkg/tuiapp/mysql" 9 | "github.com/rivo/tview" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // mysqlCmd represents the mysql command 14 | var mysqlCmd = &cobra.Command{ 15 | Use: "mysql", 16 | Short: "start a mysql tui", 17 | Long: "start a mysql tui", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | tuimysql.Init() 20 | 21 | layout := tview.NewFlex(). 22 | AddItem(tuiapp.MysqlTui.Pages, 0, 1, true) 23 | 24 | if err := tuiapp.MysqlTui.App.SetRoot(layout, true). 25 | EnableMouse(true). 26 | Run(); err != nil { 27 | panic(err) 28 | } 29 | }, 30 | } 31 | 32 | func init() { 33 | rootCmd.AddCommand(mysqlCmd) 34 | 35 | // mysqlCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 36 | } 37 | -------------------------------------------------------------------------------- /cmd/redis.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | tuiredis "github.com/LinPr/sqltui/pkg/tuiapp/redis" 6 | "github.com/rivo/tview" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // redisCmd represents the redis command 11 | var redisCmd = &cobra.Command{ 12 | Use: "redis", 13 | Short: "start a redis tui", 14 | Long: "start a redis tui", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | tuiredis.Init() 17 | 18 | layout := tview.NewFlex(). 19 | AddItem(tuiapp.RedisTui.Pages, 0, 1, true) 20 | 21 | if err := tuiapp.RedisTui.App.SetRoot(layout, true). 22 | EnableMouse(true). 23 | Run(); err != nil { 24 | panic(err) 25 | } 26 | }, 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(redisCmd) 31 | 32 | // Here you will define your flags and configuration settings. 33 | 34 | // Cobra supports Persistent Flags which will work for this command 35 | // and all subcommands, e.g.: 36 | // redisCmd.PersistentFlags().String("foo", "", "A help for foo") 37 | 38 | // Cobra supports local flags which will only run when this command 39 | // is called directly, e.g.: 40 | // redisCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 41 | } 42 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // rootCmd represents the base command when called without any subcommands 10 | var rootCmd = &cobra.Command{ 11 | Use: "sqltui", 12 | Short: "sqltui is a tui tool to operate sql and nosql databases", 13 | Long: "sqltui is a tui tool to operate sql and nosql databases", 14 | 15 | // Run: func(cmd *cobra.Command, args []string) { }, 16 | } 17 | 18 | func Execute() { 19 | err := rootCmd.Execute() 20 | if err != nil { 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func init() { 26 | // Here you will define your flags and configuration settings. 27 | // Cobra supports persistent flags, which, if defined here, 28 | // will be global for your application. 29 | 30 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sqltui.yaml)") 31 | 32 | // Cobra also supports local flags, which will only run 33 | // when this action is called directly. 34 | // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LinPr/sqltui 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.7.1 7 | github.com/go-sql-driver/mysql v1.8.1 8 | github.com/redis/go-redis/v9 v9.5.1 9 | github.com/rivo/tview v0.0.0-20240429185930-6e1e54f465d4 10 | github.com/spf13/cobra v1.8.0 11 | ) 12 | 13 | require ( 14 | filippo.io/edwards25519 v1.1.0 // indirect 15 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 16 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 17 | github.com/gdamore/encoding v1.0.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 20 | github.com/mattn/go-runewidth v0.0.15 // indirect 21 | github.com/rivo/uniseg v0.4.7 // indirect 22 | github.com/spf13/pflag v1.0.5 // indirect 23 | golang.org/x/sys v0.17.0 // indirect 24 | golang.org/x/term v0.17.0 // indirect 25 | golang.org/x/text v0.14.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 4 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 5 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 6 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 7 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 12 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 13 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 14 | github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= 15 | github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 16 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 17 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 18 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 19 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 20 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 21 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 22 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 23 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 24 | github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= 25 | github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= 26 | github.com/rivo/tview v0.0.0-20240429185930-6e1e54f465d4 h1:4krDj6r/81mUoolXfmdeNJJm/6DA0pBx/L5OqpE+24E= 27 | github.com/rivo/tview v0.0.0-20240429185930-6e1e54f465d4/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= 28 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 29 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 30 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 31 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 32 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 33 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 34 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 35 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 36 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 37 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 38 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 39 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 40 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 41 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 44 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 45 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 56 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 57 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 58 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 59 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 60 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 61 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 64 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 65 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 66 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 67 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 70 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 71 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/4.png -------------------------------------------------------------------------------- /images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/5.png -------------------------------------------------------------------------------- /images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/6.png -------------------------------------------------------------------------------- /images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/7.png -------------------------------------------------------------------------------- /images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/8.png -------------------------------------------------------------------------------- /images/sqltui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinPr/sqltui/ee62de945f9e21c3853591999d1990e46ff9f02c/images/sqltui.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 NAME HERE 3 | */ 4 | package main 5 | 6 | import ( 7 | "github.com/LinPr/sqltui/cmd" 8 | _ "github.com/LinPr/sqltui/pkg/config" 9 | ) 10 | 11 | func main() { 12 | 13 | cmd.Execute() 14 | } 15 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var dataPath = os.Getenv("HOME") + "/.config/sqltui" 10 | var LogFile = dataPath + "/sqltui.log" 11 | var ConfigFile = dataPath + "/config.yaml" 12 | 13 | type MysqlConfig struct { 14 | UserName string `json:"userName" yaml:"userName"` 15 | Password string `json:"password" yaml:"password"` 16 | Host string `json:"host" yaml:"host"` 17 | Port string `json:"port" yaml:"port"` 18 | DbName string `json:"dbName" yaml:"dbName"` 19 | } 20 | 21 | type RedisConfig struct { 22 | UserName string `json:"userName" yaml:"userName"` 23 | Password string `json:"password" yaml:"password"` 24 | Host string `json:"host" yaml:"host"` 25 | Port string `json:"port" yaml:"port"` 26 | RdbNum string `json:"rdbNum" yaml:"rdbNum"` 27 | } 28 | 29 | type SqlConfig struct { 30 | Mysql *MysqlConfig `json:"mysql" yaml:"mysql"` 31 | Redis *RedisConfig `json:"redis" yaml:"redis"` 32 | } 33 | 34 | func init() { 35 | if err := os.MkdirAll(dataPath, 0755); err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | if err := SetDefaultLog(); err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | if err := SetDefaultConfig(); err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | 48 | func SetDefaultConfig() error { 49 | if _, err := os.Stat(ConfigFile); err != nil { 50 | if os.IsNotExist(err) { 51 | log.Println("ConfigFile not exist, create default config file") 52 | 53 | sqlConfig := SqlConfig{ 54 | Mysql: &MysqlConfig{ 55 | UserName: "root", 56 | Password: "123456", 57 | Host: "127.0.0.1", 58 | Port: "3306", 59 | DbName: "test_db", 60 | }, 61 | Redis: &RedisConfig{ 62 | UserName: "", 63 | Password: "", 64 | Host: "127.0.0.1", 65 | Port: "6379", 66 | RdbNum: "0", 67 | }, 68 | } 69 | 70 | j, err := json.MarshalIndent(sqlConfig, "", " ") 71 | if err != nil { 72 | return err 73 | } 74 | 75 | if err := os.WriteFile(ConfigFile, j, 0666); err != nil { 76 | return err 77 | } 78 | } 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func ReadMySqlConfig() (*MysqlConfig, error) { 86 | conf, err := os.ReadFile(ConfigFile) 87 | if err != nil { 88 | return nil, err 89 | } 90 | var tmpConf SqlConfig 91 | if err := json.Unmarshal(conf, &tmpConf); err != nil { 92 | return nil, err 93 | } 94 | return tmpConf.Mysql, nil 95 | } 96 | 97 | func WriteMysqlConfig(mysqlConfig *MysqlConfig) error { 98 | conf, err := os.ReadFile(ConfigFile) 99 | if err != nil { 100 | return err 101 | } 102 | var tmpConf SqlConfig 103 | if err := json.Unmarshal(conf, &tmpConf); err != nil { 104 | return err 105 | } 106 | tmpConf.Mysql = mysqlConfig 107 | j, err := json.MarshalIndent(tmpConf, "", " ") 108 | if err != nil { 109 | return err 110 | } 111 | if err := os.WriteFile(ConfigFile, j, 0666); err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func ReadRedisConfig() (*RedisConfig, error) { 118 | conf, err := os.ReadFile(ConfigFile) 119 | if err != nil { 120 | return nil, err 121 | } 122 | var tmpConf SqlConfig 123 | if err := json.Unmarshal(conf, &tmpConf); err != nil { 124 | return nil, err 125 | } 126 | return tmpConf.Redis, nil 127 | } 128 | 129 | func WriteRedisConfig(redisConfig *RedisConfig) error { 130 | conf, err := os.ReadFile(ConfigFile) 131 | if err != nil { 132 | return err 133 | } 134 | var tmpConf SqlConfig 135 | if err := json.Unmarshal(conf, &tmpConf); err != nil { 136 | return err 137 | } 138 | tmpConf.Redis = redisConfig 139 | j, err := json.MarshalIndent(tmpConf, "", " ") 140 | if err != nil { 141 | return err 142 | } 143 | if err := os.WriteFile(ConfigFile, j, 0666); err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func SetDefaultLog() error { 150 | 151 | file, err := os.OpenFile(LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | log.SetOutput(file) 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/dao.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/go-sql-driver/mysql" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | // DQL 13 | SELECT = "select" 14 | SHOW = "show" 15 | 16 | // DDL & DML & DCL & TCL .... 17 | ) 18 | 19 | var ( 20 | DbClinet *DB 21 | ) 22 | 23 | type DB struct { 24 | *sql.DB 25 | dsn string 26 | } 27 | 28 | func NewDB(dsn string) (*DB, error) { 29 | if DbClinet != nil { 30 | return DbClinet, nil 31 | } 32 | dbc, err := sql.Open("mysql", dsn) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | if err := dbc.Ping(); err != nil { 38 | return nil, err 39 | } 40 | 41 | DbClinet = &DB{ 42 | DB: dbc, 43 | dsn: dsn, 44 | } 45 | return DbClinet, nil 46 | } 47 | 48 | func GetDB() *DB { 49 | return DbClinet 50 | } 51 | 52 | func GetDbName() string { 53 | dbName := strings.Split( 54 | strings.Split(DbClinet.dsn, "/")[1], 55 | "?")[0] 56 | return dbName 57 | } 58 | 59 | type RawCommandResult struct { 60 | Fields []string 61 | Records [][]string 62 | Result sql.Result 63 | IsDQL bool 64 | } 65 | 66 | func (db *DB) RawSqlCommand(query string) (rawCmdResult RawCommandResult, err error) { 67 | cmd := strings.Split(strings.Trim(query, " "), " ") 68 | if len(cmd) == 0 { 69 | return rawCmdResult, fmt.Errorf("empty query") 70 | } 71 | 72 | switch strings.ToLower(cmd[0]) { 73 | case SELECT, SHOW: 74 | rawCmdResult.IsDQL = true 75 | rawCmdResult.Fields, rawCmdResult.Records, err = db.RawQuery(query) 76 | return rawCmdResult, err 77 | default: 78 | rawCmdResult.IsDQL = false 79 | rawCmdResult.Result, err = db.RawExec(query) 80 | return rawCmdResult, err 81 | } 82 | } 83 | 84 | func (db *DB) RawQuery(query string) (fields []string, records [][]string, err error) { 85 | rows, err := db.Query(query) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | 90 | records, err = readRecords(rows) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | query = strings.ToLower(strings.TrimSuffix(query, ";")) 96 | words := strings.Split(query, " ") 97 | var tableName string 98 | for i, word := range words { 99 | switch word { 100 | case "from": 101 | tableName = words[i+1] 102 | break 103 | case "show": 104 | // TODO: 处理一些特殊情况 105 | } 106 | } 107 | 108 | if tableName != "" { 109 | fields, err = db.FetchTableFields(tableName) 110 | if err != nil { 111 | return nil, nil, err 112 | } 113 | } 114 | 115 | return fields, records, nil 116 | } 117 | 118 | func (db *DB) RawExec(query string) (sql.Result, error) { 119 | res, err := db.Exec(query) 120 | if err != nil { 121 | return nil, err 122 | } 123 | return res, nil 124 | } 125 | 126 | func (db *DB) ShowDatabases() ([]string, error) { 127 | 128 | query := "show databases" 129 | rows, err := db.Query(query) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | var databases []string 135 | for rows.Next() { 136 | var database string 137 | if err := rows.Scan(&database); err != nil { 138 | return nil, err 139 | } 140 | databases = append(databases, database) 141 | } 142 | 143 | return databases, nil 144 | } 145 | 146 | func (db *DB) ShowDatabaseTables(database string) ([]string, error) { 147 | query := "show tables" 148 | if database != "" { 149 | query = fmt.Sprintf("show tables from %s", database) 150 | } 151 | 152 | rows, err := db.Query(query) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | var tables []string 158 | for rows.Next() { 159 | var table string 160 | if err := rows.Scan(&table); err != nil { 161 | return nil, err 162 | } 163 | tables = append(tables, table) 164 | } 165 | return tables, nil 166 | } 167 | 168 | func (db *DB) ShowCurrentDatabaseTables() ([]string, error) { 169 | return db.ShowDatabaseTables("") 170 | } 171 | 172 | func (db *DB) FetchTableFields(table string) ([]string, error) { 173 | query := fmt.Sprintf("describe %s", table) 174 | rows, err := db.Query(query) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | records, err := readRecords(rows) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | fields := []string{} 185 | for _, record := range records { 186 | fields = append(fields, record[0]) 187 | } 188 | return fields, nil 189 | } 190 | 191 | func (db *DB) FetchTableRecords(table string) ([][]string, error) { 192 | query := fmt.Sprintf("select * from %s", table) 193 | rows, err := db.Query(query) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | records, err := readRecords(rows) 199 | if err != nil { 200 | return nil, err 201 | } 202 | return records, nil 203 | } 204 | 205 | func (db *DB) Close() { 206 | db.Close() 207 | } 208 | 209 | func readRecords(rows *sql.Rows) ([][]string, error) { 210 | columns, err := rows.Columns() 211 | if err != nil { 212 | return nil, err 213 | } 214 | var records [][]string 215 | for rows.Next() { 216 | record := make([]any, len(columns), len(columns)) 217 | for i := range columns { 218 | record[i] = &sql.RawBytes{} 219 | } 220 | 221 | if err = rows.Scan(record...); err != nil { 222 | return nil, err 223 | } 224 | var currentRow []string 225 | for _, rawValue := range record { 226 | field := string(*rawValue.(*sql.RawBytes)) 227 | currentRow = append(currentRow, field) 228 | } 229 | log.Printf("------ currentRow: %+v", currentRow) 230 | records = append(records, currentRow) 231 | } 232 | log.Printf("------ records: %+v", records) 233 | return records, nil 234 | } 235 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/dashboard.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | func RenderDashBoardPage() *tview.Flex { 10 | treeView := RenderTreeView() 11 | queryWidget := RenderQueryWidget() 12 | table := RenderTable() 13 | tuiapp.MysqlTui.AddWidget(treeView) 14 | tuiapp.MysqlTui.AddWidget(table) 15 | // tview.NewTextArea() 16 | // tview.NewTextView() 17 | 18 | flex := tview.NewFlex(). 19 | AddItem(treeView, 20, 1, true). 20 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 21 | // AddItem(inputField, 0, 1, false). 22 | AddItem(queryWidget, 0, 1, false). 23 | // AddItem(tview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false). 24 | AddItem(table, 0, 3, false), 0, 2, false) 25 | 26 | flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 27 | switch event.Key() { 28 | case tcell.KeyTab: 29 | // wiget := tuiapp.TuiApp.MysqlApp.GetFocus() 30 | // tuiapp.TuiApp.MysqlApp.SetFocus(tuiapp.NextWigets(wiget)) 31 | tuiapp.MysqlTui.SetNextFocus() 32 | } 33 | return event // this event should be returned and not to return nil 34 | }) 35 | 36 | // flex.SetBackgroundColor(tcell.ColorBlack) 37 | tuiapp.MysqlTui.AddPage("mysql_dashboard", flex) 38 | 39 | return flex 40 | } 41 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/input.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/gdamore/tcell/v2" 9 | "github.com/rivo/tview" 10 | ) 11 | 12 | // latest 100 histories 13 | var histories = readCommandHistroies()[:100] 14 | 15 | func RenderInputFiedl() *tview.InputField { 16 | 17 | inputField := tview.NewInputField(). 18 | SetLabel("Query: "). 19 | SetPlaceholder("Enter mysql query here..."). 20 | SetPlaceholderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorGray)). 21 | SetFieldBackgroundColor(tcell.ColorBlack). 22 | SetAutocompleteStyles(tcell.ColorBlack, tcell.StyleDefault, tcell.StyleDefault.Foreground(tcell.ColorGreen).Background(tcell.ColorGray)). 23 | SetFieldWidth(1024) 24 | 25 | inputField.SetDoneFunc(func(key tcell.Key) { 26 | switch key { 27 | case tcell.KeyEnter: 28 | // execute sql and show results in table 29 | query := inputField.GetText() 30 | rawCmdResult, err := DbClinet.RawSqlCommand(query) 31 | if err != nil { 32 | PrintfTextView("[red]Error: %s", err) 33 | ClearTableRecords() 34 | return 35 | } 36 | if rawCmdResult.IsDQL { 37 | FillTableWithQueryResult(rawCmdResult.Fields, rawCmdResult.Records) 38 | PrintfTextView("[yellow]Status: Success !") 39 | addCommandHistory(query) 40 | } else { 41 | rowAffected, err := rawCmdResult.Result.RowsAffected() 42 | if err != nil { 43 | PrintfTextView("[red]Error: %s", err) 44 | ClearTableRecords() 45 | return 46 | } 47 | lastInsertId, err := rawCmdResult.Result.LastInsertId() 48 | if err != nil { 49 | PrintfTextView("[red]Error: %s", err) 50 | ClearTableRecords() 51 | return 52 | 53 | } 54 | PrintfTextView("[yellow]Status: Success ! \n\t Rows affected: %d, Last Insert ID: %d", rowAffected, lastInsertId) 55 | addCommandHistory(query) 56 | } 57 | 58 | case tcell.KeyEscape: 59 | log.Println("KeyEscape pressed") 60 | // TODO: 61 | } 62 | }) 63 | 64 | inputField.SetAutocompleteFunc(func(currentText string) (entries []string) { 65 | if len(currentText) == 0 { 66 | return 67 | } 68 | for _, word := range histories { 69 | if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) { 70 | entries = append(entries, word) 71 | } 72 | } 73 | if len(entries) <= 1 { 74 | entries = nil 75 | } 76 | return 77 | }) 78 | inputField.SetAutocompletedFunc(func(text string, index, source int) bool { 79 | if source != tview.AutocompletedNavigate { 80 | inputField.SetText(text) 81 | } 82 | return source == tview.AutocompletedEnter || source == tview.AutocompletedClick 83 | }) 84 | // inputField.SetBorder(true) 85 | 86 | return inputField 87 | } 88 | 89 | func readCommandHistroies() []string { 90 | var historys []string 91 | rawHistory, err := os.ReadFile(os.Getenv("HOME") + "/.mysql_history") 92 | if err != nil { 93 | return historys 94 | } 95 | newHistory := strings.ReplaceAll(string(rawHistory), "\n", "") 96 | newHistory = strings.ReplaceAll(string(newHistory), "\\040", " ") 97 | 98 | historys = strings.Split(newHistory, ";") 99 | return distinctStringSlice(historys) 100 | } 101 | 102 | func distinctStringSlice(histories []string) []string { 103 | tmpSet := make(map[string]struct{}) 104 | for _, v := range histories { 105 | tmpSet[v] = struct{}{} 106 | } 107 | histories = make([]string, 0, len(tmpSet)) 108 | for k := range tmpSet { 109 | histories = append(histories, k) 110 | } 111 | return histories 112 | } 113 | 114 | func addCommandHistory(command string) { 115 | histories = append(histories, command) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/login.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/LinPr/sqltui/pkg/config" 8 | "github.com/LinPr/sqltui/pkg/tuiapp" 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/rivo/tview" 11 | ) 12 | 13 | func RenderLoginPage() *tview.Flex { 14 | form := renderLoginForm() 15 | textView := renderLoginErrTextView() 16 | 17 | flex := tview.NewFlex(). 18 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 2, false). 19 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 20 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 1, false). 21 | AddItem(form, 15, 3, true). 22 | AddItem(textView, 0, 1, false), 50, 3, true). 23 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 2, false) 24 | 25 | // tuiapp.MysqlTui.AddPage("mysql_login", form) 26 | 27 | return flex 28 | } 29 | 30 | func renderLoginForm() *tview.Form { 31 | mysqlConf, err := config.ReadMySqlConfig() 32 | if err != nil { 33 | log.Println("ReadMySqlConfig error: ", err) 34 | return nil 35 | } 36 | 37 | form := tview.NewForm(). 38 | AddInputField("username:", mysqlConf.UserName, 20, nil, nil). 39 | AddInputField("password:", mysqlConf.Password, 20, nil, nil). 40 | AddInputField(" host:", mysqlConf.Host, 20, nil, nil). 41 | AddInputField(" port:", mysqlConf.Port, 20, nil, nil). 42 | AddInputField(" dbname:", mysqlConf.DbName, 20, nil, nil). 43 | SetFieldBackgroundColor(tcell.ColorGray) 44 | // AddDropDown(" charset:", []string{"utf8", "ascall", "unicode"}, 0, nil) 45 | 46 | form.AddButton("Connect", ConnectCallback(form)). 47 | AddButton("Save", SaveCallback(form)). 48 | AddButton("Quit", QuitCallback()). 49 | SetButtonsAlign(tview.AlignCenter). 50 | SetButtonBackgroundColor(tcell.ColorGray). 51 | SetButtonTextColor(tcell.ColorLightGoldenrodYellow) 52 | 53 | form.SetBorder(true).SetBorderColor(tcell.ColorWhite) 54 | 55 | form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 56 | switch event.Key() { 57 | case tcell.KeyCtrlS: 58 | SaveCallback(form)() 59 | 60 | case tcell.KeyEnter: 61 | ConnectCallback(form)() 62 | 63 | } 64 | return event 65 | }) 66 | 67 | return form 68 | } 69 | 70 | var LoginErrOut *tview.TextView 71 | 72 | func printfLoginErrOut(format string, a ...any) { 73 | LoginErrOut.Clear() 74 | erMsg := fmt.Sprintf(format, a...) 75 | LoginErrOut.SetText(erMsg) 76 | } 77 | 78 | func renderLoginErrTextView() *tview.TextView { 79 | textView := tview.NewTextView(). 80 | SetWrap(true). 81 | SetDynamicColors(true) 82 | 83 | LoginErrOut = textView 84 | return textView 85 | } 86 | 87 | func ConnectCallback(form *tview.Form) func() { 88 | return func() { 89 | count := form.GetFormItemCount() 90 | for i := 0; i < count; i++ { 91 | log.Println(form.GetFormItem(i).GetLabel()) 92 | log.Println(form.GetFormItem(i).(*tview.InputField).GetText()) 93 | } 94 | username := form.GetFormItem(0).(*tview.InputField).GetText() 95 | password := form.GetFormItem(1).(*tview.InputField).GetText() 96 | host := form.GetFormItem(2).(*tview.InputField).GetText() 97 | port := form.GetFormItem(3).(*tview.InputField).GetText() 98 | dbname := form.GetFormItem(4).(*tview.InputField).GetText() 99 | 100 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8", username, password, host, port, dbname) 101 | log.Println(dsn) 102 | 103 | dbc, err := NewDB(dsn) // mmust init database client here 104 | if err != nil { 105 | printfLoginErrOut("[red]" + err.Error()) 106 | return 107 | } 108 | 109 | _ = dbc 110 | 111 | // save current config 112 | SaveCallback(form)() 113 | 114 | SetRootTreeNodeName(GetDbName()) 115 | tuiapp.MysqlTui.ShowPage("mysql_dashboard") 116 | } 117 | } 118 | 119 | func SaveCallback(form *tview.Form) func() { 120 | return func() { 121 | mysqlConfig := &config.MysqlConfig{ 122 | UserName: form.GetFormItem(0).(*tview.InputField).GetText(), 123 | Password: form.GetFormItem(1).(*tview.InputField).GetText(), 124 | Host: form.GetFormItem(2).(*tview.InputField).GetText(), 125 | Port: form.GetFormItem(3).(*tview.InputField).GetText(), 126 | DbName: form.GetFormItem(4).(*tview.InputField).GetText(), 127 | } 128 | if err := config.WriteMysqlConfig(mysqlConfig); err != nil { 129 | log.Println("WriteMysqlConfig error: ", err) 130 | } 131 | } 132 | } 133 | 134 | func QuitCallback() func() { 135 | return func() { 136 | // app.Stop() 137 | tuiapp.MysqlTui.App.Stop() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | // "github.com/rivo/tview" 6 | ) 7 | 8 | func Init() { 9 | tuiapp.MysqlTui.AddPage("mysql_login", RenderLoginPage()) 10 | tuiapp.MysqlTui.AddPage("mysql_dashboard", RenderDashBoardPage()) 11 | 12 | // first enter into login page 13 | tuiapp.MysqlTui.ShowPage("mysql_login") 14 | // tuiapp.ShowPage("mysql_dashboard") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/query.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | func RenderQueryWidget() *tview.Flex { 9 | inputField := RenderInputFiedl() 10 | textView := RenderTextView() 11 | 12 | tuiapp.MysqlTui.AddWidget(inputField) 13 | 14 | queryWidget := tview.NewFlex(). 15 | SetDirection(tview.FlexRow). 16 | AddItem(inputField, 2, 1, false). 17 | AddItem(textView, 0, 1, false) 18 | queryWidget.SetBorder(true).SetTitle("[green]Query") 19 | 20 | return queryWidget 21 | } 22 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/table.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | // "strings" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | var TableRecords *tview.Table 11 | 12 | func RenderTable() *tview.Table { 13 | // SetBorderStyle() 14 | 15 | table := tview.NewTable(). 16 | SetBorders(true). 17 | SetSeparator('|'). 18 | SetFixed(1, 0). 19 | Select(0, 0) 20 | 21 | table.SetDoneFunc( 22 | func(key tcell.Key) { 23 | switch key { 24 | case tcell.KeyEnter: 25 | table.SetSelectable(true, true) 26 | } 27 | }) 28 | 29 | table.SetSelectedFunc(func(row int, column int) { 30 | table.GetCell(row, column).SetTextColor(tcell.ColorRed) 31 | table.SetSelectable(false, false) 32 | }) 33 | 34 | table.SetBorder(true).SetTitle("[green]Result Table") 35 | 36 | TableRecords = table 37 | 38 | // lorem := strings.Split("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", " ") 39 | // cols, rows := 10, 40 40 | // word := 0 41 | // for r := 0; r < rows; r++ { 42 | // for c := 0; c < cols; c++ { 43 | // color := tcell.ColorWhite 44 | // if r == 0 { 45 | // color = tcell.ColorYellow 46 | // } 47 | // SetCell(r, c, lorem[word], color) 48 | // word = (word + 1) % len(lorem) 49 | // } 50 | // } 51 | 52 | return TableRecords 53 | } 54 | 55 | func ClearTableRecords() { 56 | TableRecords.Clear() 57 | } 58 | 59 | func FillTableWithQueryResult(fields []string, result [][]string) { 60 | TableRecords.Clear() 61 | // 1. fill the first line with field names 62 | startRow := 0 63 | if fields != nil { 64 | for j, field := range fields { 65 | SetCell(startRow, j, field, tcell.ColorYellow) 66 | } 67 | startRow += 1 68 | } 69 | 70 | // 2. fill result 71 | if result != nil { 72 | for i, row := range result { 73 | for j, cell := range row { 74 | SetCell(startRow+i, j, cell, tcell.ColorWhite) 75 | } 76 | } 77 | } 78 | } 79 | 80 | func SetCell(row int, column int, text string, color tcell.Color) { 81 | cell := tview.NewTableCell(text). 82 | SetTextColor(color). 83 | SetAlign(tview.AlignCenter) 84 | TableRecords.SetCell(row, column, cell) 85 | } 86 | 87 | // set new border style and return old one 88 | func SetBorderStyle() *struct { 89 | Horizontal rune 90 | Vertical rune 91 | TopLeft rune 92 | TopRight rune 93 | BottomLeft rune 94 | BottomRight rune 95 | 96 | LeftT rune 97 | RightT rune 98 | TopT rune 99 | BottomT rune 100 | Cross rune 101 | 102 | HorizontalFocus rune 103 | VerticalFocus rune 104 | TopLeftFocus rune 105 | TopRightFocus rune 106 | BottomLeftFocus rune 107 | BottomRightFocus rune 108 | } { 109 | 110 | oldBorder := tview.Borders 111 | tview.Borders.Vertical = '|' 112 | tview.Borders.Horizontal = '-' 113 | tview.Borders.TopLeft = '+' 114 | tview.Borders.TopRight = '+' 115 | tview.Borders.Cross = '+' 116 | tview.Borders.TopT = '+' 117 | tview.Borders.BottomT = '+' 118 | tview.Borders.BottomLeft = '+' 119 | tview.Borders.BottomRight = '+' 120 | tview.Borders.LeftT = '+' 121 | tview.Borders.RightT = '+' 122 | 123 | return &oldBorder 124 | } 125 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/textview.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | var textViewOut *tview.TextView 9 | 10 | // This wiil be used when the inputField capture the enter key 11 | func PrintfTextView(format string, a ...any) { 12 | textViewOut.Clear() 13 | fmt.Fprintf(textViewOut, format, a...) 14 | } 15 | 16 | func PrintlnTextView(a ...any) { 17 | textViewOut.Clear() 18 | fmt.Fprintln(textViewOut, a...) 19 | } 20 | 21 | func RenderTextView() *tview.TextView { 22 | textView := tview.NewTextView(). 23 | SetText("[yellow]Status: null"). 24 | SetWrap(true). 25 | SetTextAlign(tview.AlignLeft). 26 | SetDynamicColors(true). 27 | SetChangedFunc(func() {}) 28 | 29 | // textView.SetBorder(true).SetTitle("Query Result") 30 | textViewOut = textView 31 | return textView 32 | 33 | } 34 | -------------------------------------------------------------------------------- /pkg/tuiapp/mysql/tree.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | var RootTreeNode *tview.TreeNode 9 | 10 | func SetRootTreeNodeName(dbName string) { 11 | RootTreeNode.SetText(dbName) 12 | } 13 | 14 | func RenderTreeView() *tview.TreeView { 15 | 16 | RootTreeNode = tview.NewTreeNode("show_tables"). 17 | SetColor(tcell.ColorOlive) 18 | 19 | tree := tview.NewTreeView(). 20 | SetRoot(RootTreeNode). 21 | SetCurrentNode(RootTreeNode) 22 | 23 | tree.SetBorder(true).SetTitle("[green]Mysql Databases") 24 | 25 | tree.SetSelectedFunc(func(node *tview.TreeNode) { 26 | reference := node.GetReference() 27 | if reference == nil { 28 | // Selecting the root node d 29 | loadTables(GetDB(), node) 30 | node.SetReference("database") 31 | return 32 | } 33 | 34 | if len(node.GetChildren()) > 0 { 35 | node.SetExpanded(!node.IsExpanded()) 36 | } 37 | 38 | }) 39 | return tree 40 | } 41 | 42 | func loadTables(dbClinet *DB, targetNode *tview.TreeNode) { 43 | tables, err := dbClinet.ShowCurrentDatabaseTables() 44 | if err != nil { 45 | panic(err) 46 | } 47 | for _, table := range tables { 48 | node := tview.NewTreeNode(table). 49 | SetText(table). 50 | SetReference(table). 51 | SetSelectable(true) 52 | 53 | node.SetSelectedFunc(func() { 54 | // execute sql and show results in table 55 | query := "select * from " + node.GetText() 56 | rawCmdResult, err := DbClinet.RawSqlCommand(query) 57 | if err != nil { 58 | PrintfTextView("[red]Error: %s", err) 59 | ClearTableRecords() 60 | return 61 | } 62 | if rawCmdResult.IsDQL { 63 | FillTableWithQueryResult(rawCmdResult.Fields, rawCmdResult.Records) 64 | PrintfTextView("[yellow]Status: Success !") 65 | addCommandHistory(query) 66 | } else { 67 | rowAffected, err := rawCmdResult.Result.RowsAffected() 68 | if err != nil { 69 | PrintfTextView("[red]Error: %s", err) 70 | ClearTableRecords() 71 | return 72 | } 73 | lastInsertId, err := rawCmdResult.Result.LastInsertId() 74 | if err != nil { 75 | PrintfTextView("[red]Error: %s", err) 76 | ClearTableRecords() 77 | return 78 | 79 | } 80 | PrintfTextView("[yellow]Status: Success ! \n\t Rows affected: %d, Last Insert ID: %d", rowAffected, lastInsertId) 81 | addCommandHistory(query) 82 | } 83 | 84 | }) 85 | targetNode.AddChild(node) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/dao.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | var RdsClinet *RDS 12 | 13 | type RDS struct { 14 | rdsc *redis.Client 15 | } 16 | 17 | func NewRDS(rdsOpt *redis.Options) (*RDS, error) { 18 | if RdsClinet != nil { 19 | return RdsClinet, nil 20 | } 21 | 22 | // rdb := redis.NewClient(&redis.Options{ 23 | // Addr: "localhost:6379", 24 | // Username: "", 25 | // Password: "", // no password set 26 | // DB: 0, // use default DB 27 | // }) 28 | 29 | rdsc := redis.NewClient(rdsOpt) 30 | 31 | if _, err := rdsc.Ping(context.Background()).Result(); err != nil { 32 | return nil, err 33 | } 34 | 35 | RdsClinet = &RDS{ 36 | rdsc: rdsc, 37 | } 38 | return RdsClinet, nil 39 | } 40 | 41 | func (rds *RDS) ExecuteRawQuery(args []string) (string, error) { 42 | var tmpArgs []any 43 | for _, arg := range args { 44 | tmpArgs = append(tmpArgs, arg) 45 | } 46 | 47 | cmd := rds.rdsc.Do(context.Background(), tmpArgs...) 48 | result, err := cmd.Result() 49 | log.Println("") 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | return FormatJson(result) 55 | } 56 | 57 | func (rds *RDS) Scan(cursor uint64, match string, count int64, keyType string) ([]string, error) { 58 | iter := rds.rdsc.ScanType(context.Background(), cursor, match, count, keyType).Iterator() 59 | var keys []string 60 | for iter.Next(context.Background()) { 61 | keys = append(keys, iter.Val()) 62 | } 63 | if err := iter.Err(); err != nil { 64 | return nil, err 65 | } 66 | return keys, nil 67 | } 68 | 69 | func (rds *RDS) GetValue(key string) (string, error) { 70 | keyType := rds.rdsc.Type(context.Background(), key).Val() 71 | log.Printf("keyType: %s", keyType) 72 | switch keyType { 73 | case "string": 74 | val, err := rds.rdsc.Get(context.Background(), key).Result() 75 | if err != nil { 76 | return "", err 77 | } 78 | return FormatJson(val) 79 | 80 | case "list": 81 | val, err := rds.rdsc.LRange(context.Background(), key, 0, -1).Result() 82 | if err != nil { 83 | return "", err 84 | } 85 | return FormatJson(val) 86 | 87 | case "hash": 88 | val, err := rds.rdsc.HGetAll(context.Background(), key).Result() 89 | if err != nil { 90 | return "", err 91 | } 92 | return FormatJson(val) 93 | 94 | case "set": 95 | val, err := rds.rdsc.SMembers(context.Background(), key).Result() 96 | if err != nil { 97 | return "", err 98 | } 99 | return FormatJson(val) 100 | 101 | case "zset": 102 | val, err := rds.rdsc.ZRangeWithScores(context.Background(), key, 0, -1).Result() 103 | if err != nil { 104 | return "", err 105 | } 106 | log.Printf("zset: %+v", val) 107 | return FormatJson(val) 108 | 109 | case "bitmap": 110 | val, err := rds.rdsc.GetBit(context.Background(), key, 0).Result() 111 | if err != nil { 112 | return "", err 113 | } 114 | return FormatJson(val) 115 | 116 | default: 117 | return "sqltui has not implimented type" + keyType, nil 118 | } 119 | } 120 | 121 | func FormatJson(a any) (string, error) { 122 | j, err := json.MarshalIndent(a, "", " ") 123 | if err != nil { 124 | return "", err 125 | } 126 | return string(j), nil 127 | } 128 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/dashboard.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | func RenderDashBoardPage() *tview.Flex { 10 | 11 | treeView := RenderKeyTreeView() 12 | queryWidget := RenderQueryWidget() 13 | resultTextView := RenderResultTextView() 14 | tuiapp.RedisTui.AddWidget(treeView) 15 | tuiapp.RedisTui.AddWidget(resultTextView) 16 | // tview.NewTextArea() 17 | // tview.NewTextView() 18 | 19 | flex := tview.NewFlex(). 20 | AddItem(treeView, 20, 1, true). 21 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 22 | // AddItem(inputField, 0, 1, false). 23 | AddItem(queryWidget, 0, 1, false). 24 | // AddItem(tview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false). 25 | AddItem(resultTextView, 0, 3, false), 0, 2, false) 26 | 27 | flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 28 | switch event.Key() { 29 | case tcell.KeyTab: 30 | // wiget := tuiapp.TuiApp.MysqlApp.GetFocus() 31 | // tuiapp.TuiApp.MysqlApp.SetFocus(tuiapp.NextWigets(wiget)) 32 | tuiapp.RedisTui.SetNextFocus() 33 | } 34 | return event // this event should be returned and not to return nil 35 | }) 36 | 37 | // flex.SetBackgroundColor(tcell.ColorDefault) 38 | tuiapp.RedisTui.AddPage("redis_dashboard", flex) 39 | return flex 40 | } 41 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/errtextview.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | var errTextViewOut *tview.TextView 9 | 10 | // This wiil be used when the inputField capture the enter key 11 | func PrintfErrTextView(format string, a ...any) { 12 | errTextViewOut.Clear() 13 | fmt.Fprintf(errTextViewOut, format, a...) 14 | } 15 | 16 | func PrintlnErrTextView(a ...any) { 17 | errTextViewOut.Clear() 18 | fmt.Fprintln(errTextViewOut, a...) 19 | } 20 | 21 | func ClearErrTextView() { 22 | if errTextViewOut != nil { 23 | errTextViewOut.Clear() 24 | } 25 | } 26 | 27 | func RenderErrTextView() *tview.TextView { 28 | textView := tview.NewTextView(). 29 | SetText("[yellow]Status: null"). 30 | SetWrap(true). 31 | SetTextAlign(tview.AlignLeft). 32 | SetDynamicColors(true). 33 | SetChangedFunc(func() {}) 34 | 35 | // textView.SetBorder(true).SetTitle("Query Result") 36 | errTextViewOut = textView 37 | return textView 38 | 39 | } 40 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/input.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | var CommandHistories []string 12 | 13 | func addCommandHistory(cmd string) { 14 | CommandHistories = append(CommandHistories, cmd) 15 | if len(CommandHistories) > 100 { 16 | CommandHistories = CommandHistories[len(CommandHistories)-100:] 17 | } 18 | } 19 | 20 | func RenderInputFiedl() *tview.InputField { 21 | 22 | inputField := tview.NewInputField(). 23 | SetLabel("Query: "). 24 | SetPlaceholder("Enter redis query here..."). 25 | SetPlaceholderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorGray)). 26 | SetFieldBackgroundColor(tcell.ColorBlack). 27 | SetAutocompleteStyles(tcell.ColorBlack, tcell.StyleDefault, tcell.StyleDefault.Foreground(tcell.ColorGreen).Background(tcell.ColorGray)). 28 | SetFieldWidth(1024) 29 | 30 | inputField.SetDoneFunc(func(key tcell.Key) { 31 | switch key { 32 | case tcell.KeyEnter: 33 | // execute sql and show results in table 34 | args := strings.Split(inputField.GetText(), " ") 35 | result, err := RdsClinet.ExecuteRawQuery(args) 36 | if err != nil { 37 | PrintfErrTextView("[red]Error: %s", err) 38 | ClearResultTextView() 39 | return 40 | } 41 | PrintfResultTextView("[yellow]%s", result) 42 | addCommandHistory(inputField.GetText()) 43 | 44 | case tcell.KeyEscape: 45 | log.Println("KeyEscape pressed") 46 | // TODO: 47 | } 48 | }) 49 | 50 | inputField.SetAutocompleteFunc(func(currentText string) (entries []string) { 51 | if len(currentText) == 0 { 52 | ClearErrTextView() 53 | return 54 | } 55 | 56 | var tipCmd []RedisHelp 57 | for _, cmdHelp := range commandHelps { 58 | if strings.HasPrefix(strings.ToLower(cmdHelp.Command), strings.ToLower(strings.Trim(currentText, " "))) { 59 | entries = append(entries, cmdHelp.Command) 60 | tipCmd = append(tipCmd, cmdHelp) 61 | } 62 | } 63 | if len(entries) >= 1 { 64 | PrintfErrTextView("[dark]\n\t%s %s", tipCmd[0].Command, tipCmd[0].Args) 65 | } 66 | 67 | for _, cmdHistory := range CommandHistories { 68 | if strings.HasPrefix(strings.ToLower(cmdHistory), strings.ToLower(currentText)) { 69 | entries = append(entries, cmdHistory) 70 | } 71 | } 72 | 73 | if len(entries) <= 1 { 74 | entries = nil 75 | } 76 | return 77 | }) 78 | inputField.SetAutocompletedFunc(func(text string, index, source int) bool { 79 | if source != tview.AutocompletedNavigate { 80 | inputField.SetText(text) 81 | } 82 | return source == tview.AutocompletedEnter || source == tview.AutocompletedClick 83 | }) 84 | 85 | return inputField 86 | } 87 | 88 | type RedisHelp struct { 89 | Command string 90 | Args string 91 | Version string 92 | Desc string 93 | } 94 | 95 | var commandHelps = []RedisHelp{ 96 | {Command: "APPEND", Args: "key value", Version: "2.0.0", Desc: "Append a value to a key"}, 97 | {Command: "AUTH", Args: "password", Version: "1.0.0", Desc: "Authenticate to the server"}, 98 | {Command: "BGREWRITEAOF", Args: "-", Version: "1.0.0", Desc: "Asynchronously rewrite the append-only file"}, 99 | {Command: "BGSAVE", Args: "-", Version: "1.0.0", Desc: "Asynchronously save the dataset to disk"}, 100 | {Command: "BITCOUNT", Args: "key [start end]", Version: "2.6.0", Desc: "Count set bits in a string"}, 101 | {Command: "BITFIELD", Args: "key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]", Version: "3.2.0", Desc: "Perform arbitrary bitfield integer operations on strings"}, 102 | {Command: "BITOP", Args: "operation destkey key [key ...]", Version: "2.6.0", Desc: "Perform bitwise operations between strings"}, 103 | {Command: "BITPOS", Args: "key bit [start] [end]", Version: "2.8.7", Desc: "Find first bit set or clear in a string"}, 104 | {Command: "BLPOP", Args: "key [key ...] timeout", Version: "2.0.0", Desc: "Remove and get the first element in a list, or block until one is available"}, 105 | {Command: "BRPOP", Args: "key [key ...] timeout", Version: "2.0.0", Desc: "Remove and get the last element in a list, or block until one is available"}, 106 | {Command: "BRPOPLPUSH", Args: "source destination timeout", Version: "2.2.0", Desc: "Pop a value from a list, push it to another list and return it; or block until one is available"}, 107 | {Command: "BZPOPMAX", Args: "key [key ...] timeout", Version: "5.0.0", Desc: "Remove and return the member with the highest score from one or more sorted sets, or block until one is available"}, 108 | {Command: "BZPOPMIN", Args: "key [key ...] timeout", Version: "5.0.0", Desc: "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available"}, 109 | {Command: "CLIENT GETNAME", Args: "-", Version: "2.6.9", Desc: "Get the current connection name"}, 110 | {Command: "CLIENT ID", Args: "-", Version: "5.0.0", Desc: "Returns the client ID for the current connection"}, 111 | {Command: "CLIENT KILL", Args: "[ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [ADDR ip:port] [SKIPME yes/no]", Version: "2.4.0", Desc: "Kill the connection of a client"}, 112 | {Command: "CLIENT LIST", Args: "-", Version: "2.4.0", Desc: "Get the list of client connections"}, 113 | {Command: "CLIENT PAUSE", Args: "timeout", Version: "2.9.50", Desc: "Stop processing commands from clients for some time"}, 114 | {Command: "CLIENT REPLY", Args: "ON|OFF|SKIP", Version: "3.2", Desc: "Instruct the server whether to reply to commands"}, 115 | {Command: "CLIENT SETNAME", Args: "connection-name", Version: "2.6.9", Desc: "Set the current connection name"}, 116 | {Command: "CLIENT UNBLOCK", Args: "client-id [TIMEOUT|ERROR]", Version: "5.0.0", Desc: "Unblock a client blocked in a blocking command from a different connection"}, 117 | {Command: "CLUSTER ADDSLOTS", Args: "slot [slot ...]", Version: "3.0.0", Desc: "Assign new hash slots to receiving node"}, 118 | {Command: "CLUSTER COUNT-FAILURE-REPORTS", Args: "node-id", Version: "3.0.0", Desc: "Return the number of failure reports active for a given node"}, 119 | {Command: "CLUSTER COUNTKEYSINSLOT", Args: "slot", Version: "3.0.0", Desc: "Return the number of local keys in the specified hash slot"}, 120 | {Command: "CLUSTER DELSLOTS", Args: "slot [slot ...]", Version: "3.0.0", Desc: "Set hash slots as unbound in receiving node"}, 121 | {Command: "CLUSTER FAILOVER", Args: "[FORCE|TAKEOVER]", Version: "3.0.0", Desc: "Forces a replica to perform a manual failover of its master."}, 122 | {Command: "CLUSTER FORGET", Args: "node-id", Version: "3.0.0", Desc: "Remove a node from the nodes table"}, 123 | {Command: "CLUSTER GETKEYSINSLOT", Args: "slot count", Version: "3.0.0", Desc: "Return local key names in the specified hash slot"}, 124 | {Command: "CLUSTER INFO", Args: "-", Version: "3.0.0", Desc: "Provides info about Redis Cluster node state"}, 125 | {Command: "CLUSTER KEYSLOT", Args: "key", Version: "3.0.0", Desc: "Returns the hash slot of the specified key"}, 126 | {Command: "CLUSTER MEET", Args: "ip port", Version: "3.0.0", Desc: "Force a node cluster to handshake with another node"}, 127 | {Command: "CLUSTER NODES", Args: "-", Version: "3.0.0", Desc: "Get Cluster config for the node"}, 128 | {Command: "CLUSTER REPLICAS", Args: "node-id", Version: "5.0.0", Desc: "List replica nodes of the specified master node"}, 129 | {Command: "CLUSTER REPLICATE", Args: "node-id", Version: "3.0.0", Desc: "Reconfigure a node as a replica of the specified master node"}, 130 | {Command: "CLUSTER RESET", Args: "[HARD|SOFT]", Version: "3.0.0", Desc: "Reset a Redis Cluster node"}, 131 | {Command: "CLUSTER SAVECONFIG", Args: "-", Version: "3.0.0", Desc: "Forces the node to save cluster state on disk"}, 132 | {Command: "CLUSTER SET-CONFIG-EPOCH", Args: "config-epoch", Version: "3.0.0", Desc: "Set the configuration epoch in a new node"}, 133 | {Command: "CLUSTER SETSLOT", Args: "slot IMPORTING|MIGRATING|STABLE|NODE [node-id]", Version: "3.0.0", Desc: "Bind a hash slot to a specific node"}, 134 | {Command: "CLUSTER SLAVES", Args: "node-id", Version: "3.0.0", Desc: "List replica nodes of the specified master node"}, 135 | {Command: "CLUSTER SLOTS", Args: "-", Version: "3.0.0", Desc: "Get array of Cluster slot to node mappings"}, 136 | {Command: "COMMAND", Args: "-", Version: "2.8.13", Desc: "Get array of Redis command details"}, 137 | {Command: "COMMAND COUNT", Args: "-", Version: "2.8.13", Desc: "Get total number of Redis commands"}, 138 | {Command: "COMMAND GETKEYS", Args: "-", Version: "2.8.13", Desc: "Extract keys given a full Redis command"}, 139 | {Command: "COMMAND INFO", Args: "command-name [command-name ...]", Version: "2.8.13", Desc: "Get array of specific Redis command details"}, 140 | {Command: "CONFIG GET", Args: "parameter", Version: "2.0.0", Desc: "Get the value of a configuration parameter"}, 141 | {Command: "CONFIG RESETSTAT", Args: "-", Version: "2.0.0", Desc: "Reset the stats returned by INFO"}, 142 | {Command: "CONFIG REWRITE", Args: "-", Version: "2.8.0", Desc: "Rewrite the configuration file with the in memory configuration"}, 143 | {Command: "CONFIG SET", Args: "parameter value", Version: "2.0.0", Desc: "Set a configuration parameter to the given value"}, 144 | {Command: "DBSIZE", Args: "-", Version: "1.0.0", Desc: "Return the number of keys in the selected database"}, 145 | {Command: "DEBUG OBJECT", Args: "key", Version: "1.0.0", Desc: "Get debugging information about a key"}, 146 | {Command: "DEBUG SEGFAULT", Args: "-", Version: "1.0.0", Desc: "Make the server crash"}, 147 | {Command: "DECR", Args: "key", Version: "1.0.0", Desc: "Decrement the integer value of a key by one"}, 148 | {Command: "DECRBY", Args: "key decrement", Version: "1.0.0", Desc: "Decrement the integer value of a key by the given number"}, 149 | {Command: "DEL", Args: "key [key ...]", Version: "1.0.0", Desc: "Delete a key"}, 150 | {Command: "DISCARD", Args: "-", Version: "2.0.0", Desc: "Discard all commands issued after MULTI"}, 151 | {Command: "DUMP", Args: "key", Version: "2.6.0", Desc: "Return a serialized version of the value stored at the specified key."}, 152 | {Command: "ECHO", Args: "message", Version: "1.0.0", Desc: "Echo the given string"}, 153 | {Command: "EVAL", Args: "script numkeys key [key ...] arg [arg ...]", Version: "2.6.0", Desc: "Execute a Lua script server side"}, 154 | {Command: "EVALSHA", Args: "sha1 numkeys key [key ...] arg [arg ...]", Version: "2.6.0", Desc: "Execute a Lua script server side"}, 155 | {Command: "EXEC", Args: "-", Version: "1.2.0", Desc: "Execute all commands issued after MULTI"}, 156 | {Command: "EXISTS", Args: "key [key ...]", Version: "1.0.0", Desc: "Determine if a key exists"}, 157 | {Command: "EXPIRE", Args: "key seconds", Version: "1.0.0", Desc: "Set a key's time to live in seconds"}, 158 | {Command: "EXPIREAT", Args: "key timestamp", Version: "1.2.0", Desc: "Set the expiration for a key as a UNIX timestamp"}, 159 | {Command: "FLUSHALL", Args: "[ASYNC]", Version: "1.0.0", Desc: "Remove all keys from all databases"}, 160 | {Command: "FLUSHDB", Args: "[ASYNC]", Version: "1.0.0", Desc: "Remove all keys from the current database"}, 161 | {Command: "GEOADD", Args: "key longitude latitude member [longitude latitude member ...]", Version: "3.2.0", Desc: "Add one or more geospatial items in the geospatial index represented using a sorted set"}, 162 | {Command: "GEODIST", Args: "key member1 member2 [unit]", Version: "3.2.0", Desc: "Returns the distance between two members of a geospatial index"}, 163 | {Command: "GEOHASH", Args: "key member [member ...]", Version: "3.2.0", Desc: "Returns members of a geospatial index as standard geohash strings"}, 164 | {Command: "GEOPOS", Args: "key member [member ...]", Version: "3.2.0", Desc: "Returns longitude and latitude of members of a geospatial index"}, 165 | {Command: "GEORADIUS", Args: "key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]", Version: "3.2.0", Desc: "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point"}, 166 | {Command: "GEORADIUSBYMEMBER", Args: "key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]", Version: "3.2.0", Desc: "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member"}, 167 | {Command: "GET", Args: "key", Version: "1.0.0", Desc: "Get the value of a key"}, 168 | {Command: "GETBIT", Args: "key offset", Version: "2.2.0", Desc: "Returns the bit value at offset in the string value stored at key"}, 169 | {Command: "GETRANGE", Args: "key start end", Version: "2.4.0", Desc: "Get a substring of the string stored at a key"}, 170 | {Command: "GETSET", Args: "key value", Version: "1.0.0", Desc: "Set the string value of a key and return its old value"}, 171 | {Command: "HDEL", Args: "key field [field ...]", Version: "2.0.0", Desc: "Delete one or more hash fields"}, 172 | {Command: "HEXISTS", Args: "key field", Version: "2.0.0", Desc: "Determine if a hash field exists"}, 173 | {Command: "HGET", Args: "key field", Version: "2.0.0", Desc: "Get the value of a hash field"}, 174 | {Command: "HGETALL", Args: "key", Version: "2.0.0", Desc: "Get all the fields and values in a hash"}, 175 | {Command: "HINCRBY", Args: "key field increment", Version: "2.0.0", Desc: "Increment the integer value of a hash field by the given number"}, 176 | {Command: "HINCRBYFLOAT", Args: "key field increment", Version: "2.6.0", Desc: "Increment the float value of a hash field by the given amount"}, 177 | {Command: "HKEYS", Args: "key", Version: "2.0.0", Desc: "Get all the fields in a hash"}, 178 | {Command: "HLEN", Args: "key", Version: "2.0.0", Desc: "Get the number of fields in a hash"}, 179 | {Command: "HMGET", Args: "key field [field ...]", Version: "2.0.0", Desc: "Get the values of all the given hash fields"}, 180 | {Command: "HMSET", Args: "key field value [field value ...]", Version: "2.0.0", Desc: "Set multiple hash fields to multiple values"}, 181 | {Command: "HSCAN", Args: "key cursor [MATCH pattern] [COUNT count]", Version: "2.8.0", Desc: "Incrementally iterate hash fields and associated values"}, 182 | {Command: "HSET", Args: "key field value", Version: "2.0.0", Desc: "Set the string value of a hash field"}, 183 | {Command: "HSETNX", Args: "key field value", Version: "2.0.0", Desc: "Set the value of a hash field, only if the field does not exist"}, 184 | {Command: "HSTRLEN", Args: "key field", Version: "3.2.0", Desc: "Get the length of the value of a hash field"}, 185 | {Command: "HVALS", Args: "key", Version: "2.0.0", Desc: "Get all the values in a hash"}, 186 | {Command: "INCR", Args: "key", Version: "1.0.0", Desc: "Increment the integer value of a key by one"}, 187 | {Command: "INCRBY", Args: "key increment", Version: "1.0.0", Desc: "Increment the integer value of a key by the given amount"}, 188 | {Command: "INCRBYFLOAT", Args: "key increment", Version: "2.6.0", Desc: "Increment the float value of a key by the given amount"}, 189 | {Command: "INFO", Args: "[section]", Version: "1.0.0", Desc: "Get information and statistics about the server"}, 190 | {Command: "KEYS", Args: "pattern", Version: "1.0.0", Desc: "Find all keys matching the given pattern"}, 191 | {Command: "LASTSAVE", Args: "-", Version: "1.0.0", Desc: "Get the UNIX time stamp of the last successful save to disk"}, 192 | {Command: "LINDEX", Args: "key index", Version: "1.0.0", Desc: "Get an element from a list by its index"}, 193 | {Command: "LINSERT", Args: "key BEFORE|AFTER pivot value", Version: "2.2.0", Desc: "Insert an element before or after another element in a list"}, 194 | {Command: "LLEN", Args: "key", Version: "1.0.0", Desc: "Get the length of a list"}, 195 | {Command: "LPOP", Args: "key", Version: "1.0.0", Desc: "Remove and get the first element in a list"}, 196 | {Command: "LPUSH", Args: "key value [value ...]", Version: "1.0.0", Desc: "Prepend one or multiple values to a list"}, 197 | {Command: "LPUSHX", Args: "key value", Version: "2.2.0", Desc: "Prepend a value to a list, only if the list exists"}, 198 | {Command: "LRANGE", Args: "key start stop", Version: "1.0.0", Desc: "Get a range of elements from a list"}, 199 | {Command: "LREM", Args: "key count value", Version: "1.0.0", Desc: "Remove elements from a list"}, 200 | {Command: "LSET", Args: "key index value", Version: "1.0.0", Desc: "Set the value of an element in a list by its index"}, 201 | {Command: "LTRIM", Args: "key start stop", Version: "1.0.0", Desc: "Trim a list to the specified range"}, 202 | {Command: "MEMORY DOCTOR", Args: "-", Version: "4.0.0", Desc: "Outputs memory problems report"}, 203 | {Command: "MEMORY HELP", Args: "-", Version: "4.0.0", Desc: "Show helpful text about the different subcommands"}, 204 | {Command: "MEMORY MALLOC-STATS", Args: "-", Version: "4.0.0", Desc: "Show allocator internal stats"}, 205 | {Command: "MEMORY PURGE", Args: "-", Version: "4.0.0", Desc: "Ask the allocator to release memory"}, 206 | {Command: "MEMORY STATS", Args: "-", Version: "4.0.0", Desc: "Show memory usage details"}, 207 | {Command: "MEMORY USAGE", Args: "key [SAMPLES count]", Version: "4.0.0", Desc: "Estimate the memory usage of a key"}, 208 | {Command: "MGET", Args: "key [key ...]", Version: "1.0.0", Desc: "Get the values of all the given keys"}, 209 | {Command: "MIGRATE", Args: "host port key | destination-db timeout [COPY] [REPLACE] [KEYS key]", Version: "2.6.0", Desc: "Atomically transfer a key from a Redis instance to another one."}, 210 | {Command: "MONITOR", Args: "-", Version: "1.0.0", Desc: "Listen for all requests received by the server in real time"}, 211 | {Command: "MOVE", Args: "key db", Version: "1.0.0", Desc: "Move a key to another database"}, 212 | {Command: "MSET", Args: "key value [key value ...]", Version: "1.0.1", Desc: "Set multiple keys to multiple values"}, 213 | {Command: "MSETNX", Args: "key value [key value ...]", Version: "1.0.1", Desc: "Set multiple keys to multiple values, only if none of the keys exist"}, 214 | {Command: "MULTI", Args: "-", Version: "1.2.0", Desc: "Mark the start of a transaction block"}, 215 | {Command: "OBJECT", Args: "subcommand [arguments [arguments ...]]", Version: "2.2.3", Desc: "Inspect the internals of Redis objects"}, 216 | {Command: "PERSIST", Args: "key", Version: "2.2.0", Desc: "Remove the expiration from a key"}, 217 | {Command: "PEXPIRE", Args: "key milliseconds", Version: "2.6.0", Desc: "Set a key's time to live in milliseconds"}, 218 | {Command: "PEXPIREAT", Args: "key milliseconds-timestamp", Version: "2.6.0", Desc: "Set the expiration for a key as a UNIX timestamp specified in milliseconds"}, 219 | {Command: "PFADD", Args: "key element [element ...]", Version: "2.8.9", Desc: "Adds the specified elements to the specified HyperLogLog."}, 220 | {Command: "PFCOUNT", Args: "key [key ...]", Version: "2.8.9", Desc: "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s)."}, 221 | {Command: "PFMERGE", Args: "destkey sourcekey [sourcekey ...]", Version: "2.8.9", Desc: "Merge N different HyperLogLogs into a single one."}, 222 | {Command: "PING", Args: "[message]", Version: "1.0.0", Desc: "Ping the server"}, 223 | {Command: "PSETEX", Args: "key milliseconds value", Version: "2.6.0", Desc: "Set the value and expiration in milliseconds of a key"}, 224 | {Command: "PSUBSCRIBE", Args: "pattern [pattern ...]", Version: "2.0.0", Desc: "Listen for messages published to channels matching the given patterns"}, 225 | {Command: "PTTL", Args: "key", Version: "2.6.0", Desc: "Get the time to live for a key in milliseconds"}, 226 | {Command: "PUBLISH", Args: "channel message", Version: "2.0.0", Desc: "Post a message to a channel"}, 227 | {Command: "PUBSUB", Args: "subcommand [argument [argument ...]]", Version: "2.8.0", Desc: "Inspect the state of the Pub/Sub subsystem"}, 228 | {Command: "PUNSUBSCRIBE", Args: "[pattern [pattern ...]]", Version: "2.0.0", Desc: "Stop listening for messages posted to channels matching the given patterns"}, 229 | {Command: "QUIT", Args: "-", Version: "1.0.0", Desc: "Close the connection"}, 230 | {Command: "RANDOMKEY", Args: "-", Version: "1.0.0", Desc: "Return a random key from the keyspace"}, 231 | {Command: "READONLY", Args: "-", Version: "3.0.0", Desc: "Enables read queries for a connection to a cluster replica node"}, 232 | {Command: "READWRITE", Args: "-", Version: "3.0.0", Desc: "Disables read queries for a connection to a cluster replica node"}, 233 | {Command: "RENAME", Args: "key newkey", Version: "1.0.0", Desc: "Rename a key"}, 234 | {Command: "RENAMENX", Args: "key newkey", Version: "1.0.0", Desc: "Rename a key, only if the new key does not exist"}, 235 | {Command: "REPLICAOF", Args: "host port", Version: "5.0.0", Desc: "Make the server a replica of another instance, or promote it as master."}, 236 | {Command: "RESTORE", Args: "key ttl serialized-value [REPLACE]", Version: "2.6.0", Desc: "Create a key using the provided serialized value, previously obtained using DUMP."}, 237 | {Command: "ROLE", Args: "-", Version: "2.8.12", Desc: "Return the role of the instance in the context of replication"}, 238 | {Command: "RPOP", Args: "key", Version: "1.0.0", Desc: "Remove and get the last element in a list"}, 239 | {Command: "RPOPLPUSH", Args: "source destination", Version: "1.2.0", Desc: "Remove the last element in a list, prepend it to another list and return it"}, 240 | {Command: "RPUSH", Args: "key value [value ...]", Version: "1.0.0", Desc: "Append one or multiple values to a list"}, 241 | {Command: "RPUSHX", Args: "key value", Version: "2.2.0", Desc: "Append a value to a list, only if the list exists"}, 242 | {Command: "SADD", Args: "key member [member ...]", Version: "1.0.0", Desc: "Add one or more members to a set"}, 243 | {Command: "SAVE", Args: "-", Version: "1.0.0", Desc: "Synchronously save the dataset to disk"}, 244 | {Command: "SCAN", Args: "cursor [MATCH pattern] [COUNT count]", Version: "2.8.0", Desc: "Incrementally iterate the keys space"}, 245 | {Command: "SCARD", Args: "key", Version: "1.0.0", Desc: "Get the number of members in a set"}, 246 | {Command: "SCRIPT DEBUG", Args: "YES|SYNC|NO", Version: "3.2.0", Desc: "Set the debug mode for executed scripts."}, 247 | {Command: "SCRIPT EXISTS", Args: "sha1 [sha1 ...]", Version: "2.6.0", Desc: "Check existence of scripts in the script cache."}, 248 | {Command: "SCRIPT FLUSH", Args: "-", Version: "2.6.0", Desc: "Remove all the scripts from the script cache."}, 249 | {Command: "SCRIPT KILL", Args: "-", Version: "2.6.0", Desc: "Kill the script currently in execution."}, 250 | {Command: "SCRIPT LOAD", Args: "script", Version: "2.6.0", Desc: "Load the specified Lua script into the script cache."}, 251 | {Command: "SDIFF", Args: "key [key ...]", Version: "1.0.0", Desc: "Subtract multiple sets"}, 252 | {Command: "SDIFFSTORE", Args: "destination key [key ...]", Version: "1.0.0", Desc: "Subtract multiple sets and store the resulting set in a key"}, 253 | {Command: "SELECT", Args: "index", Version: "1.0.0", Desc: "Change the selected database for the current connection"}, 254 | {Command: "SET", Args: "key value [expiration EX seconds|PX milliseconds] [NX|XX]", Version: "1.0.0", Desc: "Set the string value of a key"}, 255 | {Command: "SETBIT", Args: "key offset value", Version: "2.2.0", Desc: "Sets or clears the bit at offset in the string value stored at key"}, 256 | {Command: "SETEX", Args: "key seconds value", Version: "2.0.0", Desc: "Set the value and expiration of a key"}, 257 | {Command: "SETNX", Args: "key value", Version: "1.0.0", Desc: "Set the value of a key, only if the key does not exist"}, 258 | {Command: "SETRANGE", Args: "key offset value", Version: "2.2.0", Desc: "Overwrite part of a string at key starting at the specified offset"}, 259 | {Command: "SHUTDOWN", Args: "[NOSAVE|SAVE]", Version: "1.0.0", Desc: "Synchronously save the dataset to disk and then shut down the server"}, 260 | {Command: "SINTER", Args: "key [key ...]", Version: "1.0.0", Desc: "Intersect multiple sets"}, 261 | {Command: "SINTERSTORE", Args: "destination key [key ...]", Version: "1.0.0", Desc: "Intersect multiple sets and store the resulting set in a key"}, 262 | {Command: "SISMEMBER", Args: "key member", Version: "1.0.0", Desc: "Determine if a given value is a member of a set"}, 263 | {Command: "SLAVEOF", Args: "host port", Version: "1.0.0", Desc: "Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead."}, 264 | {Command: "SLOWLOG", Args: "subcommand [argument]", Version: "2.2.12", Desc: "Manages the Redis slow queries log"}, 265 | {Command: "SMEMBERS", Args: "key", Version: "1.0.0", Desc: "Get all the members in a set"}, 266 | {Command: "SMOVE", Args: "source destination member", Version: "1.0.0", Desc: "Move a member from one set to another"}, 267 | {Command: "SORT", Args: "key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]", Version: "1.0.0", Desc: "Sort the elements in a list, set or sorted set"}, 268 | {Command: "SPOP", Args: "key [count]", Version: "1.0.0", Desc: "Remove and return one or multiple random members from a set"}, 269 | {Command: "SRANDMEMBER", Args: "key [count]", Version: "1.0.0", Desc: "Get one or multiple random members from a set"}, 270 | {Command: "SREM", Args: "key member [member ...]", Version: "1.0.0", Desc: "Remove one or more members from a set"}, 271 | {Command: "SSCAN", Args: "key cursor [MATCH pattern] [COUNT count]", Version: "2.8.0", Desc: "Incrementally iterate Set elements"}, 272 | {Command: "STRLEN", Args: "key", Version: "2.2.0", Desc: "Get the length of the value stored in a key"}, 273 | {Command: "SUBSCRIBE", Args: "channel [channel ...]", Version: "2.0.0", Desc: "Listen for messages published to the given channels"}, 274 | {Command: "SUNION", Args: "key [key ...]", Version: "1.0.0", Desc: "Add multiple sets"}, 275 | {Command: "SUNIONSTORE", Args: "destination key [key ...]", Version: "1.0.0", Desc: "Add multiple sets and store the resulting set in a key"}, 276 | {Command: "SWAPDB", Args: "index index", Version: "4.0.0", Desc: "Swaps two Redis databases"}, 277 | {Command: "SYNC", Args: "-", Version: "1.0.0", Desc: "Internal command used for replication"}, 278 | {Command: "TIME", Args: "-", Version: "2.6.0", Desc: "Return the current server time"}, 279 | {Command: "TOUCH", Args: "key [key ...]", Version: "3.2.1", Desc: "Alters the last access time of a key(s). Returns the number of existing keys specified."}, 280 | {Command: "TTL", Args: "key", Version: "1.0.0", Desc: "Get the time to live for a key"}, 281 | {Command: "TYPE", Args: "key", Version: "1.0.0", Desc: "Determine the type stored at key"}, 282 | {Command: "UNLINK", Args: "key [key ...]", Version: "4.0.0", Desc: "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking."}, 283 | {Command: "UNSUBSCRIBE", Args: "[channel [channel ...]]", Version: "2.0.0", Desc: "Stop listening for messages posted to the given channels"}, 284 | {Command: "UNWATCH", Args: "-", Version: "2.2.0", Desc: "Forget about all watched keys"}, 285 | {Command: "WAIT", Args: "numreplicas timeout", Version: "3.0.0", Desc: "Wait for the synchronous replication of all the write commands sent in the context of the current connection"}, 286 | {Command: "WATCH", Args: "key [key ...]", Version: "2.2.0", Desc: "Watch the given keys to determine execution of the MULTI/EXEC block"}, 287 | {Command: "XACK", Args: "key group ID [ID ...]", Version: "5.0.0", Desc: "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL."}, 288 | {Command: "XADD", Args: "key ID field string [field string ...]", Version: "5.0.0", Desc: "Appends a new entry to a stream"}, 289 | {Command: "XCLAIM", Args: "key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [force] [justid]", Version: "5.0.0", Desc: "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer."}, 290 | {Command: "XDEL", Args: "key ID [ID ...]", Version: "5.0.0", Desc: "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist."}, 291 | {Command: "XGROUP", Args: "[CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]", Version: "5.0.0", Desc: "Create, destroy, and manage consumer groups."}, 292 | {Command: "XINFO", Args: "[CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]", Version: "5.0.0", Desc: "Get information on streams and consumer groups"}, 293 | {Command: "XLEN", Args: "key", Version: "5.0.0", Desc: "Return the number of entires in a stream"}, 294 | {Command: "XPENDING", Args: "key group [start end count] [consumer]", Version: "5.0.0", Desc: "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged."}, 295 | {Command: "XRANGE", Args: "key start end [COUNT count]", Version: "5.0.0", Desc: "Return a range of elements in a stream, with IDs matching the specified IDs interval"}, 296 | {Command: "XREAD", Args: "[COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]", Version: "5.0.0", Desc: "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block."}, 297 | {Command: "XREADGROUP", Args: "GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]", Version: "5.0.0", Desc: "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block."}, 298 | {Command: "XREVRANGE", Args: "key end start [COUNT count]", Version: "5.0.0", Desc: "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE"}, 299 | {Command: "XTRIM", Args: "key MAXLEN [~] count", Version: "5.0.0", Desc: "Trims the stream to (approximately if '~' is passed) a certain size"}, 300 | {Command: "ZADD", Args: "key [NX|XX] [CH] [INCR] score member [score member ...]", Version: "1.2.0", Desc: "Add one or more members to a sorted set, or update its score if it already exists"}, 301 | {Command: "ZCARD", Args: "key", Version: "1.2.0", Desc: "Get the number of members in a sorted set"}, 302 | {Command: "ZCOUNT", Args: "key min max", Version: "2.0.0", Desc: "Count the members in a sorted set with scores within the given values"}, 303 | {Command: "ZINCRBY", Args: "key increment member", Version: "1.2.0", Desc: "Increment the score of a member in a sorted set"}, 304 | {Command: "ZINTERSTORE", Args: "destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]", Version: "2.0.0", Desc: "Intersect multiple sorted sets and store the resulting sorted set in a new key"}, 305 | {Command: "ZLEXCOUNT", Args: "key min max", Version: "2.8.9", Desc: "Count the number of members in a sorted set between a given lexicographical range"}, 306 | {Command: "ZPOPMAX", Args: "key [count]", Version: "5.0.0", Desc: "Remove and return members with the highest scores in a sorted set"}, 307 | {Command: "ZPOPMIN", Args: "key [count]", Version: "5.0.0", Desc: "Remove and return members with the lowest scores in a sorted set"}, 308 | {Command: "ZRANGE", Args: "key start stop [WITHSCORES]", Version: "1.2.0", Desc: "Return a range of members in a sorted set, by index"}, 309 | {Command: "ZRANGEBYLEX", Args: "key min max [LIMIT offset count]", Version: "2.8.9", Desc: "Return a range of members in a sorted set, by lexicographical range"}, 310 | {Command: "ZRANGEBYSCORE", Args: "key min max [WITHSCORES] [LIMIT offset count]", Version: "1.0.5", Desc: "Return a range of members in a sorted set, by score"}, 311 | {Command: "ZRANK", Args: "key member", Version: "2.0.0", Desc: "Determine the index of a member in a sorted set"}, 312 | {Command: "ZREM", Args: "key member [member ...]", Version: "1.2.0", Desc: "Remove one or more members from a sorted set"}, 313 | {Command: "ZREMRANGEBYLEX", Args: "key min max", Version: "2.8.9", Desc: "Remove all members in a sorted set between the given lexicographical range"}, 314 | {Command: "ZREMRANGEBYRANK", Args: "key start stop", Version: "2.0.0", Desc: "Remove all members in a sorted set within the given indexes"}, 315 | {Command: "ZREMRANGEBYSCORE", Args: "key min max", Version: "1.2.0", Desc: "Remove all members in a sorted set within the given scores"}, 316 | {Command: "ZREVRANGE", Args: "key start stop [WITHSCORES]", Version: "1.2.0", Desc: "Return a range of members in a sorted set, by index, with scores ordered from high to low"}, 317 | {Command: "ZREVRANGEBYLEX", Args: "key max min [LIMIT offset count]", Version: "2.8.9", Desc: "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings."}, 318 | {Command: "ZREVRANGEBYSCORE", Args: "key max min [WITHSCORES] [LIMIT offset count]", Version: "2.2.0", Desc: "Return a range of members in a sorted set, by score, with scores ordered from high to low"}, 319 | {Command: "ZREVRANK", Args: "key member", Version: "2.0.0", Desc: "Determine the index of a member in a sorted set, with scores ordered from high to low"}, 320 | {Command: "ZSCAN", Args: "key cursor [MATCH pattern] [COUNT count]", Version: "2.8.0", Desc: "Incrementally iterate sorted sets elements and associated scores"}, 321 | {Command: "ZSCORE", Args: "key member", Version: "1.2.0", Desc: "Get the score associated with the given member in a sorted set"}, 322 | {Command: "ZUNIONSTORE", Args: "destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]", Version: "2.0.0", Desc: "Add multiple sorted sets and store the resulting sorted set in a new key"}, 323 | } 324 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/keytree.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | var RootTreeNode *tview.TreeNode 11 | 12 | var RedisKeyTypes = []string{ 13 | "String", 14 | "List", 15 | "Set", 16 | "Zset", 17 | "Hash", 18 | "Bitmap", 19 | } 20 | 21 | func RenderKeyTreeView() *tview.TreeView { 22 | 23 | RootTreeNode = tview.NewTreeNode("Key Types"). 24 | SetReference(false). 25 | SetColor(tcell.ColorOlive) 26 | 27 | tree := tview.NewTreeView(). 28 | SetRoot(RootTreeNode). 29 | SetCurrentNode(RootTreeNode) 30 | 31 | tree.SetBorder(true).SetTitle("[green]Redis Databases") 32 | 33 | RootTreeNode.SetSelectedFunc(func() { 34 | if RootTreeNode.GetReference() == false { 35 | // Selecting the root node d 36 | loadRedisTypes(RootTreeNode) 37 | RootTreeNode.SetReference(RootTreeNode.GetText()) 38 | return 39 | } 40 | 41 | if len(RootTreeNode.GetChildren()) > 0 { 42 | RootTreeNode.SetExpanded(!RootTreeNode.IsExpanded()) 43 | } 44 | }) 45 | 46 | return tree 47 | } 48 | 49 | func loadRedisTypes(targetNode *tview.TreeNode) { 50 | for _, keyType := range RedisKeyTypes { 51 | node := tview.NewTreeNode("[orange]" + keyType). 52 | SetReference(false). 53 | SetExpanded(false). 54 | SetSelectable(true) 55 | 56 | node.SetSelectedFunc(func() { 57 | 58 | if node.GetReference() == false { 59 | node.SetReference(node.GetText()) 60 | keys, err := RdsClinet.Scan(0, "*", 0, keyType) 61 | if err != nil { 62 | log.Println(err) 63 | } 64 | log.Printf("redis keys: %+v", keys) 65 | 66 | for _, key := range keys { 67 | loadRedisKey(node, key) 68 | } 69 | } 70 | 71 | log.Printf("node children: %+v", node.GetChildren()) 72 | if len(node.GetChildren()) > 0 { 73 | log.Printf("node isExpanded: %+v", node.IsExpanded()) 74 | node.SetExpanded(!node.IsExpanded()) 75 | } 76 | }) 77 | 78 | targetNode.AddChild(node) 79 | 80 | } 81 | } 82 | 83 | func loadRedisKey(targetNode *tview.TreeNode, key string) { 84 | node := tview.NewTreeNode(key). 85 | SetText(key). 86 | SetReference(nil). 87 | SetSelectable(true) 88 | 89 | node.SetSelectedFunc(func() { 90 | // fetch key value 91 | value, err := RdsClinet.GetValue(key) 92 | if err != nil { 93 | log.Println(err) 94 | } 95 | 96 | PrintfResultTextView("[yellow]%s", value) 97 | }) 98 | 99 | targetNode.AddChild(node) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/login.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/LinPr/sqltui/pkg/config" 10 | "github.com/LinPr/sqltui/pkg/tuiapp" 11 | "github.com/gdamore/tcell/v2" 12 | "github.com/redis/go-redis/v9" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | func RenderLoginPage() *tview.Flex { 17 | 18 | form := renderLoginForm() 19 | textView := renderLoginErrTextView() 20 | 21 | flex := tview.NewFlex(). 22 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 2, false). 23 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 24 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 1, false). 25 | AddItem(form, 15, 3, true). 26 | AddItem(textView, 0, 1, false), 50, 3, true). 27 | AddItem(tview.NewBox().SetBorder(false).SetTitle(""), 0, 2, false) 28 | 29 | flex.SetBorder(true) 30 | 31 | tuiapp.RedisTui.AddPage("redis_login", form) 32 | 33 | return flex 34 | } 35 | 36 | var LoginErrOut *tview.TextView 37 | 38 | func printfLoginErrOut(format string, a ...any) { 39 | LoginErrOut.Clear() 40 | erMsg := fmt.Sprintf(format, a...) 41 | LoginErrOut.SetText(erMsg) 42 | } 43 | 44 | func renderLoginForm() *tview.Form { 45 | redisConf, err := config.ReadRedisConfig() 46 | if err != nil { 47 | log.Println("ReadRedisConfig error: ", err) 48 | return nil 49 | } 50 | 51 | form := tview.NewForm(). 52 | AddInputField("username:", redisConf.UserName, 20, nil, nil). 53 | AddInputField("password:", redisConf.Password, 20, nil, nil). 54 | AddInputField(" host:", redisConf.Host, 20, nil, nil). 55 | AddInputField(" port:", redisConf.Port, 20, nil, nil). 56 | AddInputField(" rdbNum:", redisConf.RdbNum, 20, nil, nil). 57 | SetFieldBackgroundColor(tcell.ColorGray) 58 | // AddDropDown(" charset:", []string{"utf8", "ascall", "unicode"}, 0, nil) 59 | 60 | form.AddButton("Connect", ConnectCallback(form)). 61 | AddButton("Save", SaveCallback(form)). 62 | AddButton("Quit", QuitCallback()). 63 | SetButtonsAlign(tview.AlignCenter). 64 | SetButtonBackgroundColor(tcell.ColorGray). 65 | SetButtonTextColor(tcell.ColorLightGoldenrodYellow) 66 | 67 | form.SetBorder(true).SetBorderColor(tcell.ColorWhite) 68 | 69 | form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 70 | log.Println("event: ", event.Key()) 71 | switch event.Key() { 72 | case tcell.KeyCtrlS: 73 | SaveCallback(form)() 74 | 75 | case tcell.KeyEnter: 76 | ConnectCallback(form)() 77 | 78 | } 79 | return event 80 | }) 81 | 82 | return form 83 | } 84 | 85 | func renderLoginErrTextView() *tview.TextView { 86 | textView := tview.NewTextView(). 87 | SetWrap(true). 88 | SetDynamicColors(true) 89 | 90 | LoginErrOut = textView 91 | return textView 92 | } 93 | 94 | func ConnectCallback(form *tview.Form) func() { 95 | return func() { 96 | count := form.GetFormItemCount() 97 | for i := 0; i < count; i++ { 98 | log.Println(form.GetFormItem(i).GetLabel()) 99 | log.Println(form.GetFormItem(i).(*tview.InputField).GetText()) 100 | } 101 | username := form.GetFormItem(0).(*tview.InputField).GetText() 102 | password := form.GetFormItem(1).(*tview.InputField).GetText() 103 | host := form.GetFormItem(2).(*tview.InputField).GetText() 104 | port := form.GetFormItem(3).(*tview.InputField).GetText() 105 | rdbNumStr := form.GetFormItem(4).(*tview.InputField).GetText() 106 | rdbNum, err := strconv.Atoi(rdbNumStr) 107 | if err != nil { 108 | printfLoginErrOut("[red]" + err.Error()) 109 | return 110 | } 111 | 112 | rdsc, err := NewRDS(&redis.Options{ 113 | Addr: fmt.Sprintf("%s:%s", host, port), 114 | Username: username, 115 | Password: password, 116 | DB: rdbNum, 117 | WriteTimeout: 3 * time.Second, 118 | ReadTimeout: 2 * time.Second, 119 | }) // mmust init database client here 120 | if err != nil { 121 | printfLoginErrOut("[red]" + err.Error()) 122 | return 123 | } 124 | _ = rdsc 125 | 126 | // save current config 127 | SaveCallback(form)() 128 | 129 | // SetRootTreeNodeName(GetDbName()) 130 | tuiapp.RedisTui.ShowPage("redis_dashboard") 131 | } 132 | } 133 | 134 | func SaveCallback(form *tview.Form) func() { 135 | return func() { 136 | redisConf := &config.RedisConfig{ 137 | UserName: form.GetFormItem(0).(*tview.InputField).GetText(), 138 | Password: form.GetFormItem(1).(*tview.InputField).GetText(), 139 | Host: form.GetFormItem(2).(*tview.InputField).GetText(), 140 | Port: form.GetFormItem(3).(*tview.InputField).GetText(), 141 | RdbNum: form.GetFormItem(4).(*tview.InputField).GetText(), 142 | } 143 | 144 | if err := config.WriteRedisConfig(redisConf); err != nil { 145 | log.Println("WriteMysqlConfig error: ", err) 146 | } 147 | } 148 | } 149 | 150 | func QuitCallback() func() { 151 | return func() { 152 | // app.Stop() 153 | tuiapp.RedisTui.App.Stop() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/query.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | func RenderQueryWidget() *tview.Flex { 9 | 10 | inputField := RenderInputFiedl() 11 | textView := RenderErrTextView() 12 | 13 | tuiapp.RedisTui.AddWidget(inputField) 14 | 15 | queryWidget := tview.NewFlex(). 16 | SetDirection(tview.FlexRow). 17 | AddItem(inputField, 2, 1, false). 18 | AddItem(textView, 0, 1, false) 19 | queryWidget.SetBorder(true).SetTitle("[green]Query") 20 | 21 | return queryWidget 22 | } 23 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/LinPr/sqltui/pkg/tuiapp" 5 | // "github.com/rivo/tview" 6 | ) 7 | 8 | func Init() { 9 | tuiapp.RedisTui.AddPage("redis_login", RenderLoginPage()) 10 | tuiapp.RedisTui.AddPage("redis_dashboard", RenderDashBoardPage()) 11 | 12 | // first enter into login page 13 | tuiapp.RedisTui.ShowPage("redis_login") 14 | // tuiapp.ShowPage("mysql_dashboard") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/tuiapp/redis/resutltextview.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | var resultTextViewOut *tview.TextView 10 | 11 | func PrintfResultTextView(format string, a ...any) { 12 | resultTextViewOut.Clear() 13 | fmt.Fprintf(resultTextViewOut, format, a...) 14 | } 15 | 16 | func PrintlnResultTextView(a ...any) { 17 | resultTextViewOut.Clear() 18 | fmt.Fprintln(resultTextViewOut, a...) 19 | } 20 | 21 | func ClearResultTextView() { 22 | resultTextViewOut.Clear() 23 | } 24 | 25 | func RenderResultTextView() *tview.TextView { 26 | textView := tview.NewTextView(). 27 | SetDynamicColors(true). 28 | SetRegions(true). 29 | SetWordWrap(true). 30 | SetChangedFunc(func() { 31 | // app.Draw() 32 | }) 33 | 34 | textView.SetBorder(true).SetTitle("Result") 35 | 36 | resultTextViewOut = textView 37 | return textView 38 | } 39 | -------------------------------------------------------------------------------- /pkg/tuiapp/tuiapp.go: -------------------------------------------------------------------------------- 1 | package tuiapp 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | // type tuiApp struct { 8 | // MysqlApp *tview.Application 9 | // RedisApp *tview.Application 10 | // } 11 | 12 | type TuiApp struct { 13 | App *tview.Application 14 | Pages *tview.Pages 15 | Widget []tview.Primitive 16 | } 17 | 18 | var ( 19 | // TuiApp *tuiApp 20 | MysqlTui *TuiApp 21 | RedisTui *TuiApp 22 | // Pages *tview.Pages 23 | // Widget []tview.Primitive 24 | ) 25 | 26 | func init() { 27 | MysqlTui = &TuiApp{ 28 | App: tview.NewApplication(), 29 | Pages: tview.NewPages(), 30 | Widget: make([]tview.Primitive, 0), 31 | } 32 | RedisTui = &TuiApp{ 33 | App: tview.NewApplication(), 34 | Pages: tview.NewPages(), 35 | Widget: make([]tview.Primitive, 0), 36 | } 37 | // Pages = tview.NewPages() 38 | // Widget = make([]tview.Primitive, 0) 39 | 40 | } 41 | 42 | func (ta *TuiApp) GetPage() *tview.Pages { 43 | return ta.Pages 44 | } 45 | 46 | func (ta *TuiApp) AddPage(name string, item tview.Primitive) { 47 | ta.Pages.AddPage(name, item, true, false) 48 | 49 | } 50 | 51 | func (ta *TuiApp) ShowPage(name string) { 52 | ta.Pages.SwitchToPage(name) 53 | } 54 | 55 | func (ta *TuiApp) AddWidget(w tview.Primitive) { 56 | ta.Widget = append(ta.Widget, w) 57 | } 58 | 59 | func (ta *TuiApp) GetCurrentFocus() tview.Primitive { 60 | return ta.App.GetFocus() 61 | } 62 | 63 | func (ta *TuiApp) SetNextFocus() { 64 | wiget := ta.GetCurrentFocus() 65 | ta.App.SetFocus(ta.NextWigets(wiget)) 66 | } 67 | 68 | func (ta *TuiApp) PreviousWidgets(curent tview.Primitive) tview.Primitive { 69 | for i, w := range ta.Widget { 70 | if w == curent { 71 | if i-1 >= 0 { 72 | return ta.Widget[i-1] 73 | } 74 | return ta.Widget[len(ta.Widget)-1] 75 | } 76 | } 77 | return ta.Widget[0] 78 | } 79 | 80 | func (ta *TuiApp) NextWigets(curent tview.Primitive) tview.Primitive { 81 | for i, w := range ta.Widget { 82 | if w == curent { 83 | if i+1 < len(ta.Widget) { 84 | return ta.Widget[i+1] 85 | } 86 | return ta.Widget[0] 87 | } 88 | } 89 | return ta.Widget[0] 90 | } 91 | --------------------------------------------------------------------------------