diff --git a/shell/console/atom/buffer.go b/shell/console/atom/buffer.go index 2b60bef..8a4f803 100644 --- a/shell/console/atom/buffer.go +++ b/shell/console/atom/buffer.go @@ -305,3 +305,11 @@ func (b *Buffer) Clone() *Buffer { return &cb } + +func (b *Buffer) SetCursor(cursor int) (ok bool) { + if ok = cursor > 0 && cursor <= b.Len() && cursor != b.cursor; ok { + b.cursor = cursor + } + + return +} diff --git a/shell/console/atom/output.go b/shell/console/atom/output.go index 32b7673..ca143aa 100644 --- a/shell/console/atom/output.go +++ b/shell/console/atom/output.go @@ -45,7 +45,7 @@ func CheckUnicode(str string) (err option.Option[error]) { if i == len(runes)-1 || runes[i+1] != '[' { return option.Some(ErrInvalidUnicode) } - } else if unicode.Is(unicode.C, r) { + } else if unicode.Is(unicode.C, r) && r != '\n' { return option.Some(ErrInvalidUnicode) } } diff --git a/shell/console/atom/state.go b/shell/console/atom/state.go index 0fea747..7e2f468 100644 --- a/shell/console/atom/state.go +++ b/shell/console/atom/state.go @@ -130,6 +130,7 @@ func (st *State) Print(str string, cursor int) (err option.Option[error]) { px, py, n := CursorOffset(str, cursor+VisibleWidth(st.prompt), st.dim.Width()) RestoreCursorPosition() + ClearEndOfScreen() if n > 0 { NewLines(n) @@ -137,7 +138,6 @@ func (st *State) Print(str string, cursor int) (err option.Option[error]) { SaveCursorPosition() } - ClearEndOfScreen() fmt.Print(str) RestoreCursorPosition() @@ -295,11 +295,25 @@ func (st *State) BufferLen() int { return st.buffer.Len() } +// SavedBuffer retouren la représentation du buffer sauvegardé, si celui-ci existe. +func (st *State) SavedBuffer() (out option.Option[string]) { + if buf, ok := st.savedBuffer.Get(); ok { + out = option.Some(buf.String()) + } + + return +} + // Width retourne la largeur du terminal. func (st *State) Width() int { return st.dim.Width() } +// Height retourn la hauteur du terminal. +func (st *State) Height() int { + return st.dim.Height() +} + // ToggleReplace se place en mode remplacement si on était on mode insertion // et vice-versa. func (st *State) ToggleReplace() { @@ -355,6 +369,11 @@ func (st *State) NextWord() bool { return st.buffer.NextWord() } +// SetCursor déplace le curseur à la position donnée +func (st *State) SetCursor(cursor int) bool { + return st.buffer.SetCursor(cursor) +} + func (st *State) rem(cb func() option.Option[string]) (ok bool) { var yank string if yank, ok = cb().Get(); ok { @@ -480,6 +499,79 @@ func (st *State) HistoryNext() (ok bool) { return } +// GetProposal retourne une suggestion de saisie pour la saisie en cours +// basée sur l’historique. +func (st *State) GetProposal(insensitive ...bool) (proposal string) { + candidates := console.HistoryFilterPrefix(st.history, st.buffer.String(), insensitive...) + if l := len(candidates); l > 0 { + proposal = candidates[l-1] + } + + return +} + +// SearchMotiveNext remonte dans l’historique de recherche. +// L’historique de recherche est initialisée avec le motif du buffer. +func (st *State) SearchMotiveNext(insensitive ...bool) (ok bool) { + var hs console.History + + if hs, ok = st.historySearch.Get(); !ok { + motive := st.buffer.String() + hs = NewHistory(false) + candidates := console.HistoryFilterMotive(st.history, motive, insensitive...) + + for _, h := range candidates { + hs.Append(h) + } + + hs.SetCursor(-1) + st.historySearch = option.Some(hs) + } + + if ok = hs.Next(); ok { + st.UnfocusYank() + st.UnfocusHistory() + st.SaveBuffer() + + var h string + if h, ok = console.FocusedElement(hs).Get(); ok { + st.SetBuffer(h) + } + } + + return +} + +// SearchMotivePrev redescend dans l’historique de recherche. +// L’historique de recherche est initialisée avec le motif du buffer. +func (st *State) SearchMotivePrev(insensitive ...bool) (ok bool) { + var hs console.History + + if hs, ok = st.historySearch.Get(); !ok { + prefix := st.buffer.String() + hs = NewHistory(false) + candidates := console.HistoryFilterMotive(st.history, prefix, insensitive...) + + for _, h := range candidates { + hs.Append(h) + } + + st.historySearch = option.Some(hs) + } + if ok = hs.Prev(); ok { + st.UnfocusYank() + st.UnfocusHistory() + st.SaveBuffer() + + var h string + if h, ok = console.FocusedElement(hs).Get(); ok { + st.SetBuffer(h) + } + } + + return +} + // SearchHistoryNext remonte dans l’historique de recherche. // L’historique de recherche est initialisée avec le préfixe du buffer. func (st *State) SearchHistoryNext(insensitive ...bool) (ok bool) { @@ -553,3 +645,8 @@ func (st *State) Cancel() (ok bool) { return } + +// GetPrompt retourne le prompt configuré. +func (st *State) GetPrompt() string { + return st.prompt +} diff --git a/shell/scanner/scanner.go b/shell/scanner/scanner.go index 8d7eaad..e95f9b3 100644 --- a/shell/scanner/scanner.go +++ b/shell/scanner/scanner.go @@ -18,6 +18,7 @@ type tk struct { quote bool escape bool spaces []rune + raw bool } func runeToBytes(r rune) []byte { @@ -27,6 +28,64 @@ func runeToBytes(r rune) []byte { return b } +func (t tk) spl(q, e, s collection.Set[rune], start int, data []byte) (advance int, token []byte, esc, quote rune, done bool) { + // Scan until space, marking end of word. + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) { + return i + width, token, esc, quote, true + } + + if e.Contains(esc) { + if q.Contains(quote) && !e.Contains(r) && r != quote { + token = append(token, runeToBytes(esc)...) + } + token = append(token, runeToBytes(r)...) + esc = 0 + } else if e.Contains(r) { + esc = r + } else if q.Contains(r) { + if !q.Contains(quote) { + quote = r + } else if quote == r { + quote = 0 + } else { + token = append(token, runeToBytes(r)...) + } + } else { + token = append(token, runeToBytes(r)...) + } + } + + return +} + +func (t tk) rawSpl(q, e, s collection.Set[rune], start int, data []byte) (advance int, token []byte, esc, quote rune, done bool) { + // Scan until space, marking end of word. + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) { + return i + width, token, esc, quote, true + } + token = append(token, runeToBytes(r)...) + if e.Contains(esc) { + esc = 0 + } else if e.Contains(r) { + esc = r + } else if q.Contains(quote) { + if quote == r { + quote = 0 + } + } else if q.Contains(r) { + quote = r + } + } + + return +} + func (t tk) Split(data []byte, atEOF bool) (advance int, token []byte, err error) { q, e := collection.NewSet[rune](), collection.NewSet[rune]() s := collection.NewSet(t.spaces...) @@ -47,35 +106,19 @@ func (t tk) Split(data []byte, atEOF bool) (advance int, token []byte, err error } } - var quote, esc rune - // Scan until space, marking end of word. - for width, i := 0, start; i < len(data); i += width { - var r rune - r, width = utf8.DecodeRune(data[i:]) - if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) { - return i + width, token, nil - } - if e.Contains(esc) { - if q.Contains(quote) && !e.Contains(r) && r != quote { - token = append(token, runeToBytes(esc)...) - } - token = append(token, runeToBytes(r)...) - esc = 0 - } else if e.Contains(r) { - esc = r - } else if q.Contains(r) { - if !q.Contains(quote) { - quote = r - } else if quote == r { - quote = 0 - } else { - token = append(token, runeToBytes(r)...) - } - } else { - token = append(token, runeToBytes(r)...) - } + var ok bool + var esc, quote rune + if t.raw { + advance, token, esc, quote, ok = t.rawSpl(q, e, s, start, data) + } else { + advance, token, esc, quote, ok = t.spl(q, e, s, start, data) } + + if ok { + return + } + // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. if atEOF && len(data) > start { if e.Contains(esc) || q.Contains(quote) { @@ -97,8 +140,8 @@ func (t tk) Split(data []byte, atEOF bool) (advance int, token []byte, err error // // Le résultat va être décomposé en 3 éléments : // - unmot -// - une\ phrase -// - "une deuxième\" phrase" +// - une phrase +// - une deuxième phrase func NewTokenizer(quote, escape bool, spaces ...rune) Tokenizer { if len(spaces) == 0 { spaces = []rune(" \t\n\v\f\r") @@ -110,6 +153,30 @@ func NewTokenizer(quote, escape bool, spaces ...rune) Tokenizer { } } +// NewRawTokenizer retourne un tokenizer d’arguments de ligne de commande. +// +// Le split s’effectue au niveau des caractères spaces fourni (tous les espaces +// si aucun de fourni) sauf si ces caractères sont échappés +// (si escape) ou entre guillemets (si quote) +// Par exemple, prenons la chaîne suivante : +// unmot une\ phrase "une deuxième\" phrase" +// +// Le résultat va être décomposé en 3 éléments : +// - unmot +// - une\ phrase +// - "une deuxième\" phrase" +func NewRawTokenizer(quote, escape bool, spaces ...rune) Tokenizer { + if len(spaces) == 0 { + spaces = []rune(" \t\n\v\f\r") + } + return tk{ + quote: quote, + escape: escape, + spaces: spaces, + raw: true, + } +} + // NewScanner retourne un nouveau scanner utilisant le tokenizer spécifié // pour la fonction de splitage (NewTokenizer(true, true) si aucun tokenizer fourni). func NewScanner(r io.Reader, t ...Tokenizer) *bufio.Scanner { @@ -123,3 +190,8 @@ func NewScanner(r io.Reader, t ...Tokenizer) *bufio.Scanner { sc.Split(s) return sc } + +// NewRawScanner retourne un scanner utilisant RawTokenizer avec détection des quotes et de l’échappement. +func NewRawScanner(r io.Reader) *bufio.Scanner { + return NewScanner(r, NewRawTokenizer(true, true)) +}