make the repl experience better

This commit is contained in:
Robin Olsen
2026-02-06 13:27:07 +01:00
parent fbcc929cb9
commit 583fb6be45
3 changed files with 97 additions and 12 deletions

12
go.mod
View File

@@ -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

22
go.sum
View File

@@ -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=

View File

@@ -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