From 583fb6be45b8ed175bdf8c90c31178c3b0846664 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 6 Feb 2026 13:27:07 +0100 Subject: [PATCH] make the repl experience better --- go.mod | 12 +++++++- go.sum | 22 +++++++++++++-- pkg/cli/repl.go | 75 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 9304c7d..04c7cad 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( github.com/a-h/templ v0.3.977 github.com/c-bata/go-prompt v0.2.6 github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/google/uuid v1.6.0 github.com/muesli/reflow v0.3.0 github.com/rs/cors v1.11.1 @@ -18,6 +20,7 @@ require ( ) require ( + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect git.sr.ht/~sbinet/gg v0.7.0 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect @@ -31,12 +34,15 @@ require ( github.com/charmbracelet/bubbles v0.21.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/glamour v0.10.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect github.com/charmbracelet/x/ansi v0.11.5 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/cli/browser v1.3.0 // indirect github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect @@ -62,6 +68,10 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-cobra v1.2.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/roff v0.1.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/ncruces/go-sqlite3 v0.30.5 // indirect diff --git a/go.sum b/go.sum index 9ec6f9e..dba30f8 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM= @@ -45,12 +47,16 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY= +github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ= +github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0= github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4= github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= @@ -59,8 +65,10 @@ github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSTh github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099 h1:H09krOypYvXsPG4kYl+9J3tCRhST8WI3WEpXJELw+eg= github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= @@ -69,6 +77,8 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= @@ -155,8 +165,16 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D 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/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= 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/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= diff --git a/pkg/cli/repl.go b/pkg/cli/repl.go index 5455b16..1858f38 100644 --- a/pkg/cli/repl.go +++ b/pkg/cli/repl.go @@ -11,6 +11,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg" "github.com/LazyBachelor/LazyPM/pkg/cli/commands" "github.com/c-bata/go-prompt" + "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/truncate" "golang.org/x/term" ) @@ -29,13 +30,22 @@ func RunREPL(ctx context.Context, config pkg.SurveyConfig) error { commands.SetServices(svc) - fmt.Printf("Welcome to Project Management CLI! Type 'pm help' for available commands.\n") - fmt.Printf("You can also run shell commands directly. Type 'exit' or 'quit' to leave.\n") + titleStyle := lipgloss.NewStyle().Align(lipgloss.Center).Bold(true).Border(lipgloss.RoundedBorder()).Padding(1).Foreground(lipgloss.Color("6")) + + title := `Welcome to Project Management CLI! Type 'pm help' for available commands. +You can also run shell commands directly. Type 'exit' or 'quit' to leave.` + + fmt.Print("") + fmt.Println(titleStyle.Render(title)) + + // Create persistent history + var history []string for { input := prompt.Input( "› ", completer, + prompt.OptionPrefixTextColor(prompt.Cyan), prompt.OptionMaxSuggestion(5), prompt.OptionSuggestionBGColor(prompt.DefaultColor), prompt.OptionSelectedSuggestionBGColor(prompt.DefaultColor), @@ -43,6 +53,7 @@ func RunREPL(ctx context.Context, config pkg.SurveyConfig) error { prompt.OptionSelectedDescriptionBGColor(prompt.DefaultColor), prompt.OptionPreviewSuggestionBGColor(prompt.DefaultColor), prompt.OptionScrollbarBGColor(prompt.DefaultColor), + prompt.OptionHistory(history), ) input = strings.TrimSpace(input) @@ -56,6 +67,19 @@ func RunREPL(ctx context.Context, config pkg.SurveyConfig) error { break } + if input == "help" { + fmt.Println("Type 'pm help' for available PM commands.\nYou can also run shell commands directly. Type 'exit' or 'quit' to leave.") + continue + } + + if input == "title" { + fmt.Println(titleStyle.Render(title)) + continue + } + + // Add to history + history = append(history, input) + // Check if it's a PM command (starts with "pm ") if after, ok := strings.CutPrefix(input, "pm "); ok { // Strip "pm " prefix and execute as PM command @@ -72,14 +96,26 @@ func RunREPL(ctx context.Context, config pkg.SurveyConfig) error { } } else { // Execute as shell command + // Save terminal state before shell command + state, _ := term.GetState(int(os.Stdin.Fd())) + cmd := exec.Command("sh", "-c", input) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { + // Restore terminal state even on error + if state != nil { + term.Restore(int(os.Stdin.Fd()), state) + } continue } + + // Restore terminal state for go-prompt + if state != nil { + term.Restore(int(os.Stdin.Fd()), state) + } } } @@ -95,11 +131,33 @@ func completer(d prompt.Document) []prompt.Suggest { text := d.TextBeforeCursor() words := strings.Fields(text) - // Only provide PM command completions if input starts with "pm" if len(words) == 0 { return nil } + if words[0] != "pm" { + suggestions := []prompt.Suggest{ + {Text: "pm", Description: "Project Management System"}, + {Text: "exit", Description: "Exit pm CLI"}, + {Text: "help", Description: "Show help information"}, + {Text: "title", Description: "Print the welcome title"}, + {Text: "git", Description: "Version control system"}, + } + + if len(words) == 0 { + return suggestions + } + + var filtered []prompt.Suggest + for _, s := range suggestions { + if strings.HasPrefix(s.Text, words[0]) { + filtered = append(filtered, s) + } + } + + return filtered + } + // If first word is "pm", provide PM command completions if words[0] == "pm" { // Remove "pm" from words to get the actual command @@ -120,14 +178,10 @@ func completer(d prompt.Document) []prompt.Suggest { func commandSuggestions(words []string) []prompt.Suggest { suggestions := []prompt.Suggest{ {Text: "help", Description: "Show help information"}, - {Text: "create", Description: "Create a new issue"}, + {Text: "delete", Description: "Delete an issue by ID"}, + {Text: "create", Description: "Create a new issue with title"}, {Text: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, - {Text: "ls", Description: "List all issues"}, - {Text: "search", Description: "Alias for ls"}, - {Text: "get", Description: "Alias for describe"}, - {Text: "read", Description: "Alias for describe"}, - {Text: "add", Description: "Alias for create"}, } if len(words) == 0 { @@ -174,6 +228,9 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { case "describe", "get", "read": return issueIdSuggestions(words) + + case "delete": + return issueIdSuggestions(words) } return nil