From d39de4069b36af03650c645a20c8cecb7f001a05 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:50:10 +0900 Subject: [PATCH 01/16] wip --- go.mod | 19 +++++-- go.sum | 43 +++++++++++++-- internal/io/ui.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 internal/io/ui.go diff --git a/go.mod b/go.mod index f3b3e87..5ea851c 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 github.com/aws/smithy-go v1.20.3 + github.com/charmbracelet/bubbletea v0.27.0 + github.com/fatih/color v1.17.0 github.com/gosuri/uilive v0.0.4 github.com/rs/zerolog v1.30.0 github.com/urfave/cli/v2 v2.27.4 @@ -33,14 +35,25 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 3e5e654..41c75aa 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU= +github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -46,6 +56,10 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= @@ -54,17 +68,30 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4Dvx github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= @@ -76,22 +103,28 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= diff --git a/internal/io/ui.go b/internal/io/ui.go new file mode 100644 index 0000000..76c198a --- /dev/null +++ b/internal/io/ui.go @@ -0,0 +1,132 @@ +package io + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/fatih/color" +) + +type UI struct { + Choices []string + Header string + Cursor int + Selected map[int]struct{} + Keyword string + isEntered bool +} + +var _ tea.Model = (*UI)(nil) + +func NewUI(choices []string, header string) *UI { + return &UI{ + Choices: choices, + Header: header, + Selected: make(map[int]struct{}), + } +} + +func (u *UI) Init() tea.Cmd { + return nil +} + +func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + //nolint:gocritic + switch msg := msg.(type) { + + case tea.KeyMsg: + + switch msg.String() { + + // Quit the selection + case "enter": + u.isEntered = true + return u, tea.Quit + + // Quit the selection and clear ALL selected items + case "ctrl+c": + u.Selected = make(map[int]struct{}) + return u, tea.Quit + + case "up": + if u.Cursor >= 0 { + u.Cursor-- + } + if u.Cursor == -1 { + u.Cursor = len(u.Choices) - 1 + } + + case "down": + if u.Cursor <= len(u.Choices)-1 { + u.Cursor++ + } + if u.Cursor == len(u.Choices) { + u.Cursor = 0 + } + + // select or deselect an item + case " ": + _, ok := u.Selected[u.Cursor] + if ok { + delete(u.Selected, u.Cursor) + } else { + u.Selected[u.Cursor] = struct{}{} + } + + // select all items + case "right": + for i := range u.Choices { + u.Selected[i] = struct{}{} + } + + // clear all selected items + case "left": + u.Selected = make(map[int]struct{}) + + case "backspace": + if len(u.Keyword) > 0 { + u.Keyword = u.Keyword[:len(u.Keyword)-1] + } + + default: + u.Keyword += msg.String() + + } + } + + return u, nil +} + +func (u *UI) View() string { + s := color.CyanString("? ") + color.New(color.Bold).Sprint(u.Header) + + if u.isEntered { + return s + } + + s += u.Keyword + + s += color.CyanString(" [Use arrows to move, space to select, to all, to none, type to filter]") + s += "\n" + + for i, choice := range u.Choices { + if u.Keyword != "" && !strings.Contains(choice, u.Keyword) { + continue + } + + cursor := " " // no cursor + if u.Cursor == i { + cursor = color.CyanString(">") // cursor! + } + + checked := "[ ]" // not selected + if _, ok := u.Selected[i]; ok { + checked = color.GreenString("[x]") // selected! + } + + s += fmt.Sprintf("%s %s %s\n", cursor, checked, choice) + } + + return s +} From ef28abff49988f6c5ca67f3c437606b1745f671b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:15:52 +0900 Subject: [PATCH 02/16] chore(io): use bubbletea for UI library --- go.mod | 4 ---- go.sum | 27 --------------------------- internal/app/app.go | 32 ++++++++------------------------ internal/io/input.go | 42 ++++++++++++++++++++++++++++++------------ internal/io/ui.go | 20 ++++++++++++++------ 5 files changed, 52 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 5ea851c..e90906d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23 toolchain go1.23.0 require ( - github.com/AlecAivazis/survey/v2 v2.3.6 github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 @@ -41,12 +40,10 @@ require ( github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -54,6 +51,5 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 41c75aa..ba3033c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -51,9 +47,6 @@ github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -63,15 +56,9 @@ github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLg github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -80,8 +67,6 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -97,8 +82,6 @@ github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= @@ -115,24 +98,14 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/app.go b/internal/app/app.go index fe130ab..6a51559 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -167,38 +167,22 @@ func (a *App) getAction() func(c *cli.Context) error { } func (a *App) doInteractiveMode(ctx context.Context, s3Wrapper *wrapper.S3Wrapper) ([]string, bool, error) { - var checkboxes []string - var keyword string - BucketNameLabel := "Filter a keyword of bucket names: " - keyword = io.InputKeywordForFilter(BucketNameLabel) + keyword := io.InputKeywordForFilter(BucketNameLabel) - label := "Select buckets." + "\n" + label := []string{"Select buckets."} bucketNames, err := s3Wrapper.ListBucketNamesFilteredByKeyword(ctx, aws.String(keyword)) if err != nil { - return checkboxes, false, err + return nil, false, err } if len(bucketNames) == 0 { errMsg := fmt.Sprintf("No buckets matching the keyword %s.", keyword) - return checkboxes, false, fmt.Errorf("NotExistsError: %v", errMsg) + return nil, false, fmt.Errorf("NotExistsError: %v", errMsg) } - for { - checkboxes = io.GetCheckboxes(label, bucketNames) - - if len(checkboxes) == 0 { - // The case for interruption(Ctrl + C) - ok := io.GetYesNo("Do you want to finish?") - if ok { - io.Logger.Info().Msg("Finished...") - return checkboxes, false, nil - } - continue - } - - ok := io.GetYesNo("OK?") - if ok { - return checkboxes, true, nil - } + checkboxes, continuation, err := io.GetCheckboxes(label, bucketNames) + if err != nil { + return nil, false, err } + return checkboxes, continuation, nil } diff --git a/internal/io/input.go b/internal/io/input.go index 5179dbf..6b03b8c 100644 --- a/internal/io/input.go +++ b/internal/io/input.go @@ -6,23 +6,41 @@ import ( "os" "strings" - "github.com/AlecAivazis/survey/v2" + tea "github.com/charmbracelet/bubbletea" + "github.com/fatih/color" ) -const SelectionPageSize = 20 +func GetCheckboxes(headers []string, opts []string) ([]string, bool, error) { + for { + ui := NewUI(opts, headers) + p := tea.NewProgram(ui) + if _, err := p.Run(); err != nil { + return nil, false, err + } -func GetCheckboxes(label string, opts []string) []string { - res := []string{} + checkboxes := []string{} + for c := range ui.Choices { + if _, ok := ui.Selected[c]; ok { + checkboxes = append(checkboxes, ui.Choices[c]) + } + } - prompt := &survey.MultiSelect{ - Message: label, - Options: opts, - PageSize: SelectionPageSize, - } - //nolint:errcheck - survey.AskOne(prompt, &res, survey.WithKeepFilter(true)) + if len(checkboxes) == 0 { + ok := GetYesNo("Do you want to finish?") + if ok { + Logger.Info().Msg("Finished...") + return checkboxes, false, nil + } + continue + } + + fmt.Fprintf(os.Stderr, " %s\n", color.CyanString(strings.Join(checkboxes, ", "))) - return res + ok := GetYesNo("OK?") + if ok { + return checkboxes, true, nil + } + } } func InputKeywordForFilter(label string) string { diff --git a/internal/io/ui.go b/internal/io/ui.go index 76c198a..01b6e10 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -10,7 +10,7 @@ import ( type UI struct { Choices []string - Header string + Headers []string Cursor int Selected map[int]struct{} Keyword string @@ -19,10 +19,10 @@ type UI struct { var _ tea.Model = (*UI)(nil) -func NewUI(choices []string, header string) *UI { +func NewUI(choices []string, headers []string) *UI { return &UI{ Choices: choices, - Header: header, + Headers: headers, Selected: make(map[int]struct{}), } } @@ -99,7 +99,11 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (u *UI) View() string { - s := color.CyanString("? ") + color.New(color.Bold).Sprint(u.Header) + s := color.CyanString("? ") + + for _, header := range u.Headers { + s += color.New(color.Bold).Sprintln(header) + } if u.isEntered { return s @@ -111,8 +115,12 @@ func (u *UI) View() string { s += "\n" for i, choice := range u.Choices { - if u.Keyword != "" && !strings.Contains(choice, u.Keyword) { - continue + if u.Keyword != "" { + lk := strings.ToLower(u.Keyword) + lc := strings.ToLower(choice) + if !strings.Contains(lc, lk) { + continue + } } cursor := " " // no cursor From 5c4900a6007a3400ff622a55476c72c2dba72c96 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Sun, 18 Aug 2024 23:41:09 +0900 Subject: [PATCH 03/16] bold --- internal/io/ui.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index 01b6e10..c74ad8b 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -99,10 +99,12 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (u *UI) View() string { + bold := color.New(color.Bold) + s := color.CyanString("? ") for _, header := range u.Headers { - s += color.New(color.Bold).Sprintln(header) + s += bold.Sprintln(header) } if u.isEntered { @@ -125,10 +127,10 @@ func (u *UI) View() string { cursor := " " // no cursor if u.Cursor == i { - cursor = color.CyanString(">") // cursor! + cursor = color.CyanString(bold.Sprint(">")) // cursor! } - checked := "[ ]" // not selected + checked := bold.Sprint("[ ]") // not selected if _, ok := u.Selected[i]; ok { checked = color.GreenString("[x]") // selected! } From 053d964b029dd2eacd00d39e70ce2ae816f04833 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:03:43 +0900 Subject: [PATCH 04/16] ui model --- internal/io/ui.go | 132 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index c74ad8b..c6a5033 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -13,17 +13,30 @@ type UI struct { Headers []string Cursor int Selected map[int]struct{} + Filtered *Filtered Keyword string isEntered bool } +type Filtered struct { + Choices map[int]struct{} + Prev *Filtered + Cursor int +} + var _ tea.Model = (*UI)(nil) func NewUI(choices []string, headers []string) *UI { + filtered := make(map[int]struct{}) + for i := range choices { + filtered[i] = struct{}{} + } + return &UI{ Choices: choices, Headers: headers, Selected: make(map[int]struct{}), + Filtered: &Filtered{Choices: filtered}, } } @@ -50,19 +63,63 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return u, tea.Quit case "up": - if u.Cursor >= 0 { - u.Cursor-- + if len(u.Filtered.Choices) < 2 { + return u, nil } - if u.Cursor == -1 { - u.Cursor = len(u.Choices) - 1 + for range u.Choices { + if u.Cursor == 0 { + u.Cursor = len(u.Choices) - 1 + } else if u.Cursor > 0 { + u.Cursor-- + } + + if _, ok := u.Filtered.Choices[u.Cursor]; ok { + if u.Filtered.Cursor == 0 { + u.Filtered.Cursor = len(u.Filtered.Choices) - 1 + } else if u.Filtered.Cursor > 0 { + u.Filtered.Cursor-- + } + + f := u.Filtered + for { + if f.Prev == nil { + break + } + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } + break + } } case "down": - if u.Cursor <= len(u.Choices)-1 { - u.Cursor++ + if len(u.Filtered.Choices) < 2 { + return u, nil } - if u.Cursor == len(u.Choices) { - u.Cursor = 0 + for range u.Choices { + if u.Cursor < len(u.Choices)-1 { + u.Cursor++ + } else if u.Cursor == len(u.Choices)-1 { + u.Cursor = 0 + } + + if _, ok := u.Filtered.Choices[u.Cursor]; ok { + if u.Filtered.Cursor < len(u.Filtered.Choices)-1 { + u.Filtered.Cursor++ + } else if u.Filtered.Cursor == len(u.Filtered.Choices)-1 { + u.Filtered.Cursor = 0 + } + + f := u.Filtered + for { + if f.Prev == nil { + break + } + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } + break + } } // select or deselect an item @@ -84,13 +141,64 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "left": u.Selected = make(map[int]struct{}) + // clear one character from the keyword case "backspace": if len(u.Keyword) > 0 { u.Keyword = u.Keyword[:len(u.Keyword)-1] + u.Filtered = u.Filtered.Prev + cnt := 0 + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + if cnt == u.Filtered.Cursor { + u.Cursor = i + break + } + cnt++ + } } + // add a character to the keyword default: u.Keyword += msg.String() + u.Filtered = &Filtered{ + Choices: make(map[int]struct{}), + Prev: u.Filtered, + } + + tmpCursor := u.Cursor + for i, choice := range u.Choices { + lk := strings.ToLower(u.Keyword) + lc := strings.ToLower(choice) + contains := strings.Contains(lc, lk) + + fLen := len(u.Filtered.Choices) + if contains && fLen != 0 && fLen <= u.Filtered.Prev.Cursor { + u.Filtered.Cursor++ + u.Cursor = i + } + + if contains { + u.Filtered.Choices[i] = struct{}{} + tmpCursor = i + } else if u.Cursor == i && u.Cursor < len(u.Choices)-1 { + u.Cursor++ + } else if u.Cursor == i { + u.Cursor = tmpCursor + } + } + + if len(u.Filtered.Choices) != 0 { + f := u.Filtered + for { + if f.Prev == nil { + break + } + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } + } } } @@ -117,12 +225,8 @@ func (u *UI) View() string { s += "\n" for i, choice := range u.Choices { - if u.Keyword != "" { - lk := strings.ToLower(u.Keyword) - lc := strings.ToLower(choice) - if !strings.Contains(lc, lk) { - continue - } + if _, ok := u.Filtered.Choices[i]; !ok { + continue } cursor := " " // no cursor From 830329db2985e609999202d8bbf7d1bdf325feac Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:53:18 +0900 Subject: [PATCH 05/16] fix right lefht --- internal/io/ui.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index c6a5033..13347d4 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -131,15 +131,29 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { u.Selected[u.Cursor] = struct{}{} } - // select all items + // select all items in filtered list case "right": for i := range u.Choices { - u.Selected[i] = struct{}{} + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + _, ok := u.Selected[i] + if !ok { + u.Selected[i] = struct{}{} + } } - // clear all selected items + // clear all selected items in filtered list case "left": - u.Selected = make(map[int]struct{}) + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + _, ok := u.Selected[i] + if ok { + delete(u.Selected, i) + } + } // clear one character from the keyword case "backspace": From 8cd1558b81b8451631a2a36d4da629f04b5c2557 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:47:52 +0900 Subject: [PATCH 06/16] when canceled --- internal/io/input.go | 8 +++++++- internal/io/ui.go | 23 ++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/internal/io/input.go b/internal/io/input.go index 6b03b8c..cf5305e 100644 --- a/internal/io/input.go +++ b/internal/io/input.go @@ -25,7 +25,13 @@ func GetCheckboxes(headers []string, opts []string) ([]string, bool, error) { } } - if len(checkboxes) == 0 { + switch { + case ui.IsCanceled: + Logger.Warn().Msg("Canceled!") + case len(checkboxes) == 0: + Logger.Warn().Msg("Not selected!") + } + if len(checkboxes) == 0 || ui.IsCanceled { ok := GetYesNo("Do you want to finish?") if ok { Logger.Info().Msg("Finished...") diff --git a/internal/io/ui.go b/internal/io/ui.go index 13347d4..88be56f 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -9,13 +9,14 @@ import ( ) type UI struct { - Choices []string - Headers []string - Cursor int - Selected map[int]struct{} - Filtered *Filtered - Keyword string - isEntered bool + Choices []string + Headers []string + Cursor int + Selected map[int]struct{} + Filtered *Filtered + Keyword string + IsEntered bool + IsCanceled bool } type Filtered struct { @@ -54,12 +55,12 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Quit the selection case "enter": - u.isEntered = true + u.IsEntered = true return u, tea.Quit - // Quit the selection and clear ALL selected items + // Quit the selection case "ctrl+c": - u.Selected = make(map[int]struct{}) + u.IsCanceled = true return u, tea.Quit case "up": @@ -229,7 +230,7 @@ func (u *UI) View() string { s += bold.Sprintln(header) } - if u.isEntered { + if u.IsEntered && len(u.Selected) != 0 { return s } From 27b8ddc94c22138c44f955b896d8a56dda395e43 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:52:40 +0900 Subject: [PATCH 07/16] zerolog ver up --- go.mod | 2 +- go.sum | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index e90906d..b24a399 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/charmbracelet/bubbletea v0.27.0 github.com/fatih/color v1.17.0 github.com/gosuri/uilive v0.0.4 - github.com/rs/zerolog v1.30.0 + github.com/rs/zerolog v1.33.0 github.com/urfave/cli/v2 v2.27.4 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 diff --git a/go.sum b/go.sum index ba3033c..0a4c8c2 100644 --- a/go.sum +++ b/go.sum @@ -56,11 +56,10 @@ github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLg github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -78,8 +77,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= @@ -98,11 +97,10 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= From 7d1837e35f722766f9d357c79b0baefc4598ce9b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 22:39:21 +0900 Subject: [PATCH 08/16] pagination --- internal/io/ui.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index 88be56f..13fa459 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -8,6 +8,8 @@ import ( "github.com/fatih/color" ) +const SelectionPageSize = 20 + type UI struct { Choices []string Headers []string @@ -239,6 +241,7 @@ func (u *UI) View() string { s += color.CyanString(" [Use arrows to move, space to select, to all, to none, type to filter]") s += "\n" + var contents []string for i, choice := range u.Choices { if _, ok := u.Filtered.Choices[i]; !ok { continue @@ -254,8 +257,21 @@ func (u *UI) View() string { checked = color.GreenString("[x]") // selected! } - s += fmt.Sprintf("%s %s %s\n", cursor, checked, choice) + contents = append(contents, fmt.Sprintf("%s %s %s\n", cursor, checked, choice)) + } + + if len(contents) > SelectionPageSize { + if u.Cursor < SelectionPageSize/2 { + contents = contents[:SelectionPageSize] + } else if u.Cursor > len(u.Choices)-SelectionPageSize/2 { + contents = contents[len(u.Choices)-SelectionPageSize:] + } else { + contents = contents[u.Cursor-SelectionPageSize/2 : u.Cursor+SelectionPageSize/2] + } + + contents = contents[:SelectionPageSize] } + s += strings.Join(contents, "") return s } From ecb3cd5ea4cd9b29f9ea55cf2e314332b168c1d7 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 22:42:45 +0900 Subject: [PATCH 09/16] fix lint error --- internal/io/ui.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index 13fa459..cadad96 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -261,11 +261,12 @@ func (u *UI) View() string { } if len(contents) > SelectionPageSize { - if u.Cursor < SelectionPageSize/2 { + switch { + case u.Cursor < SelectionPageSize/2: contents = contents[:SelectionPageSize] - } else if u.Cursor > len(u.Choices)-SelectionPageSize/2 { + case u.Cursor > len(u.Choices)-SelectionPageSize/2: contents = contents[len(u.Choices)-SelectionPageSize:] - } else { + default: contents = contents[u.Cursor-SelectionPageSize/2 : u.Cursor+SelectionPageSize/2] } From bc153d33eed134d02f0d0dde91fa0b0539a6e1ad Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:17:15 +0900 Subject: [PATCH 10/16] refactor --- internal/io/ui.go | 103 ++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index cadad96..028f494 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -76,23 +76,24 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { u.Cursor-- } - if _, ok := u.Filtered.Choices[u.Cursor]; ok { - if u.Filtered.Cursor == 0 { - u.Filtered.Cursor = len(u.Filtered.Choices) - 1 - } else if u.Filtered.Cursor > 0 { - u.Filtered.Cursor-- - } + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + continue + } + if u.Filtered.Cursor == 0 { + u.Filtered.Cursor = len(u.Filtered.Choices) - 1 + } else if u.Filtered.Cursor > 0 { + u.Filtered.Cursor-- + } - f := u.Filtered - for { - if f.Prev == nil { - break - } - f.Prev.Cursor = u.Filtered.Cursor - f = f.Prev + f := u.Filtered + for { + if f.Prev == nil { + break } - break + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev } + break } case "down": @@ -106,23 +107,24 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { u.Cursor = 0 } - if _, ok := u.Filtered.Choices[u.Cursor]; ok { - if u.Filtered.Cursor < len(u.Filtered.Choices)-1 { - u.Filtered.Cursor++ - } else if u.Filtered.Cursor == len(u.Filtered.Choices)-1 { - u.Filtered.Cursor = 0 - } + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + continue + } + if u.Filtered.Cursor < len(u.Filtered.Choices)-1 { + u.Filtered.Cursor++ + } else if u.Filtered.Cursor == len(u.Filtered.Choices)-1 { + u.Filtered.Cursor = 0 + } - f := u.Filtered - for { - if f.Prev == nil { - break - } - f.Prev.Cursor = u.Filtered.Cursor - f = f.Prev + f := u.Filtered + for { + if f.Prev == nil { + break } - break + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev } + break } // select or deselect an item @@ -160,20 +162,22 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // clear one character from the keyword case "backspace": - if len(u.Keyword) > 0 { - u.Keyword = u.Keyword[:len(u.Keyword)-1] - u.Filtered = u.Filtered.Prev - cnt := 0 - for i := range u.Choices { - if _, ok := u.Filtered.Choices[i]; !ok { - continue - } - if cnt == u.Filtered.Cursor { - u.Cursor = i - break - } - cnt++ + if len(u.Keyword) == 0 { + return u, nil + } + + u.Keyword = u.Keyword[:len(u.Keyword)-1] + u.Filtered = u.Filtered.Prev + cnt := 0 + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + if cnt == u.Filtered.Cursor { + u.Cursor = i + break } + cnt++ } // add a character to the keyword @@ -206,15 +210,16 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - if len(u.Filtered.Choices) != 0 { - f := u.Filtered - for { - if f.Prev == nil { - break - } - f.Prev.Cursor = u.Filtered.Cursor - f = f.Prev + if len(u.Filtered.Choices) == 0 { + return u, nil + } + f := u.Filtered + for { + if f.Prev == nil { + break } + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev } } From 4dbf3dff6664cf456366ae2ea7c8696cf373fcfa Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 20 Aug 2024 01:07:01 +0900 Subject: [PATCH 11/16] fix panic --- internal/app/app.go | 2 ++ internal/io/ui.go | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 6a51559..e713470 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -156,6 +156,8 @@ func (a *App) getAction() func(c *cli.Context) error { } } + // FIXME: revert + os.Exit(0) for _, bucket := range a.BucketNames.Value() { if err := s3Wrapper.ClearS3Objects(c.Context, bucket, a.ForceMode, a.OldVersionsOnly, a.QuietMode); err != nil { return err diff --git a/internal/io/ui.go b/internal/io/ui.go index 028f494..c0d9bc2 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -267,15 +267,13 @@ func (u *UI) View() string { if len(contents) > SelectionPageSize { switch { - case u.Cursor < SelectionPageSize/2: + case u.Filtered.Cursor < SelectionPageSize/2: contents = contents[:SelectionPageSize] - case u.Cursor > len(u.Choices)-SelectionPageSize/2: - contents = contents[len(u.Choices)-SelectionPageSize:] + case u.Filtered.Cursor > len(contents)-SelectionPageSize/2: + contents = contents[len(contents)-SelectionPageSize:] default: - contents = contents[u.Cursor-SelectionPageSize/2 : u.Cursor+SelectionPageSize/2] + contents = contents[u.Filtered.Cursor-SelectionPageSize/2 : u.Filtered.Cursor+SelectionPageSize/2] } - - contents = contents[:SelectionPageSize] } s += strings.Join(contents, "") From 38121d8c782e85ecaf203b84747a892dd438d0ca Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Tue, 20 Aug 2024 01:07:36 +0900 Subject: [PATCH 12/16] rm comments --- internal/app/app.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index e713470..6a51559 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -156,8 +156,6 @@ func (a *App) getAction() func(c *cli.Context) error { } } - // FIXME: revert - os.Exit(0) for _, bucket := range a.BucketNames.Value() { if err := s3Wrapper.ClearS3Objects(c.Context, bucket, a.ForceMode, a.OldVersionsOnly, a.QuietMode); err != nil { return err From ec64b79d366c1a5a0200e0ad86fa96a51a92e09b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:24:16 +0900 Subject: [PATCH 13/16] paste keyword --- internal/io/ui.go | 118 ++++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index c0d9bc2..c40eb66 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -53,19 +53,19 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: - switch msg.String() { + switch msg.Type { // Quit the selection - case "enter": + case tea.KeyEnter: u.IsEntered = true return u, tea.Quit // Quit the selection - case "ctrl+c": + case tea.KeyCtrlC: u.IsCanceled = true return u, tea.Quit - case "up": + case tea.KeyUp, tea.KeyShiftTab: if len(u.Filtered.Choices) < 2 { return u, nil } @@ -96,7 +96,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } - case "down": + case tea.KeyDown, tea.KeyTab: if len(u.Filtered.Choices) < 2 { return u, nil } @@ -128,7 +128,10 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // select or deselect an item - case " ": + case tea.KeySpace: + if _, ok := u.Filtered.Choices[u.Cursor]; !ok { + return u, nil + } _, ok := u.Selected[u.Cursor] if ok { delete(u.Selected, u.Cursor) @@ -137,7 +140,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // select all items in filtered list - case "right": + case tea.KeyRight: for i := range u.Choices { if _, ok := u.Filtered.Choices[i]; !ok { continue @@ -149,7 +152,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // clear all selected items in filtered list - case "left": + case tea.KeyLeft: for i := range u.Choices { if _, ok := u.Filtered.Choices[i]; !ok { continue @@ -161,7 +164,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // clear one character from the keyword - case "backspace": + case tea.KeyBackspace: if len(u.Keyword) == 0 { return u, nil } @@ -181,45 +184,25 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // add a character to the keyword - default: - u.Keyword += msg.String() - u.Filtered = &Filtered{ - Choices: make(map[int]struct{}), - Prev: u.Filtered, - } - - tmpCursor := u.Cursor - for i, choice := range u.Choices { - lk := strings.ToLower(u.Keyword) - lc := strings.ToLower(choice) - contains := strings.Contains(lc, lk) - - fLen := len(u.Filtered.Choices) - if contains && fLen != 0 && fLen <= u.Filtered.Prev.Cursor { - u.Filtered.Cursor++ - u.Cursor = i - } - - if contains { - u.Filtered.Choices[i] = struct{}{} - tmpCursor = i - } else if u.Cursor == i && u.Cursor < len(u.Choices)-1 { - u.Cursor++ - } else if u.Cursor == i { - u.Cursor = tmpCursor + case tea.KeyRunes: + str := msg.String() + if !msg.Paste { + u.addCharacter(str) + } else { + if strings.Contains(str, string('\n')) || strings.Contains(str, string('\r')) { + u.IsEntered = true + return u, tea.Quit } - } - if len(u.Filtered.Choices) == 0 { - return u, nil - } - f := u.Filtered - for { - if f.Prev == nil { - break + for i := range len(str) { + // characters by paste key are enclosed by '[' and ']' + if i == 0 || i == len(str)-1 { + continue + } + if str[i] != ' ' && str[i] != '\t' { + u.addCharacter(string(str[i])) + } } - f.Prev.Cursor = u.Filtered.Cursor - f = f.Prev } } @@ -228,6 +211,49 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return u, nil } +func (u *UI) addCharacter(c string) { + u.Keyword += c + u.Filtered = &Filtered{ + Choices: make(map[int]struct{}), + Prev: u.Filtered, + } + + tmpCursor := u.Cursor + for i, choice := range u.Choices { + lk := strings.ToLower(u.Keyword) + lc := strings.ToLower(choice) + contains := strings.Contains(lc, lk) + + fLen := len(u.Filtered.Choices) + if contains && fLen != 0 && fLen <= u.Filtered.Prev.Cursor { + u.Filtered.Cursor++ + u.Cursor = i + } + + switch { + case contains: + u.Filtered.Choices[i] = struct{}{} + tmpCursor = i + case u.Cursor == i && u.Cursor < len(u.Choices)-1: + u.Cursor++ + case u.Cursor == i: + u.Cursor = tmpCursor + } + } + + if len(u.Filtered.Choices) == 0 { + return + } + f := u.Filtered + for { + if f.Prev == nil { + break + } + f.Prev.Cursor = u.Filtered.Cursor + f = f.Prev + } +} + func (u *UI) View() string { bold := color.New(color.Bold) @@ -241,7 +267,7 @@ func (u *UI) View() string { return s } - s += u.Keyword + s += bold.Sprintln(u.Keyword) s += color.CyanString(" [Use arrows to move, space to select, to all, to none, type to filter]") s += "\n" From 030b7a0fc1266dd876da873f4dc83bb364f7bb09 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:41:08 +0900 Subject: [PATCH 14/16] rune --- internal/io/ui.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index c40eb66..14fa50d 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -169,7 +169,9 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return u, nil } - u.Keyword = u.Keyword[:len(u.Keyword)-1] + keywordRunes := []rune(u.Keyword) + keywordRunes = keywordRunes[:len(keywordRunes)-1] + u.Keyword = string(keywordRunes) u.Filtered = u.Filtered.Prev cnt := 0 for i := range u.Choices { @@ -187,20 +189,23 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyRunes: str := msg.String() if !msg.Paste { - u.addCharacter(str) + for _, r := range str { + u.addCharacter(string(r)) + } } else { if strings.Contains(str, string('\n')) || strings.Contains(str, string('\r')) { u.IsEntered = true return u, tea.Quit } - for i := range len(str) { + runes := []rune(str) + for i, r := range runes { // characters by paste key are enclosed by '[' and ']' - if i == 0 || i == len(str)-1 { + if i == 0 || i == len(runes)-1 { continue } - if str[i] != ' ' && str[i] != '\t' { - u.addCharacter(string(str[i])) + if r != ' ' && r != '\t' { + u.addCharacter(string(r)) } } } From d3767974d95dd9f29378908feea6059876c88d9b Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Thu, 22 Aug 2024 21:14:49 +0900 Subject: [PATCH 15/16] refactor for runes --- internal/io/ui.go | 59 ++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index 14fa50d..e397072 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -86,10 +86,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } f := u.Filtered - for { - if f.Prev == nil { - break - } + for f.Prev != nil { f.Prev.Cursor = u.Filtered.Cursor f = f.Prev } @@ -117,10 +114,7 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } f := u.Filtered - for { - if f.Prev == nil { - break - } + for f.Prev != nil { f.Prev.Cursor = u.Filtered.Cursor f = f.Prev } @@ -165,24 +159,12 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // clear one character from the keyword case tea.KeyBackspace: - if len(u.Keyword) == 0 { - return u, nil - } + u.backspace() - keywordRunes := []rune(u.Keyword) - keywordRunes = keywordRunes[:len(keywordRunes)-1] - u.Keyword = string(keywordRunes) - u.Filtered = u.Filtered.Prev - cnt := 0 - for i := range u.Choices { - if _, ok := u.Filtered.Choices[i]; !ok { - continue - } - if cnt == u.Filtered.Cursor { - u.Cursor = i - break - } - cnt++ + // clear the keyword + case tea.KeyCtrlW: + for u.Keyword != "" { + u.backspace() } // add a character to the keyword @@ -216,6 +198,28 @@ func (u *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return u, nil } +func (u *UI) backspace() { + if len(u.Keyword) == 0 { + return + } + + keywordRunes := []rune(u.Keyword) + keywordRunes = keywordRunes[:len(keywordRunes)-1] + u.Keyword = string(keywordRunes) + u.Filtered = u.Filtered.Prev + cnt := 0 + for i := range u.Choices { + if _, ok := u.Filtered.Choices[i]; !ok { + continue + } + if cnt == u.Filtered.Cursor { + u.Cursor = i + break + } + cnt++ + } +} + func (u *UI) addCharacter(c string) { u.Keyword += c u.Filtered = &Filtered{ @@ -250,10 +254,7 @@ func (u *UI) addCharacter(c string) { return } f := u.Filtered - for { - if f.Prev == nil { - break - } + for f.Prev != nil { f.Prev.Cursor = u.Filtered.Cursor f = f.Prev } From e94aba46791c117d2dd6227518c6fcb83e1f82a2 Mon Sep 17 00:00:00 2001 From: go-to-k <24818752+go-to-k@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:49:05 +0900 Subject: [PATCH 16/16] init --- internal/io/ui.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/io/ui.go b/internal/io/ui.go index e397072..b7810ce 100644 --- a/internal/io/ui.go +++ b/internal/io/ui.go @@ -30,20 +30,20 @@ type Filtered struct { var _ tea.Model = (*UI)(nil) func NewUI(choices []string, headers []string) *UI { - filtered := make(map[int]struct{}) - for i := range choices { - filtered[i] = struct{}{} - } - return &UI{ Choices: choices, Headers: headers, Selected: make(map[int]struct{}), - Filtered: &Filtered{Choices: filtered}, } } func (u *UI) Init() tea.Cmd { + filtered := make(map[int]struct{}) + for i := range u.Choices { + filtered[i] = struct{}{} + } + u.Filtered = &Filtered{Choices: filtered} + return nil }