├── .gitignore ├── LICENSE ├── README.md ├── common └── common.go ├── example ├── processbar │ └── main.go ├── prompt │ └── main.go └── selector │ └── main.go ├── go.mod ├── go.sum ├── progressbar └── progressbar.go ├── prompt └── prompt.go ├── resources ├── progressbar.gif ├── prompt.gif └── selector.gif └── selector └── selector.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mritd 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 | # bubbles 2 | 3 | bubbles is a terminal prompt library created based on [charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea). 4 | Based on bubbletea, it can be more robust and easy to maintain. Now bubbles are 5 | used to replace [promptx](https://github.com/mritd/promptx). 6 | 7 | ### selector 8 | 9 | The `selector` is a terminal single-selection list library. The `selector` library provides the functions 10 | of page up and down and key movement, and supports custom rendering methods. 11 | 12 | ![selector.gif](resources/selector.gif) 13 | 14 | ### prompt 15 | 16 | The `prompt` is a terminal input prompt library. The `prompt` library provides CJK character support 17 | and standard terminal shortcut keys (such as `ctrl+a`, `ctrl+e`), password input echo and other functions. 18 | 19 | ![prompt.gif](resources/prompt.gif) 20 | 21 | ### progressbar 22 | 23 | The `progressbar` is a terminal progress bar library. The terminal `progressbar` library provides a terminal 24 | progress bar with a function. After each function is executed successfully, the progress bar advances 25 | a certain distance. If the function returns an error message, the progress bar is terminated. 26 | 27 | ![progressbar.gif](resources/progressbar.gif) -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/muesli/termenv" 6 | ) 7 | 8 | const DONE = "DONE" 9 | 10 | var term = termenv.ColorProfile() 11 | 12 | // FontColor sets the color of the given string and bolds the font 13 | func FontColor(str, color string) string { 14 | return termenv.String(str).Foreground(term.Color(color)).Bold().String() 15 | } 16 | 17 | // GenSpaces generate a space string of specified length 18 | func GenSpaces(l int) string { 19 | return GenStr(l, " ") 20 | } 21 | 22 | // GenMask generate a mask string of the specified length 23 | func GenMask(l int) string { 24 | return GenStr(l, "*") 25 | } 26 | 27 | // GenStr generate a string of the specified length, the string is composed of the given characters 28 | func GenStr(l int, s string) string { 29 | var ss string 30 | for i := 0; i < l; i++ { 31 | ss += s 32 | } 33 | return ss 34 | } 35 | 36 | func Done() tea.Msg { 37 | return DONE 38 | } 39 | -------------------------------------------------------------------------------- /example/processbar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/mritd/bubbles/progressbar" 10 | ) 11 | 12 | func main() { 13 | m := &progressbar.Model{ 14 | Width: 40, 15 | InitMessage: "Initializing, please wait...", 16 | Stages: []progressbar.ProgressFunc{ 17 | func() (string, error) { 18 | time.Sleep(time.Second) 19 | return "🐌 INFO: stage1", nil 20 | }, 21 | func() (string, error) { 22 | time.Sleep(time.Second) 23 | return "🐌🐌 INFO: stage2", nil 24 | }, 25 | func() (string, error) { 26 | time.Sleep(time.Second) 27 | return "🐌🐌🐌 INFO: stage3", nil 28 | }, 29 | func() (string, error) { 30 | time.Sleep(time.Second) 31 | return "🐌🐌🐌🐌 INFO: stage4", nil 32 | }, 33 | func() (string, error) { 34 | time.Sleep(time.Second) 35 | return "🐌🐌🐌🐌🐌 INFO: stage5", fmt.Errorf("🐞 Error: test error") 36 | }, 37 | }, 38 | } 39 | 40 | p := tea.NewProgram(m) 41 | err := p.Start() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | if m.Error() != nil { 47 | fmt.Printf("Stage func [%d] run failed: %s\n", m.Index()+1, m.Error()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/prompt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mritd/bubbles/common" 5 | "log" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/mritd/bubbles/prompt" 9 | ) 10 | 11 | type model struct { 12 | input *prompt.Model 13 | } 14 | 15 | func (m model) Init() tea.Cmd { 16 | return nil 17 | } 18 | 19 | func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 20 | // By default, the prompt component will not return a "tea.Quit" 21 | // message unless Ctrl+C is pressed. 22 | // 23 | // If there is no error in the input, the prompt component returns 24 | // a "common.DONE" message when the Enter key is pressed. 25 | switch msg { 26 | case common.DONE: 27 | return m, tea.Quit 28 | } 29 | 30 | _, cmd := m.input.Update(msg) 31 | return m, cmd 32 | } 33 | 34 | func (m model) View() string { 35 | return m.input.View() 36 | } 37 | 38 | func (m model) Value() string { 39 | return m.input.Value() 40 | } 41 | 42 | func main() { 43 | m := model{input: &prompt.Model{ValidateFunc: prompt.VFNotBlank}} 44 | p := tea.NewProgram(&m) 45 | err := p.Start() 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | log.Println(m.Value()) 50 | } 51 | -------------------------------------------------------------------------------- /example/selector/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/mritd/bubbles/common" 8 | 9 | "github.com/mritd/bubbles/selector" 10 | 11 | tea "github.com/charmbracelet/bubbletea" 12 | ) 13 | 14 | type model struct { 15 | sl selector.Model 16 | } 17 | 18 | func (m model) Init() tea.Cmd { 19 | return nil 20 | } 21 | func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 22 | // By default, the prompt component will not return a "tea.Quit" 23 | // message unless Ctrl+C is pressed. 24 | // 25 | // If there is no error in the input, the prompt component returns 26 | // a "common.DONE" message when the Enter key is pressed. 27 | switch msg { 28 | case common.DONE: 29 | return m, tea.Quit 30 | } 31 | 32 | _, cmd := m.sl.Update(msg) 33 | return m, cmd 34 | } 35 | 36 | func (m model) View() string { 37 | return m.sl.View() 38 | } 39 | 40 | type TypeMessage struct { 41 | Type string 42 | ZHDescription string 43 | ENDescription string 44 | } 45 | 46 | func main() { 47 | m := &model{ 48 | sl: selector.Model{ 49 | Data: []interface{}{ 50 | TypeMessage{Type: "feat", ZHDescription: "新功能", ENDescription: "Introducing new features"}, 51 | TypeMessage{Type: "fix", ZHDescription: "修复 Bug", ENDescription: "Bug fix"}, 52 | TypeMessage{Type: "docs", ZHDescription: "添加文档", ENDescription: "Writing docs"}, 53 | TypeMessage{Type: "style", ZHDescription: "调整格式", ENDescription: "Improving structure/format of the code"}, 54 | TypeMessage{Type: "refactor", ZHDescription: "重构代码", ENDescription: "Refactoring code"}, 55 | TypeMessage{Type: "test", ZHDescription: "增加测试", ENDescription: "When adding missing tests"}, 56 | TypeMessage{Type: "chore", ZHDescription: "CI/CD 变动", ENDescription: "Changing CI/CD"}, 57 | TypeMessage{Type: "perf", ZHDescription: "性能优化", ENDescription: "Improving performance"}, 58 | }, 59 | PerPage: 5, 60 | // Use the arrow keys to navigate: ↓ ↑ → ← 61 | // Select Commit Type: 62 | HeaderFunc: selector.DefaultHeaderFuncWithAppend("Select Commit Type:"), 63 | // [1] feat (Introducing new features) 64 | SelectedFunc: func(m selector.Model, obj interface{}, gdIndex int) string { 65 | t := obj.(TypeMessage) 66 | return common.FontColor(fmt.Sprintf("[%d] %s (%s)", gdIndex+1, t.Type, t.ENDescription), selector.ColorSelected) 67 | }, 68 | // 2. fix (Bug fix) 69 | UnSelectedFunc: func(m selector.Model, obj interface{}, gdIndex int) string { 70 | t := obj.(TypeMessage) 71 | return common.FontColor(fmt.Sprintf(" %d. %s (%s)", gdIndex+1, t.Type, t.ENDescription), selector.ColorUnSelected) 72 | }, 73 | // --------- Commit Type ---------- 74 | // Type: feat 75 | // Description: 新功能(Introducing new features) 76 | FooterFunc: func(m selector.Model, obj interface{}, gdIndex int) string { 77 | t := m.Selected().(TypeMessage) 78 | footerTpl := ` 79 | Type: %s 80 | Description: %s(%s)` 81 | return common.FontColor(fmt.Sprintf(footerTpl, t.Type, t.ZHDescription, t.ENDescription), selector.ColorFooter) 82 | }, 83 | FinishedFunc: func(s interface{}) string { 84 | return common.FontColor("Current selected: ", selector.ColorFinished) + s.(TypeMessage).Type + "\n" 85 | }, 86 | }, 87 | } 88 | 89 | p := tea.NewProgram(m) 90 | err := p.Start() 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | if !m.sl.Canceled() { 95 | log.Printf("selected index => %d\n", m.sl.Index()) 96 | log.Printf("selected vaule => %s\n", m.sl.Selected()) 97 | } else { 98 | log.Println("user canceled...") 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mritd/bubbles 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.8.0 7 | github.com/charmbracelet/bubbletea v0.14.1 8 | github.com/lucasb-eyer/go-colorful v1.2.0 9 | github.com/mattn/go-runewidth v0.0.13 10 | github.com/muesli/reflow v0.3.0 11 | github.com/muesli/termenv v0.9.0 12 | ) 13 | 14 | require ( 15 | github.com/atotto/clipboard v0.1.2 // indirect 16 | github.com/charmbracelet/lipgloss v0.1.2 // indirect 17 | github.com/containerd/console v1.0.1 // indirect 18 | github.com/mattn/go-isatty v0.0.13 // indirect 19 | github.com/pkg/errors v0.9.1 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect 22 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= 2 | github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/charmbracelet/bubbles v0.8.0 h1:+l2op90Ag37Vn+30O1hbg/0wBl+e+sxHhgY1F/rvdHs= 4 | github.com/charmbracelet/bubbles v0.8.0/go.mod h1:5WX1sSSjNCgCrzvRMN/z23HxvWaa+AI16Ch0KPZPeDs= 5 | github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg= 6 | github.com/charmbracelet/bubbletea v0.14.1 h1:pD/bM5LBEH/nDo7nKcgNUgi4uRHQhpWTIHZbG5vuSlc= 7 | github.com/charmbracelet/bubbletea v0.14.1/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE= 8 | github.com/charmbracelet/lipgloss v0.1.2 h1:D+LUMg34W7n2pkuMrevKVxT7HXqnoRHm7IoomkX3/ZU= 9 | github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg= 10 | github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= 11 | github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= 12 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 13 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 14 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 15 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 16 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 17 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= 18 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 19 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 20 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 21 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 22 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 23 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 24 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 25 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 26 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 27 | github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8= 28 | github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= 29 | github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= 30 | github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= 31 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 32 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 33 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 34 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 35 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 38 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= 47 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0= 49 | golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 50 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 51 | -------------------------------------------------------------------------------- /progressbar/progressbar.go: -------------------------------------------------------------------------------- 1 | package progressbar 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/lucasb-eyer/go-colorful" 11 | "github.com/muesli/reflow/indent" 12 | "github.com/muesli/termenv" 13 | ) 14 | 15 | const ( 16 | progressFullChar = "█" 17 | progressEmptyChar = "░" 18 | ) 19 | 20 | // General stuff for styling the view 21 | var ( 22 | term = termenv.ColorProfile() 23 | subtle = makeFgStyle("241") 24 | progressEmpty = subtle(progressEmptyChar) 25 | ) 26 | 27 | // ProgressFunc is a simple function, the progress bar will step a certain distance after each execution 28 | type ProgressFunc func() (string, error) 29 | 30 | // Model is a data container used to store TUI status information. 31 | type Model struct { 32 | Width int 33 | Stages []ProgressFunc 34 | InitMessage string 35 | stageIndex int 36 | message string 37 | err error 38 | progress float64 39 | loaded bool 40 | init bool 41 | canceled bool 42 | } 43 | 44 | // Init performs some io initialization actions, The current Init returns the first ProgressFunc 45 | // to trigger the program to run. 46 | func (m Model) Init() tea.Cmd { 47 | return func() tea.Msg { 48 | if len(m.Stages) > 0 { 49 | return m.Stages[m.stageIndex] 50 | } else { 51 | return nil 52 | } 53 | } 54 | } 55 | 56 | // View reads the data state of the data model for rendering 57 | func (m Model) View() string { 58 | prompt := indent.String("\n"+makeInfo(m.message), 2) 59 | if m.err != nil { 60 | prompt = indent.String("\n"+makeError(m.err.Error()), 2) 61 | } 62 | bar := indent.String("\n"+progressbar(m.Width, m.progress)+"%"+"\n\n", 2) 63 | return prompt + bar 64 | } 65 | 66 | // Canceled determine whether the operation is cancelled 67 | func (m *Model) Canceled() bool { 68 | return m.canceled 69 | } 70 | 71 | // Update method responds to various events and modifies the data model 72 | // according to the corresponding events 73 | func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 74 | if !m.init { 75 | m.initData() 76 | m.message = makeInfo(m.InitMessage) 77 | return m, nil 78 | } 79 | 80 | // Make sure these keys always quit 81 | if msg, ok := msg.(tea.KeyMsg); ok { 82 | k := msg.String() 83 | if k == "q" || k == "esc" || k == "ctrl+c" { 84 | m.canceled = true 85 | return m, tea.Quit 86 | } 87 | } 88 | 89 | if pf, ok := msg.(ProgressFunc); ok { 90 | m.message, m.err = pf() 91 | if m.err != nil { 92 | return m, tea.Quit 93 | } 94 | if m.stageIndex < len(m.Stages)-1 { 95 | m.stageIndex++ 96 | } 97 | if !m.loaded { 98 | // The progress bar steps a certain distance after each successful execution 99 | m.progress += float64(1) / float64(len(m.Stages)) 100 | // If all ProgressFunc has been executed, exit the TUI 101 | if m.progress > 1 { 102 | m.progress = 1 103 | m.loaded = true 104 | return m, tea.Quit 105 | } 106 | } 107 | } 108 | 109 | // Return to the next ProgressFunc, trigger the traversal 110 | return m, func() tea.Msg { 111 | return m.Stages[m.stageIndex] 112 | } 113 | } 114 | 115 | // initData initialize the data model, set the default value and 116 | // fix the wrong parameter settings during initialization 117 | func (m *Model) initData() { 118 | m.stageIndex = 0 119 | if m.Width == 0 { 120 | m.Width = 40 121 | } 122 | m.init = true 123 | } 124 | 125 | // Error returns the error generated during the execution of ProgressFunc 126 | func (m *Model) Error() error { 127 | return m.err 128 | } 129 | 130 | // Index returns the index of the ProgressFunc currently executed 131 | func (m *Model) Index() int { 132 | return m.stageIndex 133 | } 134 | 135 | // progressbar is responsible for rendering the progress bar UI 136 | func progressbar(width int, percent float64) string { 137 | w := float64(width) 138 | 139 | fullSize := int(math.Round(w * percent)) 140 | var fullCells string 141 | for i := 0; i < fullSize; i++ { 142 | fullCells += termenv.String(progressFullChar).Foreground(term.Color(makeRamp("#B14FFF", "#00FFA3", w)[i])).String() 143 | } 144 | 145 | emptySize := int(w) - fullSize 146 | emptyCells := strings.Repeat(progressEmpty, emptySize) 147 | 148 | return fmt.Sprintf("%s%s %3.0f", fullCells, emptyCells, math.Round(percent*100)) 149 | } 150 | 151 | // Utils 152 | 153 | // Color a string's foreground with the given value. 154 | func colorFg(val, color string) string { 155 | return termenv.String(val).Foreground(term.Color(color)).String() 156 | } 157 | 158 | // Return a function that will colorize the foreground of a given string. 159 | func makeFgStyle(color string) func(string) string { 160 | return termenv.Style{}.Foreground(term.Color(color)).Styled 161 | } 162 | 163 | // Color a string's foreground and background with the given value. 164 | func makeFgBgStyle(fg, bg string) func(string) string { 165 | return termenv.Style{}. 166 | Foreground(term.Color(fg)). 167 | Background(term.Color(bg)). 168 | Styled 169 | } 170 | 171 | // Generate a blend of colors. 172 | func makeRamp(colorA, colorB string, steps float64) (s []string) { 173 | cA, _ := colorful.Hex(colorA) 174 | cB, _ := colorful.Hex(colorB) 175 | 176 | for i := 0.0; i < steps; i++ { 177 | c := cA.BlendLuv(cB, i/steps) 178 | s = append(s, colorToHex(c)) 179 | } 180 | return 181 | } 182 | 183 | // Convert a colorful.Color to a hexidecimal format compatible with termenv. 184 | func colorToHex(c colorful.Color) string { 185 | return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B)) 186 | } 187 | 188 | // Helper function for converting colors to hex. Assumes a value between 0 and 189 | // 1. 190 | func colorFloatToHex(f float64) (s string) { 191 | s = strconv.FormatInt(int64(f*255), 16) 192 | if len(s) == 1 { 193 | s = "0" + s 194 | } 195 | return 196 | } 197 | 198 | func makeInfo(msg string) string { 199 | return termenv.String(msg).Foreground(term.Color("2")).Bold().String() 200 | } 201 | 202 | func makeError(msg string) string { 203 | return termenv.String(msg).Foreground(term.Color("9")).Bold().String() 204 | } 205 | -------------------------------------------------------------------------------- /prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/mritd/bubbles/common" 7 | "strings" 8 | 9 | "github.com/charmbracelet/bubbles/textinput" 10 | tea "github.com/charmbracelet/bubbletea" 11 | ) 12 | 13 | const ( 14 | DefaultPrompt = "Please Input: " 15 | DefaultValidateOkPrefix = "✔" 16 | DefaultValidateErrPrefix = "✘" 17 | 18 | ColorPrompt = "2" 19 | colorValidateOk = "2" 20 | colorValidateErr = "1" 21 | ) 22 | 23 | // EchoMode sets the input behavior of the text input field. 24 | type EchoMode int 25 | 26 | // copy from textinput.Model 27 | const ( 28 | // EchoNormal displays text as is. This is the default behavior. 29 | EchoNormal EchoMode = iota 30 | 31 | // EchoPassword displays the EchoCharacter mask instead of actual 32 | // characters. This is commonly used for password fields. 33 | EchoPassword 34 | 35 | // EchoNone displays nothing as characters are entered. This is commonly 36 | // seen for password fields on the command line. 37 | EchoNone 38 | ) 39 | 40 | // Model is a data container used to store TUI status information, 41 | // the ui rendering success style is as follows: 42 | // 43 | // ✔ Please Input: aaaa 44 | type Model struct { 45 | // CharLimit is the maximum amount of characters this input element will 46 | // accept. If 0 or less, there's no limit. 47 | CharLimit int 48 | 49 | // Width is the maximum number of characters that can be displayed at once. 50 | // It essentially treats the text field like a horizontally scrolling 51 | // viewport. If 0 or less this setting is ignored. 52 | Width int 53 | 54 | // Prompt is the prefix of the prompt library, the user needs to define 55 | // the format(including spaces) 56 | Prompt string 57 | 58 | // ValidateFunc is a "real-time verification" function, which verifies 59 | // whether the terminal input data is legal in real time 60 | ValidateFunc func(string) error 61 | 62 | // ValidateOkPrefix is the prompt prefix when the validation fails 63 | ValidateOkPrefix string 64 | 65 | // ValidateErrPrefix is the prompt prefix when the verification is successful 66 | ValidateErrPrefix string 67 | 68 | // EchoMode sets the input behavior of the text input field. 69 | EchoMode EchoMode 70 | 71 | init bool 72 | canceled bool 73 | finished bool 74 | showErr bool 75 | err error 76 | 77 | input textinput.Model 78 | } 79 | 80 | // initData initialize the data model, set the default value and 81 | // fix the wrong parameter settings during initialization 82 | func (m *Model) initData() { 83 | if m.ValidateFunc == nil { 84 | m.ValidateFunc = VFDoNothing 85 | } 86 | if m.ValidateOkPrefix == "" { 87 | m.ValidateOkPrefix = DefaultValidateOkPrefix 88 | } 89 | if m.ValidateErrPrefix == "" { 90 | m.ValidateErrPrefix = DefaultValidateErrPrefix 91 | } 92 | if m.Prompt == "" { 93 | m.Prompt = common.FontColor(DefaultPrompt, ColorPrompt) 94 | } 95 | 96 | in := textinput.NewModel() 97 | in.CharLimit = m.CharLimit 98 | in.Width = m.Width 99 | in.Prompt = m.Prompt 100 | in.EchoMode = textinput.EchoMode(m.EchoMode) 101 | in.Focus() 102 | 103 | m.input = in 104 | m.init = true 105 | } 106 | 107 | // View reads the data state of the data model for rendering 108 | func (m Model) View() string { 109 | if m.finished { 110 | switch m.EchoMode { 111 | case EchoNormal: 112 | return common.FontColor(m.ValidateOkPrefix, colorValidateOk) + " " + m.Prompt + m.Value() + "\n" 113 | case EchoNone: 114 | return common.FontColor(m.ValidateOkPrefix, colorValidateOk) + " " + m.Prompt + "\n" 115 | case EchoPassword: 116 | return common.FontColor(m.ValidateOkPrefix, colorValidateOk) + " " + m.Prompt + common.GenMask(len([]rune(m.Value()))) + "\n" 117 | } 118 | } 119 | 120 | var prompt, errMsg string 121 | if m.err != nil { 122 | prompt = common.FontColor(m.ValidateErrPrefix, colorValidateErr) + " " + m.input.View() 123 | if m.showErr { 124 | errMsg = common.FontColor(fmt.Sprintf("%s ERROR: %s\n", m.ValidateErrPrefix, m.err.Error()), colorValidateErr) 125 | return fmt.Sprintf("%s\n%s\n", prompt, errMsg) 126 | } 127 | } else { 128 | prompt = common.FontColor(m.ValidateOkPrefix, colorValidateOk) + " " + m.input.View() 129 | } 130 | 131 | return prompt + "\n" 132 | } 133 | 134 | // Update method responds to various events and modifies the data model 135 | // according to the corresponding events 136 | func (m *Model) Update(msg tea.Msg) (*Model, tea.Cmd) { 137 | if !m.init { 138 | m.initData() 139 | return m, nil 140 | } 141 | 142 | var cmd tea.Cmd 143 | switch msg := msg.(type) { 144 | case tea.KeyMsg: 145 | // We intercept some key events, because we need to handle it in the upper layer 146 | switch msg.Type { 147 | case tea.KeyCtrlC: 148 | // Terminate the UI program when Ctrl+C is pressed 149 | m.canceled = true 150 | return m, tea.Quit 151 | case tea.KeyEnter: 152 | // If the real-time verification function does not return an error, 153 | // then the input has been completed 154 | if m.err == nil { 155 | m.finished = true 156 | return m, common.Done 157 | } 158 | 159 | // If there is a verification error, the error message should be display 160 | m.showErr = true 161 | case tea.KeyRunes: 162 | // Hide verification failure message when entering content again 163 | m.showErr = false 164 | m.err = nil 165 | } 166 | 167 | // Call the underlying textinput to update the terminal display 168 | m.input, cmd = m.input.Update(msg) 169 | // Perform real-time verification function after each input 170 | m.err = m.ValidateFunc(m.input.Value()) 171 | 172 | // We handle errors just like any other message 173 | // Note: msg is error only when there is an unexpected error in the underlying textinput 174 | case error: 175 | m.err = msg 176 | m.showErr = true 177 | return m, nil 178 | } 179 | 180 | return m, cmd 181 | } 182 | 183 | // Value return the input string 184 | func (m Model) Value() string { 185 | return m.input.Value() 186 | } 187 | 188 | // Canceled determine whether the operation is cancelled 189 | func (m Model) Canceled() bool { 190 | return m.canceled 191 | } 192 | 193 | // VFDoNothing is a verification function that does nothing 194 | func VFDoNothing(_ string) error { return nil } 195 | 196 | // VFNotBlank is a verification function that checks whether the input is empty 197 | func VFNotBlank(s string) error { 198 | if strings.TrimSpace(s) == "" { 199 | return errors.New("input is empty") 200 | } 201 | return nil 202 | } 203 | -------------------------------------------------------------------------------- /resources/progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mritd/bubbles/cb7a572fb8316ff1ab10615573d93b2f5298d4d6/resources/progressbar.gif -------------------------------------------------------------------------------- /resources/prompt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mritd/bubbles/cb7a572fb8316ff1ab10615573d93b2f5298d4d6/resources/prompt.gif -------------------------------------------------------------------------------- /resources/selector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mritd/bubbles/cb7a572fb8316ff1ab10615573d93b2f5298d4d6/resources/selector.gif -------------------------------------------------------------------------------- /selector/selector.go: -------------------------------------------------------------------------------- 1 | // Package selector is a terminal single-selection list library. selector library provides the 2 | // functions of page up and down and key movement, and supports custom rendering methods. 3 | package selector 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/mritd/bubbles/common" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/mattn/go-runewidth" 14 | ) 15 | 16 | const ( 17 | DefaultHeader = "Use the arrow keys to navigate: ↓ ↑ → ←" 18 | DefaultFooter = "Current page number details: %d/%d" 19 | DefaultCursor = "»" 20 | DefaultFinished = "Current selected: %s\n" 21 | 22 | ColorHeader = "15" 23 | ColorFooter = "15" 24 | ColorCursor = "2" 25 | ColorFinished = "2" 26 | ColorSelected = "14" 27 | ColorUnSelected = "8" 28 | ) 29 | 30 | // Model is a data container used to store TUI status information, 31 | // the ui rendering success style is as follows: 32 | // 33 | // Use the arrow keys to navigate: ↓ ↑ → ← 34 | // Select Commit Type: 35 | // 36 | // » [1] feat (Introducing new features) 37 | // 2. fix (Bug fix) 38 | // 3. docs (Writing docs) 39 | // 4. style (Improving structure/format of the code) 40 | // 5. refactor (Refactoring code) 41 | // 42 | // --------- Commit Type ---------- 43 | // Type: feat 44 | // Description: 新功能(Introducing new features) 45 | type Model struct { 46 | // HeaderFunc Header rendering function 47 | HeaderFunc func(m Model, obj interface{}, gdIndex int) string 48 | // Cursor cursor rendering style 49 | Cursor string 50 | // CursorColor cursor rendering color 51 | CursorColor string 52 | // SelectedFunc selected data rendering function 53 | SelectedFunc func(m Model, obj interface{}, gdIndex int) string 54 | // UnSelectedFunc unselected data rendering function 55 | UnSelectedFunc func(m Model, obj interface{}, gdIndex int) string 56 | // FooterFunc footer rendering function 57 | FooterFunc func(m Model, obj interface{}, gdIndex int) string 58 | // FinishedFunc finished rendering function 59 | FinishedFunc func(selected interface{}) string 60 | // PerPage data count per page 61 | PerPage int 62 | // Data the data set to be rendered 63 | Data []interface{} 64 | 65 | // init indicates whether the data model has completed initialization 66 | init bool 67 | // canceled indicates whether the operation was cancelled 68 | canceled bool 69 | // finished indicates whether it has exited 70 | finished bool 71 | // pageData data set rendered in real time on the current page 72 | pageData []interface{} 73 | // index global real time index 74 | index int 75 | // maxIndex global max index 76 | maxIndex int 77 | // pageIndex real time index of current page 78 | pageIndex int 79 | // pageMaxIndex current page max index 80 | pageMaxIndex int 81 | } 82 | 83 | // View reads the data state of the data model for rendering 84 | func (m Model) View() string { 85 | if m.finished { 86 | return m.FinishedFunc(m.Selected()) 87 | } 88 | 89 | // the cursor only needs to be displayed correctly 90 | cursor := common.FontColor(m.Cursor, m.CursorColor) 91 | // template functions may be displayed dynamically at the head, tail and data area 92 | // of the list, and a dynamic index(globalDynamicIndex) needs to be added 93 | var header, data, footer string 94 | for i, obj := range m.pageData { 95 | // cursor prefix (selected lines need to be displayed, 96 | // non-selected lines need not be displayed) 97 | var cursorPrefix string 98 | // the rendering style of each row of data (the rendering color 99 | // of selected rows and non-selected rows is different) 100 | var dataLine string 101 | // consider three cases when calculating globalDynamicIndex: 102 | // 103 | // first page: pageIndex(real time page index)、index(global real time index) keep the two consistent 104 | // 1. feat (Introducing new features) 105 | // 2. fix (Bug fix) 106 | // 3. docs (Writing docs) 107 | // 4. style (Improving structure/format of the code) 108 | // 5. refactor (Refactoring code) 109 | // » [6] test (When adding missing tests) 110 | // 111 | // slide down to page: pageIndex fixed to maximum, index increasing with sliding 112 | // 2. fix (Bug fix) 113 | // 3. docs (Writing docs) 114 | // 4. style (Improving structure/format of the code) 115 | // 5. refactor (Refactoring code) 116 | // 6. test (When adding missing tests) 117 | // » [7] chore (Changing CI/CD) 118 | // 119 | // swipe up to page: pageIndex fixed to minimum, index decrease with sliding 120 | // » [3] docs (Writing docs) 121 | // 4. style (Improving structure/format of the code) 122 | // 5. refactor (Refactoring code) 123 | // 6. test (When adding missing tests) 124 | // 7. chore (Changing CI/CD) 125 | // 8. perf (Improving performance) 126 | // 127 | // in three cases, `m.index - m.pageIndex = n`, `n` is the distance between the global real-time 128 | // index and the page real-time index. when traversing the page data area, think of the traversal 129 | // index i as a real-time page index pageIndex, `i + n =` i corresponding global index 130 | globalDynamicIndex := i + (m.index - m.pageIndex) 131 | // when traversing the data area, if the traversed index is equal to the current page index, 132 | // the currently traversed data is the data selected in the list menu, otherwise it is unselected data 133 | if i == m.pageIndex { 134 | // keep a space between the cursor and the selected data style 135 | cursorPrefix = cursor + " " 136 | // m: A copy of the current object and pass it to the user-defined rendering function to facilitate 137 | // the user to read some state information for rendering 138 | // 139 | // obj: The single data currently traversed to the data area; pass it to the user-defined rendering 140 | // function to help users know the current data that needs to be rendered 141 | // 142 | // globalDynamicIndex: The global data index corresponding to the current traverse data; pass it 143 | // to the user-defined rendering function to help users achieve rendering 144 | // actions such as adding serial numbers 145 | dataLine = m.SelectedFunc(m, obj, globalDynamicIndex) + "\n" 146 | } else { 147 | // the cursor is not displayed on the unselected line, and the selected line is aligned with the blank character 148 | cursorPrefix = common.GenSpaces(runewidth.StringWidth(m.Cursor) + 1) 149 | dataLine = m.UnSelectedFunc(m, obj, globalDynamicIndex) + "\n" 150 | } 151 | data += cursorPrefix + dataLine 152 | header = m.HeaderFunc(m, obj, globalDynamicIndex) 153 | footer = m.FooterFunc(m, obj, globalDynamicIndex) 154 | } 155 | 156 | return fmt.Sprintf("%s\n\n%s\n%s", header, data, footer) 157 | } 158 | 159 | // Update method responds to various events and modifies the data model 160 | // according to the corresponding events 161 | func (m *Model) Update(msg tea.Msg) (*Model, tea.Cmd) { 162 | if !m.init { 163 | m.initData() 164 | return m, nil 165 | } 166 | 167 | switch msg := msg.(type) { 168 | case tea.KeyMsg: 169 | switch strings.ToLower(msg.String()) { 170 | case "q", "ctrl+c": 171 | m.canceled = true 172 | return m, tea.Quit 173 | case "enter": 174 | m.finished = true 175 | return m, common.Done 176 | case "down": 177 | m.moveDown() 178 | case "up": 179 | m.moveUp() 180 | case "right", "pgdown", "l", "k": 181 | m.nextPage() 182 | case "left", "pgup", "h", "j": 183 | m.prePage() 184 | case "1", "2", "3", "4", "5", "6", "7", "8", "9": 185 | m.forward(msg.String()) 186 | } 187 | } 188 | return m, nil 189 | } 190 | 191 | // moveDown executes the downward movement of the cursor, 192 | // while adjusting the internal index and refreshing the data area 193 | func (m *Model) moveDown() { 194 | // the page index has not reached the maximum value, and the page 195 | // data area does not need to be updated 196 | if m.pageIndex < m.pageMaxIndex { 197 | m.pageIndex++ 198 | // check whether the global index reaches the maximum value before sliding 199 | if m.index < m.maxIndex { 200 | m.index++ 201 | } 202 | return 203 | } 204 | 205 | // the page index reaches the maximum value, slide the page data area window, 206 | // the page index maintains the maximum value 207 | if m.pageIndex == m.pageMaxIndex { 208 | // check whether the global index reaches the maximum value before sliding 209 | if m.index < m.maxIndex { 210 | // global index increment 211 | m.index++ 212 | // window slide down one data 213 | m.pageData = m.Data[m.index+1-m.PerPage : m.index+1] 214 | return 215 | } 216 | } 217 | } 218 | 219 | // moveUp performs an upward movement of the cursor, 220 | // while adjusting the internal index and refreshing the data area 221 | func (m *Model) moveUp() { 222 | // the page index has not reached the minimum value, and the page 223 | // data area does not need to be updated 224 | if m.pageIndex > 0 { 225 | m.pageIndex-- 226 | // check whether the global index reaches the minimum before sliding 227 | if m.index > 0 { 228 | m.index-- 229 | } 230 | return 231 | } 232 | 233 | // the page index reaches the minimum value, slide the page data window, 234 | // and the page index maintains the minimum value 235 | if m.pageIndex == 0 { 236 | // check whether the global index reaches the minimum before sliding 237 | if m.index > 0 { 238 | // window slide up one data 239 | m.pageData = m.Data[m.index-1 : m.index-1+m.PerPage] 240 | // global index decrement 241 | m.index-- 242 | return 243 | } 244 | } 245 | } 246 | 247 | // nextPage triggers the page-down action, and does not change 248 | // the real-time page index(pageIndex) 249 | func (m *Model) nextPage() { 250 | // Get the start and end position of the page data area slice: m.Data[start:end] 251 | // 252 | // note: the slice is closed left and opened right: `[start,end)` 253 | // assuming that the global data area has unlimited length, 254 | // end should always be the actual page `length+1`, 255 | // the maximum value of end should be equal to `len(m.Data)` 256 | // under limited length 257 | pageStart, pageEnd := m.pageIndexInfo() 258 | // there are two cases when `end` does not reach the maximum value 259 | if pageEnd < len(m.Data) { 260 | // the `end` value is at least one page length away from the global maximum index 261 | if len(m.Data)-pageEnd >= m.PerPage { 262 | // slide back one page in the page data area 263 | m.pageData = m.Data[pageStart+m.PerPage : pageEnd+m.PerPage] 264 | // Global real-time index increases by one page length 265 | m.index += m.PerPage 266 | } else { // `end` is less than a page length from the global maximum index 267 | // slide the page data area directly to the end 268 | m.pageData = m.Data[len(m.Data)-m.PerPage : len(m.Data)] 269 | // `sliding distance` = `position after sliding` - `position before sliding` 270 | // the global real-time index should also synchronize the same sliding distance 271 | m.index += len(m.Data) - pageEnd 272 | } 273 | } 274 | } 275 | 276 | // prePage triggers the page-up action, and does not change 277 | // the real-time page index(pageIndex) 278 | func (m *Model) prePage() { 279 | // Get the start and end position of the page data area slice: m.Data[start:end] 280 | // 281 | // note: the slice is closed left and opened right: `[start,end)` 282 | // assuming that the global data area has unlimited length, 283 | // end should always be the actual page `length+1`, 284 | // the maximum value of end should be equal to `len(m.Data)` 285 | // under limited length 286 | pageStart, pageEnd := m.pageIndexInfo() 287 | // there are two cases when `start` does not reach the minimum value 288 | if pageStart > 0 { 289 | // `start` is at least one page length from the minimum 290 | if pageStart >= m.PerPage { 291 | // slide the page data area forward one page 292 | m.pageData = m.Data[pageStart-m.PerPage : pageEnd-m.PerPage] 293 | // Global real-time index reduces the length of one page 294 | m.index -= m.PerPage 295 | } else { // `start` to the minimum value less than one page length 296 | // slide the page data area directly to the start 297 | m.pageData = m.Data[:m.PerPage] 298 | // `sliding distance` = `position before sliding` - `minimum value(0)` 299 | // the global real-time index should also synchronize the same sliding distance 300 | m.index -= pageStart - 0 301 | } 302 | } 303 | } 304 | 305 | // forward triggers a fast jump action, if the pageIndex 306 | // is invalid, keep it as it is 307 | func (m *Model) forward(pageIndex string) { 308 | // the caller guarantees that pageIndex is an integer, and err is not processed here 309 | idx, _ := strconv.Atoi(pageIndex) 310 | idx-- 311 | 312 | // pageIndex has exceeded the maximum index of the page, ignore 313 | if idx > m.pageMaxIndex { 314 | return 315 | } 316 | 317 | // calculate the distance moved to pageIndex 318 | l := idx - m.pageIndex 319 | // update the global real time index 320 | m.index += l 321 | // update the page real time index 322 | m.pageIndex = idx 323 | 324 | } 325 | 326 | // initData initialize the data model, set the default value and 327 | // fix the wrong parameter settings during initialization 328 | func (m *Model) initData() { 329 | if m.PerPage > len(m.Data) || m.PerPage < 1 { 330 | m.PerPage = len(m.Data) 331 | m.pageData = m.Data 332 | } else { 333 | m.pageData = m.Data[:m.PerPage] 334 | } 335 | 336 | m.pageIndex = 0 337 | m.pageMaxIndex = m.PerPage - 1 338 | m.index = 0 339 | m.maxIndex = len(m.Data) - 1 340 | if m.HeaderFunc == nil { 341 | m.HeaderFunc = func(_ Model, _ interface{}, _ int) string { 342 | return common.FontColor(DefaultHeader, ColorHeader) 343 | } 344 | } 345 | if m.Cursor == "" { 346 | m.Cursor = DefaultCursor 347 | } 348 | if m.CursorColor == "" { 349 | m.CursorColor = ColorCursor 350 | } 351 | if m.SelectedFunc == nil { 352 | m.SelectedFunc = func(m Model, obj interface{}, gdIndex int) string { 353 | return common.FontColor(fmt.Sprint(obj), ColorSelected) 354 | } 355 | } 356 | if m.UnSelectedFunc == nil { 357 | m.UnSelectedFunc = func(m Model, obj interface{}, gdIndex int) string { 358 | return common.FontColor(fmt.Sprint(obj), ColorUnSelected) 359 | } 360 | } 361 | if m.FooterFunc == nil { 362 | m.FooterFunc = func(_ Model, _ interface{}, _ int) string { 363 | return common.FontColor(DefaultFooter, ColorFooter) 364 | } 365 | } 366 | if m.FinishedFunc == nil { 367 | m.FinishedFunc = func(s interface{}) string { 368 | return common.FontColor(fmt.Sprintf(DefaultFinished, s), ColorFinished) 369 | } 370 | } 371 | m.init = true 372 | } 373 | 374 | // pageIndexInfo return the start and end positions of the slice of the 375 | // page data area corresponding to the global data area 376 | func (m *Model) pageIndexInfo() (start, end int) { 377 | // `Global real-time index` - `page real-time index` = `start index of page data area` 378 | start = m.index - m.pageIndex 379 | // `Page data area start index` + `single page size` = `page data area end index` 380 | end = start + m.PerPage 381 | return 382 | } 383 | 384 | // DefaultHeaderFuncWithAppend return the default HeaderFunc and append 385 | // the given string to the next line of the default header 386 | func DefaultHeaderFuncWithAppend(append string) func(m Model, obj interface{}, gdIndex int) string { 387 | return func(m Model, obj interface{}, gdIndex int) string { 388 | return common.FontColor(DefaultHeader+"\n"+append, ColorHeader) 389 | } 390 | } 391 | 392 | // DefaultSelectedFuncWithIndex return the default SelectedFunc and adds 393 | // the serial number prefix of the given format 394 | func DefaultSelectedFuncWithIndex(indexFormat string) func(m Model, obj interface{}, gdIndex int) string { 395 | return func(m Model, obj interface{}, gdIndex int) string { 396 | return common.FontColor(fmt.Sprintf(indexFormat+" %v", gdIndex+1, obj), ColorSelected) 397 | } 398 | } 399 | 400 | // DefaultUnSelectedFuncWithIndex return the default UnSelectedFunc and 401 | // adds the serial number prefix of the given format 402 | func DefaultUnSelectedFuncWithIndex(indexFormat string) func(m Model, obj interface{}, gdIndex int) string { 403 | return func(m Model, obj interface{}, gdIndex int) string { 404 | return common.FontColor(fmt.Sprintf(indexFormat+" %v", gdIndex+1, obj), ColorUnSelected) 405 | } 406 | } 407 | 408 | // Index return the global real time index 409 | func (m Model) Index() int { 410 | return m.index 411 | } 412 | 413 | // PageIndex return the real time index of the page 414 | func (m Model) PageIndex() int { 415 | return m.pageIndex 416 | } 417 | 418 | // PageData return the current page data area slice 419 | func (m Model) PageData() []interface{} { 420 | return m.pageData 421 | } 422 | 423 | // Selected return the currently selected data 424 | func (m Model) Selected() interface{} { 425 | return m.Data[m.index] 426 | } 427 | 428 | //// PageSelected return the currently selected data(same as the Selected func) 429 | //func (m Model) PageSelected() interface{} { 430 | // return m.pageData[m.pageIndex] 431 | //} 432 | 433 | // Canceled determine whether the operation is cancelled 434 | func (m Model) Canceled() bool { 435 | return m.canceled 436 | } 437 | --------------------------------------------------------------------------------