diff --git a/README.md b/README.md index d4fa360..c3185ea 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,66 @@ # readline -Readline est un prompt Linux qui se veut similaire à C readline mais en ligne de commande. \ No newline at end of file +Readline est un prompt Linux qui se veut similaire à C readline mais en ligne de commande. + +## Utilisation typique + +readline peut être utilisé de la façon suivante : + +```go +package main + +import ( + "fmt" + "os" + "gitea.zaclys.net/bvaudour/readline" +) + +func main() { + prompt := "\033[1;31m> \033[m" + rl := readline.New() + defer readline.Close() + for { + input := rl.Prompt(prompt) // rl.PromptPassword(prompt) pour masquer ce qui est saisi + if result, ok := input.Ok(); ok { + //@TODO Do some stuff with input + if result == "exit" { + return + } + } else if err, ok := input.Err(); ok { + fmt.Printf("\033[1,31mErreur système : %s\033[m\n", err) + os.Exit(1) + } + } +} +``` + + + +## Raccourcis clavier disponibles + +| Raccourci clavier | Action | +| -------------------------------------- | ------------------------------------------------------------ | +| Ctrl+A, Home | Retour au début de la saisie | +| Ctrl+E, End | Retour à la fin de la saisie | +| Ctrl+B, Left | Déplacement d’un caractère vers la gauche | +| Ctrl+F, Right | Déplacement d’un caractère vers la droite | +| Alt+B, Ctrl+Left | Déplacement d’un mot vers la gauche | +| Alt+F, Ctrl+Right | Déplacement d’un mot vers la droite | +| Ctrl+U | Suppression de tous les caractères du début de la ligne au curseur (curseur non compris) | +| Ctrl+K | Suppression de tous les carctères du curseur (compris) à la fin de la ligne | +| Ctrl+D (si saisie commencée), Del | Suppression du caractère sous le curseur | +| Ctrl+D (si ligne vide) | Termine l’application (EOF) | +| Ctrl+H, Backspace | Suppression du caractère avant le curseur | +| Alt+D, Alt+Del | Suppression du prochain mot | +| Alt+Backspace | Suppression du précédent mot | +| Ctrl+C | Termine l’application (avec erreur) | +| Ctrl+P, Up (si ligne vide au départ) | Remonte à l’historique précédent | +| Ctrl+N, Down (si ligne vide au départ) | Descend dans l’historique | +| Ctrl+R, Up (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir de la fin | +| Ctrl+S, Down (si ligne non vide) | Recherche dans l’historique (par préfixe de ligne) à partir du début | +| Ctrl+Y | Copie le dernier élément supprimé | +| Alt+Y | Remonte dans l’historique des éléments supprimés et les copie (implique Ctrl+Y précédemment lancé) | +| Ctrl+G, Cancel | Annule les dernières actions temporaires (historique, recherche, copie, historique de copie) | + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b18831e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.zaclys.com/bvaudour/readline + +go 1.21.3 diff --git a/readline.go b/readline.go new file mode 100644 index 0000000..e994b65 --- /dev/null +++ b/readline.go @@ -0,0 +1,217 @@ +package readline + +import ( + "os" + + . "gitea.zaclys.com/bvaudour/gob/option" + "gitea.zaclys.com/bvaudour/gob/shell/console/atom" +) + +type Readline struct { + st *atom.State +} + +func New() *Readline { + return &Readline{ + st: atom.NewState(), + } +} + +func (rl *Readline) execChar(r atom.Char, isPassword bool) (fix, stop bool) { + if stop = r == '\n' || r == '\r'; stop { + fix = true + return + } + + if atom.IsPrintable(r) { + rl.st.Insert(r) + fix = true + return + } + + var noBeep bool + switch r { + case atom.Bs, atom.C_H: // Back + noBeep, fix = rl.st.Back(), true + case atom.C_W: // Back word + noBeep, fix = rl.st.BackWord(), true + case atom.C_D: // Del or reset line + l := rl.st.BufferLen() + if l == 0 { + rl.st.Return() + os.Exit(0) + } else { + noBeep, fix = rl.st.Del(), true + rl.st.Restart() + } + case atom.C_U: // Remove BOL + noBeep, fix = rl.st.RemoveBegin(), true + case atom.C_K: // Remove EOL + noBeep, fix = rl.st.RemoveEnd(), true + case atom.C_A: // Move to BOL + noBeep, fix = rl.st.Begin(), true + case atom.C_E: // Move to EOL + noBeep, fix = rl.st.End(), true + case atom.C_B: // Move left + noBeep, fix = rl.st.Left(), true + case atom.C_F: // Move right + noBeep, fix = rl.st.Right(), true + case atom.C_T: // Transpose + noBeep, fix = rl.st.Transpose(), true + case atom.C_P: // Previous history + if !isPassword { + noBeep = rl.st.HistoryPrev() + } + case atom.C_N: // Next history + if !isPassword { + noBeep = rl.st.HistoryNext() + } + case atom.C_S: // Search history + if !isPassword { + noBeep = rl.st.SearchHistoryNext() + } + case atom.C_R: // Reverse search history + if !isPassword { + noBeep = rl.st.SearchHistoryPrev() + } + case atom.C_G, atom.Esc: // Cancel history + noBeep, fix = rl.st.Cancel(), true + case atom.C_Y: // Yank + noBeep = rl.st.Yank() + case atom.C_C: // Emergency stop + rl.st.Return() + os.Exit(1) + } + + if !noBeep { + rl.st.Beep() + } + + return +} + +func (rl *Readline) execSequence(s atom.Sequence, isPassword bool) (fix, stop bool) { + var noBeep bool + switch s { + case atom.Up: // Previous history + if !isPassword { + l := rl.st.BufferLen() + if l == 0 || rl.st.IsHistoryMode() { + noBeep = rl.st.HistoryPrev() + } else { + noBeep = rl.st.SearchHistoryPrev() + } + } + case atom.Down: // Next history + if !isPassword { + l := rl.st.BufferLen() + if l == 0 || rl.st.IsHistoryMode() { + noBeep = rl.st.HistoryNext() + } else { + noBeep = rl.st.SearchHistoryNext() + } + } + case atom.Right: // Move right + noBeep, fix = rl.st.Right(), true + case atom.Left: // Move left + noBeep, fix = rl.st.Left(), true + case atom.Ins: // Toggle insert + rl.st.ToggleReplace() + noBeep = true + case atom.Del: // Delete under cursor + noBeep, fix = rl.st.Del(), true + case atom.Home: // Move BOL + noBeep, fix = rl.st.Begin(), true + case atom.End: // Move EOL + noBeep, fix = rl.st.End(), true + case atom.A_Bs: // Delete begin of word + noBeep, fix = rl.st.BackWord(), true + case atom.A_D, atom.A_Del: // Delete end of word + noBeep, fix = rl.st.DelWord(), true + case atom.C_Right, atom.A_F: // Next word + noBeep, fix = rl.st.NextWord(), true + case atom.C_Left, atom.A_B: // Begin of word + noBeep, fix = rl.st.PrevWord(), true + case atom.A_Y: // Previous yank + noBeep = rl.st.YankPrev() + } + + if !noBeep { + rl.st.Beep() + } + + return +} + +func (rl *Readline) prompt(p string, isPassword bool) (result Result[string]) { + if err, ok := rl.st.SetPrompt(p).Get(); ok { + return Err[string](err) + } + + rl.st.Start() + defer rl.st.Stop() + + var refresh func() Option[error] + if isPassword { + refresh = func() Option[error] { return rl.st.Print("", 0) } + } else { + refresh = func() Option[error] { + str, cursor := rl.st.Buffer() + return rl.st.Print(str, cursor) + } + } + refresh() + + var fix, stop, isErr bool + for { + n := rl.st.Next() + if err, ok := n.Err(); ok { + result, isErr = Err[string](err), true + break + } + + key, _ := n.Ok() + if s, ok := key.Sequence(); ok { + fix, stop = rl.execSequence(s, isPassword) + } else if c, ok := key.Char(); ok { + fix, stop = rl.execChar(c, isPassword) + } + + err := refresh() + if e, ok := err.Get(); ok { + result, isErr = Err[string](e), true + break + } + + if stop { + break + } + + if fix { + rl.st.FixState() + } + } + + if isErr { + r, _ := rl.st.Buffer() + result = Ok(r) + if !isPassword && len(r) > 0 { + rl.st.AppendHistory(r) + } + } + + rl.st.Clear() + rl.st.FixState() + rl.st.Return() + + return +} + +// Prompt retourne la chaîne saisie. +func (rl *Readline) Prompt(p string) Result[string] { return rl.prompt(p, false) } + +// PromptPassword agit comme Prompt mais n’affiche pas à l’écran ce qui est saisi. +func (rl *Readline) PromptPassword(p string) Result[string] { return rl.prompt(p, true) } + +// Close ferme proprement le Readline. +func (rl *Readline) Close() Option[error] { return rl.st.Close() }