diff --git a/go.mod b/go.mod index cd57e2c..325020f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/nobe4/gh-not go 1.22.0 require ( + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.3 github.com/cli/go-gh/v2 v2.9.0 github.com/fatih/color v1.17.0 github.com/itchyny/gojq v0.12.15 @@ -11,12 +13,18 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c // indirect + github.com/charmbracelet/x/ansi v0.1.1 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // 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/cli/safeexec v1.0.0 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect @@ -24,7 +32,10 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -32,7 +43,9 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect - golang.org/x/sys v0.19.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect diff --git a/go.sum b/go.sum index e9a43d4..b7fcf27 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,25 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= +github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY= github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= +github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc= github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= +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/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI= github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= @@ -16,6 +30,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= @@ -39,9 +55,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 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/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/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= @@ -64,11 +86,18 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +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= +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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/internal/actors/actors.go b/internal/actors/actors.go index 032df36..147bb54 100644 --- a/internal/actors/actors.go +++ b/internal/actors/actors.go @@ -25,5 +25,5 @@ func Map(client *gh.Client) ActorsMap { } type Actor interface { - Run(notifications.Notification) (notifications.Notification, error) + Run(notifications.Notification) (notifications.Notification, string, error) } diff --git a/internal/actors/debug/debug.go b/internal/actors/debug/debug.go index be0e7f2..a26a2e5 100644 --- a/internal/actors/debug/debug.go +++ b/internal/actors/debug/debug.go @@ -1,14 +1,11 @@ package debug import ( - "fmt" - "github.com/nobe4/gh-not/internal/notifications" ) type Actor struct{} -func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, error) { - fmt.Printf("DEBUG Run %s\n", n.ToString()) - return n, nil +func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { + return n, "DEBUG" + n.ToString(), nil } diff --git a/internal/actors/done/done.go b/internal/actors/done/done.go index e9dd9dd..08f3703 100644 --- a/internal/actors/done/done.go +++ b/internal/actors/done/done.go @@ -1,7 +1,6 @@ package done import ( - "fmt" "log/slog" "net/http" @@ -16,17 +15,15 @@ type Actor struct { Client *gh.Client } -func (a *Actor) Run(n notifications.Notification) (notifications.Notification, error) { +func (a *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { slog.Debug("marking notification as done", "notification", n.ToString()) emptyNotification := notifications.Notification{} err := a.Client.API.Do(http.MethodDelete, n.URL, nil, nil) if err != nil { - return emptyNotification, err + return emptyNotification, "", err } - fmt.Printf(colors.Red(fmt.Sprintf("DONE %s\n", n.ToString()))) - - return emptyNotification, nil + return emptyNotification, colors.Red("DONE ") + n.ToString(), nil } diff --git a/internal/actors/hide/hide.go b/internal/actors/hide/hide.go index eb4e1d4..53e0ef9 100644 --- a/internal/actors/hide/hide.go +++ b/internal/actors/hide/hide.go @@ -4,8 +4,8 @@ import "github.com/nobe4/gh-not/internal/notifications" type Actor struct{} -func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, error) { +func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { n.Meta.Hidden = !n.Meta.Hidden - return n, nil + return n, "", nil } diff --git a/internal/actors/pass/pass.go b/internal/actors/pass/pass.go index 3043496..97750a8 100644 --- a/internal/actors/pass/pass.go +++ b/internal/actors/pass/pass.go @@ -4,6 +4,6 @@ import "github.com/nobe4/gh-not/internal/notifications" type Actor struct{} -func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, error) { - return n, nil +func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { + return n, "", nil } diff --git a/internal/actors/print/print.go b/internal/actors/print/print.go index ea6e4cc..dc4ee53 100644 --- a/internal/actors/print/print.go +++ b/internal/actors/print/print.go @@ -1,19 +1,15 @@ package print import ( - "fmt" - "github.com/nobe4/gh-not/internal/notifications" ) type Actor struct{} -func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, error) { +func (_ *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { if n.Meta.Hidden { - return n, nil + return n, "", nil } - fmt.Println(n.ToString()) - - return n, nil + return n, n.ToString(), nil } diff --git a/internal/actors/read/read.go b/internal/actors/read/read.go index 0fc0e22..6ba9864 100644 --- a/internal/actors/read/read.go +++ b/internal/actors/read/read.go @@ -1,7 +1,6 @@ package read import ( - "fmt" "net/http" "github.com/nobe4/gh-not/internal/colors" @@ -15,19 +14,17 @@ type Actor struct { Client *gh.Client } -func (a *Actor) Run(n notifications.Notification) (notifications.Notification, error) { +func (a *Actor) Run(n notifications.Notification) (notifications.Notification, string, error) { err := a.Client.API.Do(http.MethodPatch, n.URL, nil, nil) // go-gh currently fails to handle HTTP-205 correctly, however it's possible // to catch this case. // ref: https://github.com/cli/go-gh/issues/161 if err != nil && err.Error() != "unexpected end of JSON input" { - return n, err + return n, "", err } n.Unread = false - fmt.Printf(colors.Yellow(fmt.Sprintf("READ %s\n", n.ToString()))) - - return n, nil + return n, colors.Yellow("READ") + n.ToString(), nil } diff --git a/internal/cmd/list.go b/internal/cmd/list.go index c808277..82cb7db 100644 --- a/internal/cmd/list.go +++ b/internal/cmd/list.go @@ -57,6 +57,8 @@ func runList(cmd *cobra.Command, args []string) error { out = notifications.ToString() } + out += fmt.Sprintf("Found %d notifications", len(notifications)) + fmt.Println(out) return nil diff --git a/internal/cmd/repl.go b/internal/cmd/repl.go new file mode 100644 index 0000000..3e60c49 --- /dev/null +++ b/internal/cmd/repl.go @@ -0,0 +1,347 @@ +package cmd + +import ( + "fmt" + "log/slog" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/nobe4/gh-not/internal/actors" + "github.com/nobe4/gh-not/internal/colors" + "github.com/nobe4/gh-not/internal/notifications" + "github.com/spf13/cobra" +) + +var ( + replCmd = &cobra.Command{ + Use: "repl", + Aliases: []string{"r"}, + Short: "Launch a REPL with notifications", + RunE: runRepl, + } +) + +type Mode int64 + +const ( + Normal Mode = iota + Search + Command + Result + Help + + defaultMessage = "press ? for help" + helpMessage = ` + + Move cursor + + + Toggle notification + + Exit + +? Show this help + +a Select all notifications + +/ Search mode + press to validate to cancel + +: Command mode + Type a command, press to run against + all selected notifications. + +Press any key to exit help +` +) + +type filteredList []int + +type selection struct { + id int + selected bool +} + +type model struct { + mode Mode + + cursor int + choices notifications.Notifications + visibleChoices filteredList + + actors actors.ActorsMap + + renderCache []string + selected map[int]bool + filter textinput.Model + command textinput.Model + result string +} + +func init() { + rootCmd.AddCommand(replCmd) +} + +func runRepl(cmd *cobra.Command, args []string) error { + notifications, err := client.Notifications() + if err != nil { + slog.Error("Failed to list notifications", "err", err) + return err + } + + renderCache, err := notifications.ToTable() + if err != nil { + return err + } + + model := model{ + cursor: 0, + actors: actors.Map(client), + choices: notifications, + selected: map[int]bool{}, + renderCache: strings.Split(renderCache, "\n"), + } + + model.filter = textinput.New() + model.filter.Prompt = "/" + + model.command = textinput.New() + model.command.Prompt = ":" + + suggestions := make([]string, 0, len(model.actors)) + for k := range model.actors { + suggestions = append(suggestions, k) + } + model.command.SetSuggestions(suggestions) + model.command.ShowSuggestions = true + + p := tea.NewProgram(model) + if _, err := p.Run(); err != nil { + return err + } + + return nil +} + +func (m model) Init() tea.Cmd { + return m.applyFilter() +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + + case filteredList: + m.visibleChoices = msg + + case result: + m.result = msg.ToString() + + case selection: + m.selected[msg.id] = msg.selected + + case tea.KeyMsg: + switch m.mode { + case Normal: + switch msg.String() { + case "?": + m.mode = Help + + case "/": + m.mode = Search + m.filter.Focus() + + case ":": + m.mode = Command + m.command.Focus() + + case "esc": + return m, tea.Quit + + case "up": + if m.cursor > 0 { + m.cursor-- + } + case "down": + if m.cursor < len(m.visibleChoices)-1 { + m.cursor++ + } + + case "enter", " ": + return m, m.toggleSelect() + case "a": + return m, m.selectAll() + + } + + case Search: + switch msg.String() { + case "esc": + m.mode = Normal + m.filter.SetValue("") + m.filter.Blur() + case "enter": + m.mode = Normal + m.filter.Blur() + default: + m.filter, _ = m.filter.Update(msg) + } + return m, m.applyFilter() + + case Command: + switch msg.String() { + case "esc": + m.mode = Normal + m.command.SetValue("") + m.command.Blur() + case "enter": + command := m.command.Value() + m.mode = Result + m.command.SetValue("") + m.command.Blur() + return m, m.runCommand(command) + default: + m.command, _ = m.command.Update(msg) + } + + case Result: + m.mode = Normal + + case Help: + m.mode = Normal + } + } + + return m, cmd +} + +func (m model) View() string { + if m.mode == Result { + return m.result + } + + if m.mode == Help { + return helpMessage + } + + out := "" + + for i, id := range m.visibleChoices { + cursor := " " + if m.cursor == i { + cursor = ">" + } + + checked := " " + if v, ok := m.selected[id]; ok && v { + checked = "x" + } + + out += fmt.Sprintf("%s%s%s\n", checked, cursor, m.renderCache[id]) + } + + switch m.mode { + case Normal: + out += defaultMessage + case Search: + out += m.filter.View() + case Command: + out += m.command.View() + } + + return out +} + +type result struct { + out string + err error +} + +func (r result) ToString() string { + out := "" + + if r.err != nil { + out = colors.Red(r.err.Error()) + } else { + out = r.out + } + + return out + "\npress any key to continue" +} + +func (m model) runCommand(command string) tea.Cmd { + return func() tea.Msg { + actor, ok := m.actors[command] + if !ok { + return result{ + out: "", + err: fmt.Errorf("unknown command: %s", command), + } + } + + hasSelected := false + out := "" + for i, selected := range m.selected { + if selected { + hasSelected = true + n, outn, err := actor.Run(m.choices[i]) + if err != nil { + return result{err: err} + } + + m.choices[i] = n + out += outn + "\n" + } + } + + if !hasSelected { + return result{err: fmt.Errorf("no notification selected")} + } + + return result{out: out} + } +} + +func (m model) applyFilter() tea.Cmd { + return func() tea.Msg { + m.cursor = 0 + f := m.filter.Value() + + visibleChoices := filteredList{} + + for i, line := range m.renderCache { + if f == "" || strings.Contains(line, f) { + visibleChoices = append(visibleChoices, i) + } + } + + return visibleChoices + } +} + +func (m model) toggleSelect() tea.Cmd { + return func() tea.Msg { + visibleLineId := m.visibleChoices[m.cursor] + selected, ok := m.selected[visibleLineId] + + return selection{ + id: visibleLineId, + selected: !(selected && ok), + } + } +} +func (m model) selectAll() tea.Cmd { + cmds := tea.BatchMsg{} + + for _, id := range m.visibleChoices { + cmds = append(cmds, + func() tea.Msg { + return selection{id: id, selected: true} + }, + ) + } + + return tea.Batch(cmds...) +} diff --git a/internal/config/config.go b/internal/config/config.go index f3b1240..d46add7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -57,6 +57,7 @@ func (c *Config) Apply(n notifications.Notifications, actors map[string]actors.A return nil, err } + var out string for _, id := range selectedIds { i := indexIDMap[id] notification := n[i] @@ -65,10 +66,13 @@ func (c *Config) Apply(n notifications.Notifications, actors map[string]actors.A if noop { fmt.Printf("NOOP'ing action %s on notification %s\n", rule.Action, notification.ToString()) } else { - notification, err = actor.Run(notification) + notification, out, err = actor.Run(notification) if err != nil { slog.Error("action failed", "action", rule.Action, "err", err) } + if out != "" { + fmt.Println(out) + } } } else { slog.Error("unknown action", "action", rule.Action) diff --git a/internal/notifications/notifications.go b/internal/notifications/notifications.go index 9127246..68c5a0e 100644 --- a/internal/notifications/notifications.go +++ b/internal/notifications/notifications.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "slices" + "strings" "time" "github.com/cli/go-gh/v2/pkg/tableprinter" @@ -126,9 +127,7 @@ func (n Notifications) ToTable() (string, error) { return "", err } - fmt.Fprintf(&out, "Found %d notifications", len(n)) - - return out.String(), nil + return strings.TrimRight(out.String(), "\n"), nil } func (n Notifications) IDList() []string {