From 75f0c1350b89eb608f92cc9148cdc0765a6e4ed8 Mon Sep 17 00:00:00 2001 From: Benjamin VAUDOUR Date: Sat, 7 Oct 2023 21:13:39 +0200 Subject: [PATCH] Module shell (3) --- option/choice.go | 46 +++ shell/console/atom/buffer.go | 307 +++++++++++++++++++ shell/console/atom/cycler.go | 98 ++++++ shell/console/atom/history.go | 117 +++++++ shell/console/atom/input.go | 338 +++++++++++++++++++++ shell/console/atom/key.go | 401 ++++++++++++++++++++++++ shell/console/atom/output.go | 195 ++++++++++++ shell/console/atom/state.go | 555 ++++++++++++++++++++++++++++++++++ shell/console/atom/termios.go | 87 ++++++ shell/console/console.go | 175 +++++++++++ shell/console/errors/error.go | 29 ++ shell/console/prompt.go | 152 ++++++++++ shell/console/read/read.go | 144 +++++++++ 13 files changed, 2644 insertions(+) create mode 100644 option/choice.go create mode 100644 shell/console/atom/buffer.go create mode 100644 shell/console/atom/cycler.go create mode 100644 shell/console/atom/history.go create mode 100644 shell/console/atom/input.go create mode 100644 shell/console/atom/key.go create mode 100644 shell/console/atom/output.go create mode 100644 shell/console/atom/state.go create mode 100644 shell/console/atom/termios.go create mode 100644 shell/console/console.go create mode 100644 shell/console/errors/error.go create mode 100644 shell/console/prompt.go create mode 100644 shell/console/read/read.go diff --git a/option/choice.go b/option/choice.go new file mode 100644 index 0000000..9aaa8cc --- /dev/null +++ b/option/choice.go @@ -0,0 +1,46 @@ +package option + +// Option permet de définir des valeurs alternative. +type Choice[T1 any, T2 any] struct { + left Option[T1] + right Option[T2] +} + +// Left retourne un choix avec la valeur à gauche. +func Left[T1 any, T2 any](v T1) (c Choice[T1, T2]) { + c.left = Some(v) + + return +} + +// Right retourne un choix avec la valeur à droite. +func Right[T1 any, T2 any](v T2) (c Choice[T1, T2]) { + c.right = Some(v) + + return +} + +// Left retourne la valeur à gauche, si elle existe. +func (c Choice[T1, T2]) Left() (T1, bool) { + return c.left.Get() +} + +// Right retourne la valeur à droite, si elle existe. +func (c Choice[T1, T2]) Right() (T2, bool) { + return c.right.Get() +} + +// IsLeft retourne vrai si la valeur à gauche est définie. +func (c Choice[T1, T2]) IsLeft() bool { + return c.left.IsDefined() +} + +// IsRight retourne vrai si la valeur à droit est définie. +func (c Choice[T1, T2]) IsRight() bool { + return c.right.IsDefined() +} + +// IsNil retourne vrai si aucune valeur n’est définie. +func (c Choice[T1, T2]) IsNil() bool { + return !c.IsLeft() && !c.IsRight() +} diff --git a/shell/console/atom/buffer.go b/shell/console/atom/buffer.go new file mode 100644 index 0000000..2b60bef --- /dev/null +++ b/shell/console/atom/buffer.go @@ -0,0 +1,307 @@ +package atom + +import ( + "gitea.zaclys.com/bvaudour/gob/option" +) + +// Buffer stocke l’ensemble des caractère saisis et la position du curseur. +type Buffer struct { + data []rune + cursor int +} + +// Len retourne le nombre de caractères du buffer. +func (b *Buffer) Len() int { + return len(b.data) +} + +// Insert ajoute les caractères donnés à la position du curseur. +func (b *Buffer) Insert(data ...rune) { + l1, l2, c := b.Len(), len(data), b.cursor + newData := make([]rune, l1+l2) + + if c > 0 { + copy(newData[:c], b.data[:c]) + } + + copy(newData[c:c+l2], data) + + if c < l1 { + copy(newData[c+l2:], b.data[c:]) + } + + b.data = newData + b.cursor += l2 +} + +// Replace remplace les caractères du buffer à partir de la position du curseur +// par les caractères donnés. +// Si le buffer n’est pas assez grand, le buffer est agrandi. +func (b *Buffer) Replace(data ...rune) { + l1, l2, c := b.Len(), len(data), b.cursor + + l := max(l1, c+l2) + newData := make([]rune, l) + + if c > 0 { + copy(newData[:c], b.data[:c]) + } + + copy(newData[c:c+l2], data) + + if c+l2 < l1 { + copy(newData[c+l2:], b.data[c+l2:]) + } + + b.data = newData + b.cursor += l2 +} + +// Transpose transpose le caractère sous le curseur avec le caractère précédent. +// Si la transposition n’a pu se faire, il retourne faux. +func (b *Buffer) Transpose() (ok bool) { + l := b.Len() + + if ok = b.cursor > 0 && l >= 2; ok { + c := b.cursor + if c == l { + c-- + } + b.data[c-1], b.data[c] = b.data[c], b.data[c-1] + } + + return +} + +// Move déplace le curseur avec l’incrément donné. +// Si l’incrément est négatif, le déplacement se fait vers la gauche. +// Sinon, il se fait vers la droite. +// Si la position du curseur sort du buffer, retourne faux. +func (b *Buffer) Move(inc int) (ok bool) { + c, l := b.cursor+inc, b.Len() + if ok = c >= 0 && c <= l; ok { + b.cursor = c + } + + return +} + +// Left déplace le curseur d’un caractère vers la gauche. +func (b *Buffer) Left() (ok bool) { + return b.Move(-1) +} + +// Right déplace le curseur d’un caractère vers la droite. +func (b *Buffer) Right() (ok bool) { + return b.Move(1) +} + +// Begin déplace le curseur au début du buffer. +func (b *Buffer) Begin() (ok bool) { + if ok = b.cursor > 0; ok { + b.cursor = 0 + } + + return +} + +// End déplace le curseur à la fin du buffer. +func (b *Buffer) End() (ok bool) { + l := b.Len() + if ok = b.cursor < l; ok { + b.cursor = l + } + + return +} + +// PrevWord déplace le curseur au début du mot, +// ou au début du précédent mot si le curseur n’est +// pas positionné sur un mot. +func (b *Buffer) PrevWord() (ok bool) { + l := b.Len() + if l == 0 || b.cursor == 0 { + return + } + + i := b.cursor - 1 + for i == l || IsSpace(b.data[i]) { + i-- + } + + for i > 0 && !IsSpace(b.data[i-1]) { + i-- + } + + return b.Move(i - b.cursor) +} + +// NextWord déplace le curseur au début du prochain mot. +func (b *Buffer) NextWord() (ok bool) { + i, l := b.cursor, b.Len() + if i == l { + return + } + + for i < l && !IsSpace(b.data[i]) { + i++ + } + + for i < l && IsSpace(b.data[i]) { + i++ + } + + return b.Move(i - b.cursor) +} + +// Backn supprime n caractères à gauche du curseur. +// Si la suppression a bien été effectuée, il retourne +// la chaîne supprimée. +func (b *Buffer) Backn(n int) (shifted option.Option[string]) { + l, c := b.Len(), b.cursor + c2 := c - n + + if n <= 0 || c2 < 0 { + return + } + + shifted = option.Some(string(b.data[c2:c])) + newData := make([]rune, l-n) + + copy(newData[:c2], b.data[:c2]) + + if c < l { + copy(newData[c2:], b.data[c:]) + } + + b.data = newData + b.cursor = c2 + + return +} + +// Deln supprime n caractères à partir du curseur. +// Si la suppression a bien été effectuée, il retourne +// la chaîne supprimée. +func (b *Buffer) Deln(n int) (shifted option.Option[string]) { + l, c := b.Len(), b.cursor + c2 := c + n + + if n <= 0 || c2 > l { + return + } + + shifted = option.Some(string(b.data[c:c2])) + newData := make([]rune, l-n) + + copy(newData[:c], b.data[:c]) + + if c2 < l { + copy(newData[c:], b.data[c2:]) + } + + b.data = newData + + return +} + +// Back supprime le caractère avant le curseur. +func (b *Buffer) Back() option.Option[string] { + return b.Backn(1) +} + +// Del supprime le caractère sous le curseur. +func (b *Buffer) Del() option.Option[string] { + return b.Deln(1) +} + +// BackWord supprime le mot avant le caractère. +func (b *Buffer) BackWord() (shifted option.Option[string]) { + l := b.Len() + if l == 0 || b.cursor == 0 { + return + } + + i := b.cursor - 1 + for i == l || IsSpace(b.data[i]) { + i-- + } + + for i > 0 && !IsSpace(b.data[i-1]) { + i-- + } + + return b.Backn(b.cursor - i) +} + +// DelWord supprime le mot après le caractère. +func (b *Buffer) DelWord() (shifted option.Option[string]) { + i, l := b.cursor, b.Len() + if i == l { + return + } + + for i < l || IsSpace(b.data[i]) { + i++ + } + + for i < l && !IsSpace(b.data[i]) { + i++ + } + + return b.Deln(i - b.cursor) +} + +// RemoveBegin supprime les caractères avant le curseur. +func (b *Buffer) RemoveBegin() (shifted option.Option[string]) { + if b.cursor > 0 { + shifted = option.Some(string(b.data[:b.cursor])) + b.data = b.data[b.cursor:] + b.cursor = 0 + } + + return +} + +// RemoveEnd supprime les caractères après le curseur, curseur compris. +func (b *Buffer) RemoveEnd() (shifted option.Option[string]) { + if b.cursor < b.Len() { + shifted = option.Some(string(b.data[b.cursor:])) + b.data = b.data[:b.cursor] + } + + return +} + +// Clear efface tout le buffer. +func (b *Buffer) Clear() (shifted option.Option[string]) { + if b.Len() > 0 { + shifted = option.Some(string(b.data)) + b.data, b.cursor = b.data[:0], 0 + } + + return +} + +// String retourne le contenu du buffer sous forme de chaîne de caractères. +func (b *Buffer) String() string { + return string(b.data) +} + +// Cursor retourne la position du curseur. +func (b *Buffer) Cursor() int { + return b.cursor +} + +// Clone effectue une copie du buffer. +func (b *Buffer) Clone() *Buffer { + l, c := len(b.data), cap(b.data) + cb := Buffer{ + data: make([]rune, l, c), + cursor: b.cursor, + } + + copy(cb.data, b.data) + + return &cb +} diff --git a/shell/console/atom/cycler.go b/shell/console/atom/cycler.go new file mode 100644 index 0000000..7b47987 --- /dev/null +++ b/shell/console/atom/cycler.go @@ -0,0 +1,98 @@ +package atom + +import ( + "gitea.zaclys.com/bvaudour/gob/option" +) + +// Cycler stocke les données possibles de l’autocomplétion +type Cycler struct { + data []string + cursor int + cycled bool +} + +func isCursorValid(c, l int) bool { + return c >= 0 && c < l +} + +// Len retourne le nombre de propositions du cycler. +func (c *Cycler) Len() int { + return len(c.data) +} + +// Cursor retourne la position de l’élément pointé. +func (c *Cycler) Cursor() int { + return c.cursor +} + +// Index retourne l’élément selon son index. +func (c *Cycler) Index(i int) (value option.Option[string]) { + l := c.Len() + if i < 0 { + i += l + } + + if isCursorValid(i, l) { + value = option.Some(c.data[i]) + } + + return +} + +// SetCursor positionne le pointeur à l’index donné. +func (c *Cycler) SetCursor(n int) (ok bool) { + l := c.Len() + if n < 0 { + n += l + } + + if ok = isCursorValid(n, l); ok { + c.cursor = n + } + + return +} + +// Append ajoute une proposition au cycler. +func (c *Cycler) Append(data string) { + c.data = append(c.data, data) +} + +// Clear efface le cycler. +func (c *Cycler) Clear() { + c.data = c.data[:0] + c.cursor = -1 +} + +// Next incrémente le pointeur et retourne vrai +// si le pointeur pointe sur un élément. +func (c *Cycler) Next() (ok bool) { + n := c.cursor + 1 + if ok = c.SetCursor(n); !ok && c.cycled { + ok = c.SetCursor(0) + } + + return +} + +// Prev décrémente le pointeur et retourne vrai +// si le pointeur pointe sur un élément. +func (c *Cycler) Prev() (ok bool) { + n := c.cursor - 1 + if ok = n >= 0 && c.SetCursor(n); !ok && c.cycled { + ok = c.SetCursor(-1) + } + + return +} + +// NewCycler crée un nouveau cycler avec les données fournies. +// Si cycled est vrai, le cycler revient au début s’il a atteint la +// fin et inversement. +func NewCycler(cycled bool, data ...string) *Cycler { + return &Cycler{ + data: data, + cycled: cycled, + cursor: -1, + } +} diff --git a/shell/console/atom/history.go b/shell/console/atom/history.go new file mode 100644 index 0000000..d91c5ca --- /dev/null +++ b/shell/console/atom/history.go @@ -0,0 +1,117 @@ +package atom + +import ( + "bufio" + "fmt" + "io" + "unicode/utf8" + + "gitea.zaclys.com/bvaudour/gob/option" +) + +// History stocke l’historique des saisies. +type History struct { + Cycler +} + +// Append ajoute une entrée dans l’historique. +func (h *History) Append(data string) { + h.Cycler.Append(data) + h.cursor = h.Len() +} + +// Clear efface l’historique. +func (h *History) Clear() { + h.Cycler.Clear() + h.cursor = 0 +} + +// SetCursor positionne le pointeur de l’historique. +func (h *History) SetCursor(n int) (ok bool) { + l := h.Len() + if ok = n >= -1 && n <= l; ok { + h.cursor = n + } + + return +} + +// Next incrémente le pointeur de l’historique +// et retourne vrai si le pointeur pointe vers un élément. +func (h *History) Next() (ok bool) { + if ok = h.SetCursor(h.cursor + 1); ok { + ok = isCursorValid(h.cursor, h.Len()) + } else if h.cycled { + ok = h.SetCursor(0) + } + + return +} + +// Prev décrémente le pointeur de l’historique +// et retourne vrai si le pointeur pointe vers un élément. +func (h *History) Prev() (ok bool) { + if ok = h.SetCursor(h.cursor - 1); ok { + ok = isCursorValid(h.cursor, h.Len()) + } else if l := h.Len(); h.cycled && l > 0 { + ok = h.SetCursor(l - 1) + } + + return +} + +// Read charge l’historique à partir d’un fichier +// et retourne le nombre d’entrées ajoutées. +func (h *History) Read(r io.Reader) option.Result[int] { + var n int + buf := bufio.NewReader(r) + + for { + line, part, err := buf.ReadLine() + + if err == nil { + if part { + err = fmt.Errorf("line %d is too long", n+1) + } else if !utf8.Valid(line) { + err = fmt.Errorf("invalid string at line %d", n+1) + } else { + h.Append(string(line)) + n++ + } + } + + if err != nil { + if err == io.EOF { + break + } + return option.Err[int](err) + } + } + + return option.Ok(n) +} + +// Write persiste l’historique dans un fichier +// et retourne le nombre d’entrées ajoutées. +func (h *History) Write(w io.Writer) option.Result[int] { + var n int + + for _, item := range h.data { + _, err := fmt.Fprintln(w, item) + if err != nil { + return option.Err[int](err) + } + n++ + } + + return option.Ok(n) +} + +// NewHistory retourne un nouvel historique. +// Si cycled est faux l’historique ne peut être parcouru en boucle. +func NewHistory(cycled bool) *History { + var h History + h.cycled = cycled + + return &h +} diff --git a/shell/console/atom/input.go b/shell/console/atom/input.go new file mode 100644 index 0000000..fbf1d1b --- /dev/null +++ b/shell/console/atom/input.go @@ -0,0 +1,338 @@ +package atom + +import ( + "bufio" + "io" + "strconv" + "time" +) + +func timeout() <-chan time.Time { + return time.After(50 * time.Millisecond) +} + +type input struct { + buf *bufio.Reader + next <-chan nchar + pending []Char +} + +func (in *input) shift() (r Char) { + if len(in.pending) > 0 { + r, in.pending = in.pending[0], in.pending[1:] + } + return +} + +func (in *input) push(r Char) Char { + in.pending = append(in.pending, r) + return r +} + +func (in *input) clear() { + in.pending = in.pending[:0] +} + +func (in *input) readRune() nchar { + if r, _, err := in.buf.ReadRune(); err != nil { + return ec(err) + } else { + return nc(r) + } + +} + +func newInput(r io.Reader) *input { + return &input{ + buf: bufio.NewReader(r), + } +} + +func (in *input) restart() { + next := make(chan nchar, 200) + + go func() { + for { + n := in.readRune() + next <- n + + needClose := !n.IsOk() + if !needClose { + r, ok := n.Ok() + needClose = ok && (r == Lf || r == Cr || r == C_C || r == C_D) + } + + if needClose { + close(next) + return + } + } + }() + + in.next = next +} + +func (in *input) isWaiting() bool { + return len(in.next) > 0 +} + +func (in *input) nextc() (n nchar) { + var ok bool + + select { + case n, ok = <-in.next: + if !ok { + return ec(ErrInternal) + } + if r, ok := n.Ok(); ok { + in.push(r) + return nc(r) + } + } + + return +} + +func (in *input) nextt() (n nchar) { + var ok bool + + select { + case n, ok = <-in.next: + if !ok { + return ec(ErrInternal) + if r, ok := n.Ok(); ok { + in.push(r) + return nc(r) + } + } + case <-timeout(): + return ec(ErrTimeout) + } + + return +} + +func (in *input) escO() (key nkey) { + key = nk(KeyNil) + n := in.nextt() + + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return nk(keyS(AS_O)) + } + return ek(err) + } + + r, ok := n.Ok() + if !ok { + return + } + + var s Sequence + if r >= '0' && r < '9' { + s = Sequence(r << 8) + n = in.nextt() + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, _ = n.Ok() + } + + s |= Sequence(r) + + return nk(keyS(s)) +} + +func (in *input) escBracket() (key nkey) { + key = nk(KeyNil) + n := in.nextt() + + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, ok := n.Ok() + if !ok { + return + } + + var s Sequence + switch r { + case 'A': + s = Up + case 'B': + s = Down + case 'C': + s = Right + case 'D': + s = Left + case 'F': + s = End + case 'H': + s = Home + case 'Z': + s = S_Tab + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return in.escNumberBracket(r) + default: + } + + return nk(keyS(s)) +} + +func (in *input) escNumberBracket(r Char) (key nkey) { + key = nk(KeyNil) + num := []Char{r} + + for { + n := in.nextt() + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, ok := n.Ok() + if !ok { + return + } + + switch r { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + num = append(num, r) + case '~': + return in.escTildeBracket(num) + case ';': + return in.escSemiColonBracket(num) + default: + return + } + } + + return +} + +func (in *input) escTildeBracket(num []Char) (key nkey) { + x, _ := strconv.ParseInt(string(num), 10, 32) + s := Sequence(x) + + return nk(keyS(s)) +} + +func (in *input) escSemiColonBracket(num []Char) (key nkey) { + key = nk(KeyNil) + x, _ := strconv.ParseInt(string(num), 10, 32) + seqn := x != 1 + + var s Sequence + if seqn { + s = Sequence(x) + } + + n := in.nextt() + + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, ok := n.Ok() + if !ok { + return + } + + s |= Sequence(r << 8) + n = in.nextt() + + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, ok = n.Ok() + if !ok { + return + } + + if r == '~' { + if !seqn { + return nk(KeyNil) + } + } else if !seqn { + s |= Sequence(r) + } else { + return + } + + return nk(keyS(s)) +} + +func (in *input) nextChar() (key nkey) { + key = nk(KeyNil) + + if len(in.pending) > 0 { + r := in.shift() + return nk(keyC(r)) + } + + n := in.nextc() + + if err, ok := n.Err(); ok { + in.shift() + return ek(err) + } + + r, ok := n.Ok() + if !ok { + return + } + + if r != Esc { + in.shift() + return nk(keyC(r)) + } + + n = in.nextt() + if err, ok := n.Err(); ok { + if err == ErrTimeout { + return + } + return ek(err) + } + + r, ok = n.Ok() + if !ok { + return + } + + var s Sequence + switch r { + case Bs: + s = A_Bs + case 'O': + return in.escO() + case '[': + return in.escBracket() + case 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z': + s = Sequence(r << 16) + default: + return + } + + in.clear() + + return nk(keyS(s)) +} diff --git a/shell/console/atom/key.go b/shell/console/atom/key.go new file mode 100644 index 0000000..b3038b4 --- /dev/null +++ b/shell/console/atom/key.go @@ -0,0 +1,401 @@ +package atom + +import ( + "errors" + + "gitea.zaclys.com/bvaudour/gob/option" +) + +// Char représente un caractère UTF-8 saisi +// via une touche ou une combinaison de touches. +type Char = rune + +// Sequence représente une séquence de caractères UTF-8 +// saisis via une touche ou une combinaison de touche. +type Sequence rune + +type nchar = option.Result[Char] +type nseq = option.Result[Sequence] +type nkey = option.Result[Key] + +func nc(c Char) option.Result[Char] { return option.Ok(c) } +func ns(c Sequence) option.Result[Sequence] { return option.Ok(c) } +func nk(c Key) option.Result[Key] { return option.Ok(c) } +func ec(err error) option.Result[Char] { return option.Err[Char](err) } +func es(err error) option.Result[Sequence] { return option.Err[Sequence](err) } +func ek(err error) option.Result[Key] { return option.Err[Key](err) } + +// Key représente un caractère ou une séquence de caractères. +type Key struct { + option.Choice[Char, Sequence] +} + +func keyC(c Char) Key { + return Key{option.Left[Char, Sequence](c)} +} + +func keyS(c Sequence) Key { + return Key{option.Right[Char, Sequence](c)} +} + +// Char retourne un caractère. +func (k Key) Char() (Char, bool) { + return k.Left() +} + +// Sequence retourne une séquence. +func (k Key) Sequence() (Sequence, bool) { + return k.Right() +} + +// IsChar retourne vrai si k représente un caractère. +func (k Key) IsChar() bool { + return k.IsLeft() +} + +// IsSequence retourne vrai si k représente une séquence. +func (k Key) IsSequence() bool { + return k.IsRight() +} + +var ( + KeyNil Key +) + +const ( + Bs = '\u007f' // \? + Sp = '\u0020' +) + +const ( + C_A rune = iota + 1 + C_B + C_C + C_D + C_E + C_F + C_G + C_H + C_I // \t + C_J // \n + C_K + C_L + C_M // \r + C_N + C_O + C_P + C_Q + C_R + C_S + C_T + C_U + C_V + C_W + C_X + C_Y + C_Z + Esc // \e +) + +const ( + Tab = C_I + Lf = C_J + Cr = C_M +) + +const ( + shift Sequence = (iota + '2') << 8 + alt + altShit + ctrl + ctrlShift + ctrlAlt + ctrlAltShift +) + +const ( + MaskKey Sequence = 127 // Permet, en appliquant &, de récupérer la séquence sans les touches modificatrices +) + +const ( + Up Sequence = iota + 'A' // ^[[A + Down // ^[[B + Right // ^[[C + Left // ^[[D +) + +const ( + Ins Sequence = iota + 2 // ^[[2~ + Del // ^[[3~ + _ + PgUp Sequence = 5 // ^[[5~ + PgDown = 6 // ^[[6~ + End Sequence = 'F' // ^[[F + Home Sequence = 'H' // ^[[H +) + +const ( + F1 Sequence = iota + 'P' // ^[OP + F2 // ^[OQ + F3 // ^[OR + F4 // ^[OS +) + +const ( + F5 Sequence = iota + 15 // ^[[15~ + _ + F6 // ^[[17~ + F7 // ^[[18~ + F8 // ^[[19~ + F9 // ^[[20~ + F10 // ^[[21~ + _ + F11 // ^[[23~ + F12 // ^[[24~ +) + +const ( + S_Up = shift | Up // ^[[1;2A + S_down = shift | Down // ^[[1;2B + S_Right = shift | Right // ^[[1;2C + S_Left = shift | Left // ^[[1;2D + S_Ins = shift | Ins // ^[[2;2~ + S_Del = shift | Del // ^[[3;2~ + S_PgUp = shift | PgUp // ^[[5;2~ + S_PgDown = shift | PgDown // ^[[6;2~ + S_End = shift | End // ^[[1;2F + S_Home = shift | Home // ^[[1;2H + S_F1 = shift | F1 // ^[O2P + S_F2 = shift | F2 // ^[O2Q + S_F3 = shift | F3 // ^[O2R + S_F4 = shift | F4 // ^[O2S + S_F5 = shift | F5 // ^[[15;2~ + S_F6 = shift | F6 // ^[[17;2~ + S_F7 = shift | F7 // ^[[18;2~ + S_F8 = shift | F8 // ^[[19;2~ + S_F9 = shift | F9 // ^[[20;2~ + S_F10 = shift | F10 // ^[[21;2~ + S_F11 = shift | F11 // ^[[23;2~ + S_F12 = shift | F12 // ^[[24;2~ + S_Tab = shift | Sequence(Tab) // ^[[Z + + A_Up = alt | Up // ^[[1;3A + A_down = alt | Down // ^[[1;3B + A_Right = alt | Right // ^[[1;3C + A_Left = alt | Left // ^[[1;3D + A_Ins = alt | Ins // ^[[2;3~ + A_Del = alt | Del // ^[[3;3~ + A_PgUp = alt | PgUp // ^[[5;3~ + A_PgDown = alt | PgDown // ^[[6;3~ + A_End = alt | End // ^[[1;3F + A_Home = alt | Home // ^[[1;3H + A_F1 = alt | F1 // ^[O3P + A_F2 = alt | F2 // ^[O3Q + A_F3 = alt | F3 // ^[O3R + A_F4 = alt | F4 // ^[O3S + A_F5 = alt | F5 // ^[[15;3~ + A_F6 = alt | F6 // ^[[17;3~ + A_F7 = alt | F7 // ^[[18;3~ + A_F8 = alt | F8 // ^[[19;3~ + A_F9 = alt | F9 // ^[[20;3~ + A_F10 = alt | F10 // ^[[21;3~ + A_F11 = alt | F11 // ^[[23;3~ + A_F12 = alt | F12 // ^[[24;3~ + A_Bs = alt | Sequence(Bs) // ^[ + + AS_Up = altShit | Up // ^[[1;4A + AS_down = altShit | Down // ^[[1;4B + AS_Right = altShit | Right // ^[[1;4C + AS_Left = altShit | Left // ^[[1;4D + AS_Ins = altShit | Ins // ^[[2;4~ + AS_Del = altShit | Del // ^[[3;4~ + AS_PgUp = altShit | PgUp // ^[[5;4~ + AS_PgDown = altShit | PgDown // ^[[6;4~ + AS_End = altShit | End // ^[[1;4F + AS_Home = altShit | Home // ^[[1;4H + AS_F1 = altShit | F1 // ^[O4P + AS_F2 = altShit | F2 // ^[O4Q + AS_F3 = altShit | F3 // ^[O4R + AS_F4 = altShit | F4 // ^[O4S + AS_F5 = altShit | F5 // ^[[15;4~ + AS_F6 = altShit | F6 // ^[[17;4~ + AS_F7 = altShit | F7 // ^[[18;4~ + AS_F8 = altShit | F8 // ^[[19;4~ + AS_F9 = altShit | F9 // ^[[20;4~ + AS_F10 = altShit | F10 // ^[[21;4~ + AS_F11 = altShit | F11 // ^[[23;4~ + AS_F12 = altShit | F12 // ^[[24;4~ + + C_Up = ctrl | Up // ^[[1;5A + C_down = ctrl | Down // ^[[1;5B + C_Right = ctrl | Right // ^[[1;5C + C_Left = ctrl | Left // ^[[1;5D + C_Ins = ctrl | Ins // ^[[2;5~ + C_Del = ctrl | Del // ^[[3;5~ + C_PgUp = ctrl | PgUp // ^[[5;5~ + C_PgDown = ctrl | PgDown // ^[[6;5~ + C_End = ctrl | End // ^[[1;5F + C_Home = ctrl | Home // ^[[1;5H + C_F1 = ctrl | F1 // ^[O5P + C_F2 = ctrl | F2 // ^[O5Q + C_F3 = ctrl | F3 // ^[O5R + C_F4 = ctrl | F4 // ^[O5S + C_F5 = ctrl | F5 // ^[[15;5~ + C_F6 = ctrl | F6 // ^[[17;5~ + C_F7 = ctrl | F7 // ^[[18;5~ + C_F8 = ctrl | F8 // ^[[19;5~ + C_F9 = ctrl | F9 // ^[[20;5~ + C_F10 = ctrl | F10 // ^[[21;5~ + C_F11 = ctrl | F11 // ^[[23;5~ + C_F12 = ctrl | F12 // ^[[34;5~ + + CS_Up = ctrlShift | Up // ^[[1;6A + CS_down = ctrlShift | Down // ^[[1;6B + CS_Right = ctrlShift | Right // ^[[1;6C + CS_Left = ctrlShift | Left // ^[[1;6D + CS_Ins = ctrlShift | Ins // ^[[2;6~ + CS_Del = ctrlShift | Del // ^[[3;6~ + CS_PgUp = ctrlShift | PgUp // ^[[5;6~ + CS_PgDown = ctrlShift | PgDown // ^[[6;6~ + CS_End = ctrlShift | End // ^[[1;6F + CS_Home = ctrlShift | Home // ^[[1;6H + CS_F1 = ctrlShift | F1 // ^[O6P + CS_F2 = ctrlShift | F2 // ^[O6Q + CS_F3 = ctrlShift | F3 // ^[O6R + CS_F4 = ctrlShift | F4 // ^[O6S + CS_F5 = ctrlShift | F5 // ^[[15;6~ + CS_F6 = ctrlShift | F6 // ^[[17;6~ + CS_F7 = ctrlShift | F7 // ^[[18;6~ + CS_F8 = ctrlShift | F8 // ^[[19;6~ + CS_F9 = ctrlShift | F9 // ^[[20;6~ + CS_F10 = ctrlShift | F10 // ^[[21;6~ + CS_F11 = ctrlShift | F11 // ^[[23;6~ + CS_F12 = ctrlShift | F12 // ^[[24;6~ + + CA_Up = ctrlAlt | Up // ^[[1;7A + CA_down = ctrlAlt | Down // ^[[1;7B + CA_Right = ctrlAlt | Right // ^[[1;7C + CA_Left = ctrlAlt | Left // ^[[1;7D + CA_Ins = ctrlAlt | Ins // ^[[2;7~ + CA_Del = ctrlAlt | Del // ^[[3;7~ + CA_PgUp = ctrlAlt | PgUp // ^[[5;7~ + CA_PgDown = ctrlAlt | PgDown // ^[[6;7~ + CA_End = ctrlAlt | End // ^[[1;7F + CA_Home = ctrlAlt | Home // ^[[1;7H + CA_F1 = ctrlAlt | F1 // ^[O7P + CA_F2 = ctrlAlt | F2 // ^[O7Q + CA_F3 = ctrlAlt | F3 // ^[O7R + CA_F4 = ctrlAlt | F4 // ^[O7S + CA_F5 = ctrlAlt | F5 // ^[[15;7~ + CA_F6 = ctrlAlt | F6 // ^[[17;7~ + CA_F7 = ctrlAlt | F7 // ^[[18;7~ + CA_F8 = ctrlAlt | F8 // ^[[19;7~ + CA_F9 = ctrlAlt | F9 // ^[[20;7~ + CA_F10 = ctrlAlt | F10 // ^[[21;7~ + CA_F11 = ctrlAlt | F11 // ^[[23;7~ + CA_F12 = ctrlAlt | F12 // ^[[24;7~ + + CAS_Up = ctrlAltShift | Up // ^[[1;8A + CAS_down = ctrlAltShift | Down // ^[[1;8B + CAS_Right = ctrlAltShift | Right // ^[[1;8C + CAS_Left = ctrlAltShift | Left // ^[[1;8D + CAS_Ins = ctrlAltShift | Ins // ^[[2;8~ + CAS_Del = ctrlAltShift | Del // ^[[3;8~ + CAS_PgUp = ctrlAltShift | PgUp // ^[[5;8~ + CAS_PgDown = ctrlAltShift | PgDown // ^[[6;8~ + CAS_End = ctrlAltShift | End // ^[[1;8F + CAS_Home = ctrlAltShift | Home // ^[[1;8H + CAS_F1 = ctrlAltShift | F1 // ^[O8P + CAS_F2 = ctrlAltShift | F2 // ^[O8Q + CAS_F3 = ctrlAltShift | F3 // ^[O8R + CAS_F4 = ctrlAltShift | F4 // ^[O8S + CAS_F5 = ctrlAltShift | F5 // ^[[15;8~ + CAS_F6 = ctrlAltShift | F6 // ^[[17;8~ + CAS_F7 = ctrlAltShift | F7 // ^[[18;8~ + CAS_F8 = ctrlAltShift | F8 // ^[[19;8~ + CAS_F9 = ctrlAltShift | F9 // ^[[20;8~ + CAS_F10 = ctrlAltShift | F10 // ^[[21;8~ + CAS_F11 = ctrlAltShift | F11 // ^[[23;8~ + CAS_F12 = ctrlAltShift | F12 // ^[[24;8~ +) + +const ( + A_A Sequence = ('a' + iota) << 16 // ^[a + A_B // ^[b + A_C // ^[c + A_D // ^[d + A_E // ^[e + A_F // ^[f + A_G // ^[g + A_H // ^[h + A_I // ^[i + A_J // ^[j + A_K // ^[k + A_L // ^[l + A_M // ^[m + A_N // ^[n + A_O // ^[o + A_P // ^[p + A_Q // ^[q + A_R // ^[r + A_S // ^[s + A_T // ^[t + A_U // ^[u + A_V // ^[v + A_W // ^[w + A_X // ^[x + A_Y // ^[y + A_Z // ^[z +) + +const ( + AS_A Sequence = ('A' + iota) << 16 // ^[A + AS_B // ^[B + AS_C // ^[C + AS_D // ^[D + AS_E // ^[E + AS_F // ^[F + AS_G // ^[G + AS_H // ^[H + AS_I // ^[I + AS_J // ^[J + AS_K // ^[K + AS_L // ^[L + AS_M // ^[M + AS_N // ^[N + AS_O // ^[O + AS_P // ^[P + AS_Q // ^[Q + AS_R // ^[R + AS_S // ^[S + AS_T // ^[T + AS_U // ^[U + AS_V // ^[V + AS_W // ^[W + AS_X // ^[X + AS_Y // ^[Y + AS_Z // ^[Z +) + +var ( + ErrInternal = errors.New("internal error") + ErrTimeout = errors.New("timeout") + + evChar = map[int64]Sequence{ + 2: Ins, + 3: Del, + 4: End, + 5: PgUp, + 6: PgDown, + 7: Home, + 8: End, + 15: F5, + 17: F6, + 18: F7, + 19: F8, + 20: F9, + 21: F10, + 23: F11, + 24: F12, + } +) diff --git a/shell/console/atom/output.go b/shell/console/atom/output.go new file mode 100644 index 0000000..32b7673 --- /dev/null +++ b/shell/console/atom/output.go @@ -0,0 +1,195 @@ +package atom + +import ( + "errors" + "fmt" + "strings" + "unicode" + "unicode/utf8" + + "gitea.zaclys.com/bvaudour/gob/option" +) + +var ( + ErrInvalidUnicode = errors.New("Not unicode") +) + +// NextUnescape enlève le caractère d’échapement en début de chaîne. +func NextUnescape(str string) (result option.Option[string]) { + if !strings.HasPrefix(str, "\033[") { + result = option.Some(str) + } else if i := strings.Index(str, "m"); i >= 0 { + result = option.Some(str[i+1:]) + } + + return +} + +// IsPrintable retourne vrai si le caractère donné est imprimable. +func IsPrintable(r Char) bool { + return r == '\n' || unicode.IsPrint(r) +} + +// IsSpace retourne vrai si le caractère donné est un caractère blanc. +func IsSpace(r Char) bool { + return unicode.IsSpace(r) +} + +// CheckUnicode vérifie si la chaîne de caractère +// ne contient que des caractères imprimables. +func CheckUnicode(str string) (err option.Option[error]) { + runes := []rune(str) + + for i, r := range runes { + if r == Esc { + if i == len(runes)-1 || runes[i+1] != '[' { + return option.Some(ErrInvalidUnicode) + } + } else if unicode.Is(unicode.C, r) { + return option.Some(ErrInvalidUnicode) + } + } + + return +} + +// VisibleWith indique le nombre de colonnes nécessaires +// pour afficher d’une chaîne passée à echo. +func VisibleWidth(str string) int { + w := 0 + + for len(str) > 0 { + cleaned := NextUnescape(str) + if s, ok := cleaned.Get(); ok { + str = s + } else { + break + } + + r, i := utf8.DecodeRuneInString(str) + if IsPrintable(r) { + w++ + } + + str = str[i:] + } + + return w +} + +// CursorOffset calcule la position relative du curseur +// par rapport au début de la chaîne, ainsi que le +// le n° de la dernière ligne (en commençant de 0). +func CursorOffset(str string, p, w int) (px, py, l int) { + if len(str) == 0 { + return + } + + var ( + n, c int + ) + + for len(str) > 0 { + cleaned := NextUnescape(str) + if s, ok := cleaned.Get(); ok { + str = s + } else { + break + } + + r, i := utf8.DecodeRuneInString(str) + str = str[i:] + + if IsPrintable(r) { + if p == n { + px, py = c, l + } + n++ + + if r == '\n' { + c, l = 0, l+1 + } else { + c++ + if c == w { + c, l = 0, l+1 + } + } + } + } + + if p >= n { + px, py = c, l + } + + return +} + +// ClearBeginOfLine efface le début de la ligne, curseur compris +func ClearBeginOfLine() { fmt.Print("\033[1K") } + +// ClearEndOfLine efface la fin de la ligne, curseur compris. +func ClearEndOfLine() { fmt.Print("\033[0K") } + +// ClearLine efface toute la ligne. +func ClearLine() { fmt.Print("\033[2K") } + +// ClearBeginOfScreen efface l’écran jusqu’au curseur, curseur compris. +func ClearBeginOfScreen() { fmt.Print("\033[1J") } + +// ClearBeginOfScreen efface l’écran à partir du curseur, curseur compris. +func ClearEndOfScreen() { fmt.Print("\033[0J") } + +// ClearAllScreen efface tout l’écran. +func ClearAllScreen() { fmt.Print("\033[2J") } + +// ClearAllScreenAndHistory efface tout l’écran, ainsi que l’historique de l’affichage. +func ClearAllScreenAndHistory() { fmt.Print("\033[3J") } + +// MoveUp déplace le curseur de c lignes vers le haut. +func MoveUp(c int) { fmt.Printf("\033[%dA", c) } + +// MoveDown déplace le curseur de c lignes vers le bas. +func MoveDown(c int) { fmt.Printf("\033[%dB", c) } + +// MoveLeft déplace le curseur de c colonnes vers la gauche. +func MoveLeft(c int) { fmt.Printf("\033[%dD", c) } + +// MoveRight déplace le curseur de c colonnes vers la droite. +func MoveRight(c int) { fmt.Printf("\033[%dC", c) } + +// MoveLineUp se déplace de c lignes vers le haut et revient en début de ligne. +func MoveLineUp(c int) { fmt.Printf("\033[%dF", c) } + +// MoveLineDown se déplace de c lignes vers le bas et revient en début de ligne. +func MoveLineDown(c int) { fmt.Printf("\033[%dE", c) } + +// MoveToColumn se déplace à la colonne c. La colonne commence à 1. +func MoveToColumn(c int) { fmt.Printf("\033[%dG", c) } + +// MoveToScreen se déplace à la ligne l et la colonne c de l’écran. +// l et c commencent à 1. +func MoveToScreen(l, c int) { fmt.Print("\033[%d;%dH", l, c) } + +// MoveTopLeft se déplace au tout début de l’écran +func MoveTopLeft() { MoveToScreen(1, 1) } + +// SaveCursorPosition enregistre la position du curseur +// pour pouvoir y revenir plus tard. +func SaveCursorPosition() { fmt.Print("\033[s") } + +// RestoreCursorPosition revient à la position précédemment enregistrée. +func RestoreCursorPosition() { fmt.Print("\033[u") } + +// Beep émet un beep. +func Beep() { fmt.Print("\a") } + +// CarriageReturn revient en début de ligne. +func CarriageReturn() { fmt.Print("\r") } + +// NewLines rajoute c lignes. +func NewLines(c int) { + for c > 0 { + fmt.Print("\n") + c-- + } +} diff --git a/shell/console/atom/state.go b/shell/console/atom/state.go new file mode 100644 index 0000000..0fea747 --- /dev/null +++ b/shell/console/atom/state.go @@ -0,0 +1,555 @@ +package atom + +import ( + "fmt" + "io" + "os" + + "gitea.zaclys.com/bvaudour/gob/option" + "gitea.zaclys.com/bvaudour/gob/shell/console" +) + +// State représente l’état d’un terminal. +type State struct { + origTerm termios + defaultTerm termios + in *input + buffer Buffer + history console.History + historySearch option.Option[console.History] + yank console.History + savedBuffer option.Option[*Buffer] + dim TerminalSize + prompt string + replace bool +} + +// NewState retourne un nouvel état de terminal. +func NewState(hist ...console.History) *State { + var h console.History + if len(hist) > 0 { + h = hist[0] + } else { + h = NewHistory(false) + } + st := State{ + in: newInput(os.Stdin), + history: h, + yank: NewHistory(false), + } + + if !terminalSupported() { + panic("Unsupported terminal") + } + + m := newTermios(stdin) + if err, ok := m.Err(); ok { + panic(fmt.Sprintf("Unsupported terminal: %s", err)) + } + if t, ok := m.Ok(); ok { + st.origTerm = *t + } + + if err, ok := newTermios(stdout).Err(); ok { + panic(fmt.Sprintf("Unsupported terminal: %s", err)) + } + + st.origTerm.Iflag &^= icrnl | inpck | istrip | ixon + st.origTerm.Cflag |= cs8 + st.origTerm.Lflag &^= echo | icanon | iexten + st.origTerm.Cc[vmin] = 1 + st.origTerm.Cc[vtime] = 0 + st.origTerm.applyMode() + + var ok bool + + ts := GetTerminalSize() + if st.dim, ok = ts.Get(); !ok { + panic("Unsupported terminal: unknown dimensions") + } + + return &st +} + +// Start lance l’initialisation du terminal. +func (st *State) Start() { + m, _ := newTermios(stdin).Ok() + st.defaultTerm = *m + st.defaultTerm.Lflag &^= isig + st.defaultTerm.applyMode() + st.Restart() +} + +// Restart redémarre la possibilité de saisir des caractères. +func (st *State) Restart() { + st.in.restart() +} + +// Stop retourne à l’état originel du terminal. +func (st *State) Stop() { + st.defaultTerm.applyMode() +} + +// Next retourne la prochaine saisie de touche ou de combinaison de touches. +func (st *State) Next() option.Result[Key] { + return st.in.nextChar() +} + +// Close retourne à l’état originel du terminal. +func (st *State) Close() option.Option[error] { + return st.origTerm.applyMode() +} + +// InputWaiting retourne vrai si une saisie est en attente de traitement. +func (st *State) InputWaiting() bool { + return st.in.isWaiting() +} + +// SetPrompt enregistre le prompt à utiiser. +// Si le prompt contient des caractères non imprimables, retourne une erreur. +func (st *State) SetPrompt(p string) (err option.Option[error]) { + if err := CheckUnicode(p); err.IsDefined() { + return err + } + + st.prompt = p + SaveCursorPosition() + + return +} + +// Print imprime le prompt suivie de la chaîne spécifié et place +// le curseur à la position indiquée (commence à 0 à partir du début de la chaîne). +func (st *State) Print(str string, cursor int) (err option.Option[error]) { + if err = CheckUnicode(str); err.IsDefined() { + return + } + + st.dim, _ = GetTerminalSize().Get() + str = st.prompt + str + px, py, n := CursorOffset(str, cursor+VisibleWidth(st.prompt), st.dim.Width()) + + RestoreCursorPosition() + + if n > 0 { + NewLines(n) + MoveLineUp(n) + SaveCursorPosition() + } + + ClearEndOfScreen() + fmt.Print(str) + RestoreCursorPosition() + + if py > 0 { + MoveDown(py) + } + + if px > 0 { + MoveRight(px) + } + + return +} + +// Return ajoute une nouvelle ligne. +func (st *State) Return() { + NewLines(1) +} + +// Beep émet un bip. +func (st *State) Beep() { + Beep() +} + +// SaveBuffer enregistre le buffer actuel. +// Si force, force l’enregistrement du buffer même s’il y a une sauvegarde existante. +func (st *State) SaveBuffer(force ...bool) (ok bool) { + f := false + if len(force) > 0 { + f = force[0] + } + + if ok = f || !st.savedBuffer.IsDefined(); ok { + st.savedBuffer = option.Some(st.buffer.Clone()) + } + + return +} + +// RestoreBuffer restaure le buffer à partir de sa sauvegarde précédente. +func (st *State) RestoreBuffer() (ok bool) { + var sb *Buffer + + if sb, ok = st.savedBuffer.Get(); ok { + b := sb.Clone() + st.buffer = *b + } + + return +} + +// RemoveSavedBuffer supprime le buffer de sauvegarde. +func (st *State) RemoveSavedBuffer() (ok bool) { + if ok = st.savedBuffer.IsDefined(); ok { + st.savedBuffer = option.None[*Buffer]() + } + + return +} + +// RemoveHistorySearch supprime la recherche d’historique. +func (st *State) RemoveHistorySearch() (ok bool) { + if ok = st.historySearch.IsDefined(); ok { + st.historySearch = option.None[console.History]() + } + + return +} + +// UnfocusHistory supprime le pointage vers un élément de l’historique. +func (st *State) UnfocusHistory() (ok bool) { + if ok = console.IsFocused(st.history); ok { + console.Unfocus(st.history) + } + + return +} + +// UnfocusYank supprime le pointage vers un élément copiable. +func (st *State) UnfocusYank() (ok bool) { + if ok = console.IsFocused(st.yank); ok { + console.Unfocus(st.yank) + } + + return +} + +// FixState s’assure que le buffer est désormais fixé. +func (st *State) FixState() (ok bool) { + ok1 := st.RemoveSavedBuffer() + ok2 := st.RemoveHistorySearch() + ok3 := st.UnfocusHistory() + ok4 := st.UnfocusYank() + + return ok1 || ok2 || ok3 || ok4 +} + +// IsHistoryMode retourne vrai si on est en mode parcours de l’historique. +func (st *State) IsHistoryMode() bool { + return console.IsFocused(st.history) +} + +// IsYankMode retourne vrai si on est en mode collage. +func (st *State) IsYankMode() bool { + return console.IsFocused(st.yank) +} + +// IsHistorySearchMode retourne vrai si on est en mode recherche dans l’historique. +func (st *State) IsHistorySearchMode() bool { + return st.historySearch.IsDefined() +} + +// IsTmpMode retourne vrai si le buffer peut être restauré à un état précédent. +func (st *State) IsTmpMode() bool { + return st.savedBuffer.IsDefined() +} + +// IsReplaceMode retourne vrai si on est en mode remplacement. +func (st *State) IsReplaceMode() (ok bool) { + return st.replace +} + +// LoadHistory charge l’historique à partir d’un fichier. +func (st *State) LoadHistory(r io.Reader) option.Result[int] { + return st.history.Read(r) +} + +// SaveHistory persiste l’historique dans un fichier. +func (st *State) SaveHistory(w io.Writer) option.Result[int] { + return st.history.Write(w) +} + +// ClearHistory efface l’historique. +func (st *State) ClearHistory() { + st.history.Clear() +} + +// AppendHistory ajoute une entrée dans l’historique. +func (st *State) AppendHistory(line string) { + st.history.Append(line) +} + +// AppendYank ajoute une entrée dans la liste des éléments copiables. +func (st *State) AppendYank(yank string) { + st.yank.Append(yank) +} + +// Buffer retourne la représentation du buffer et la position du curseur dans le buffer. +func (st *State) Buffer() (string, int) { + return st.buffer.String(), st.buffer.Cursor() +} + +// BufferLen retourne le nombre de caractères du buffer. +func (st *State) BufferLen() int { + return st.buffer.Len() +} + +// Width retourne la largeur du terminal. +func (st *State) Width() int { + return st.dim.Width() +} + +// ToggleReplace se place en mode remplacement si on était on mode insertion +// et vice-versa. +func (st *State) ToggleReplace() { + st.replace = !st.replace +} + +// Insert insert (ou remplace, suivant le mode actuel) les caractères donnés +// dans le buffer. +func (st *State) Insert(data ...Char) (ok bool) { + if ok = len(data) > 0; ok { + if st.replace { + st.buffer.Replace(data...) + } else { + st.buffer.Insert(data...) + } + } + + return +} + +// Transpose transpose le caractère sous le curseur avec le caractère précédent. +func (st *State) Transpose() bool { + return st.buffer.Transpose() +} + +// Left déplace le curseur vers la gauche. +func (st *State) Left() bool { + return st.buffer.Left() +} + +// Right déplace le curseur vers la droite. +func (st *State) Right() bool { + return st.buffer.Right() +} + +// Begin déplace le curseur au début du buffer. +func (st *State) Begin() bool { + return st.buffer.Begin() +} + +// End déplace le curseur à la fin du buffer. +func (st *State) End() bool { + return st.buffer.End() +} + +// PrevWord déplace le curseur au début du mot précédent. +func (st *State) PrevWord() bool { + return st.buffer.PrevWord() +} + +// NextWord déplace le curseur au début du mot suivant. +func (st *State) NextWord() bool { + return st.buffer.NextWord() +} + +func (st *State) rem(cb func() option.Option[string]) (ok bool) { + var yank string + if yank, ok = cb().Get(); ok { + st.AppendYank(yank) + } + + return +} + +// Back supprime le caractère situé avant le curseur. +func (st *State) Back() bool { + return st.rem(st.buffer.Back) +} + +// Del supprime le caractère sous le curseur. +func (st *State) Del() bool { + return st.rem(st.buffer.Del) +} + +// BackWord supprime le mot avant le curseur. +func (st *State) BackWord() bool { + return st.rem(st.buffer.BackWord) +} + +// DelWord supprime le mot après le curseur. +func (st *State) DelWord() bool { + return st.rem(st.buffer.DelWord) +} + +// RemoveBegin supprime les caractères avant le curseur. +func (st *State) RemoveBegin() bool { + return st.rem(st.buffer.RemoveBegin) +} + +// RemoveEnd supprime les caractères après le curseur, curseur compris. +func (st *State) RemoveEnd() bool { + return st.rem(st.buffer.RemoveEnd) +} + +// Clear efface le buffer. +func (st *State) Clear() bool { + return st.buffer.Clear().IsDefined() +} + +// SetBuffer réinitialise le buffer avec la chaîne donnée. +func (st *State) SetBuffer(value string) bool { + st.buffer.Clear() + st.buffer.Insert([]rune(value)...) + + return true +} + +// YankPrev colle dans le buffer le précédent élément copié. +func (st *State) YankPrev() (ok bool) { + if ok = st.yank.Prev() && st.RestoreBuffer(); ok { + st.UnfocusHistory() + st.RemoveHistorySearch() + + var yank string + if yank, ok = console.FocusedElement(st.yank).Get(); ok { + ok = st.Insert([]rune(yank)...) + } + } + + return +} + +// Yank colle le dernier élément copié. +func (st *State) Yank() (ok bool) { + st.yank.SetCursor(st.yank.Len()) + + if ok = st.yank.Prev(); ok { + st.UnfocusHistory() + st.RemoveHistorySearch() + st.SaveBuffer() + + var yank string + if yank, ok = console.FocusedElement(st.yank).Get(); ok { + ok = st.Insert([]rune(yank)...) + } + } + + return +} + +// HistoryPrev remonte dans l’historique. +func (st *State) HistoryPrev() (ok bool) { + if ok = st.history.Prev(); !ok { + ok = st.history.SetCursor(0) + } + + if ok { + st.UnfocusYank() + st.RemoveHistorySearch() + st.SaveBuffer() + + var h string + if h, ok = console.FocusedElement(st.history).Get(); ok { + ok = st.SetBuffer(h) + } + } + + return +} + +// HistoryNext redescend dans l’historique. +func (st *State) HistoryNext() (ok bool) { + if ok = st.history.Next(); ok { + st.UnfocusYank() + st.RemoveHistorySearch() + st.SaveBuffer() + + var h string + h, ok = console.FocusedElement(st.history).Get() + + if ok { + ok = st.SetBuffer(h) + } else { + ok = st.RestoreBuffer() + } + } + + 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) { + var hs console.History + + if hs, ok = st.historySearch.Get(); !ok { + prefix := st.buffer.String() + hs = NewHistory(false) + candidates := console.HistoryFilterPrefix(st.history, prefix, 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 +} + +// SearchHistoryPrev redescend dans l’historique de recherche. +// L’historique de recherche est initialisée avec le préfixe du buffer. +func (st *State) SearchHistoryPrev(insensitive ...bool) (ok bool) { + var hs console.History + + if hs, ok = st.historySearch.Get(); !ok { + prefix := st.buffer.String() + hs = NewHistory(false) + candidates := console.HistoryFilterPrefix(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 +} + +// Cancel annule l’historique, la recherche ou la copie en cours. +func (st *State) Cancel() (ok bool) { + var sb *Buffer + if sb, ok = st.savedBuffer.Get(); ok { + b := sb.Clone() + st.buffer = *b + st.FixState() + } + + return +} diff --git a/shell/console/atom/termios.go b/shell/console/atom/termios.go new file mode 100644 index 0000000..b3adc74 --- /dev/null +++ b/shell/console/atom/termios.go @@ -0,0 +1,87 @@ +package atom + +import ( + "os" + "strings" + "syscall" + "unsafe" + + "gitea.zaclys.com/bvaudour/gob/option" +) + +const ( + cs8 = syscall.CS8 + echo = syscall.ECHO + tcgets = syscall.TCGETS + tcsets = syscall.TCSETS + icanon = syscall.ICANON + icrnl = syscall.ICRNL + iexten = syscall.IEXTEN + inpck = syscall.INPCK + ioctl = syscall.SYS_IOCTL + isig = syscall.ISIG + istrip = syscall.ISTRIP + ixon = syscall.IXON + opost = syscall.OPOST + tiocgwinsz = syscall.TIOCGWINSZ + vmin = syscall.VMIN + vtime = syscall.VTIME +) + +var ( + stdin = syscall.Stdin + stdout = syscall.Stdout +) + +type termios struct { + syscall.Termios +} + +func (mode *termios) applyMode() (err option.Option[error]) { + _, _, errno := syscall.Syscall(ioctl, uintptr(stdin), tcsets, uintptr(unsafe.Pointer(mode))) + if errno != 0 { + return option.Some[error](errno) + } + + return +} + +func newTermios(handle int) option.Result[*termios] { + mode := new(termios) + _, _, errno := syscall.Syscall(ioctl, uintptr(handle), tcgets, uintptr(unsafe.Pointer(mode))) + + if errno != 0 { + return option.Err[*termios](errno) + } + return option.Ok(mode) +} + +func terminalSupported() bool { + bad := map[string]bool{"": true, "dumb": true, "cons25": true} + return !bad[strings.ToLower(os.Getenv("TERM"))] +} + +type TerminalSize struct { + row, col uint16 + xpixel, ypixel uint16 +} + +func (ts TerminalSize) Width() int { return int(ts.col) } +func (ts TerminalSize) Height() int { return int(ts.row) } + +// GetTerminalSize retourne les dimensions actuelles du terminal. +func GetTerminalSize() (result option.Option[TerminalSize]) { + var ts TerminalSize + sc, _, _ := syscall.Syscall(ioctl, uintptr(stdout), tiocgwinsz, uintptr(unsafe.Pointer(&ts))) + + if int(sc) >= 0 { + return option.Some(ts) + } + return +} + +func checkOutput() (useCHA bool) { + // xterm is known to support CHA + useCHA = strings.Contains(strings.ToLower(os.Getenv("TERM")), "xterm") + return +} diff --git a/shell/console/console.go b/shell/console/console.go new file mode 100644 index 0000000..fadde61 --- /dev/null +++ b/shell/console/console.go @@ -0,0 +1,175 @@ +package console + +import ( + "fmt" + "io" + "os" + "regexp" + "strings" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// Cycler est une interface permettant de cycler dans les entrées d’une liste. +type Cycler interface { + Len() int // Retourne la longueur de la liste + Cursor() int // Retourne l’index de l’entrée pointée + Index(int) Option[string] // Retourne l’entrée à l’index donné + SetCursor(int) bool // Déplace l’entrée pointée à l’index demandé et retourne vrai si cet index existe + Append(string) // Ajoute une entrée dans la liste + Clear() // Efface la liste + Next() bool // Pointe vers l’entrée suivante + Prev() bool // Pointe vers l’entrée précédente +} + +// History est une interface pour gérer l’historique des commandes saisies. +type History interface { + Cycler + Read(io.Reader) Result[int] + Write(io.Writer) Result[int] +} + +// Prompter est une interface pour représenter un prompt. +type Prompter interface { + Prompt(string) Result[string] // Affiche le prompt et retourne la saisie +} + +// Console est un prompt avec gestion de l’historique. +type Console interface { + Prompter + LoadHistory(io.Reader) Result[int] + SaveHistory(io.Writer) Result[int] + ClearHistory() + AppendHistory(string) +} + +// Error représente une erreur terminal +type Error interface { + Error() string + Code() int +} + +// Exit quitte le programme en affichant une erreur. +// Si aucun descripteur n’est fourni, le message d’erreur +// est écrit sur la sortie erreur standard. +func Exit(code int, msg string, w ...io.Writer) { + var out io.Writer + if len(w) > 0 { + out = w[0] + } else { + out = os.Stderr + } + + fmt.Fprintln(out, msg) + os.Exit(code) +} + +// ExitfOn agit comme Exit mais mais formate le message. +func ExitfOn(code int, tmpl string, w io.Writer, args ...any) { + msg := fmt.Sprintf(tmpl, args...) + Exit(code, msg, w) +} + +// Exitf agit comme ExitfOn sur la sortie erreur standard. +func Exitf(code int, tmpl string, args ...any) { + ExitfOn(code, tmpl, os.Stderr, args...) +} + +// ExitWithError quitte le programme en utilisant +// l’erreur fournie. +func ExitWithError(err Error, w ...io.Writer) { + Exit(err.Code(), err.Error(), w...) +} + +// HistoryList retourne le contenu de l’historique. +func HistoryList(h History) (out []string) { + l := h.Len() + out = make([]string, l) + for i := 0; i < l; i++ { + if line, ok := h.Index(i).Get(); ok { + out[i] = line + } + } + + return +} + +func hfilter(h History, motive string, cb func(string, string) bool, insensitive ...bool) (out []string) { + l := h.Len() + if l == 0 { + return + } + + ins := false + if len(insensitive) > 0 { + ins = insensitive[0] + } + + var cmp func(string) bool + if ins { + motive = strings.ToLower(motive) + cmp = func(e string) bool { + return cb(strings.ToLower(e), motive) + } + } else { + cmp = func(e string) bool { return cb(e, motive) } + } + + for i := 0; i < l; i++ { + if e, ok := h.Index(i).Get(); ok && cmp(e) { + out = append(out, e) + } + } + + return +} + +// HistoryFilterMotive retourne les entrées de l’historique contenant le motif donné. +// Il est possible de faire une recherche insensible à la casse. +func HistoryFilterMotive(h History, motive string, insensitive ...bool) (out []string) { + return hfilter(h, motive, strings.Contains, insensitive...) +} + +// HistoryFilterPrefix retourne les entrées de l’historique commençant le préfixe donné. +// Il est possible de faire une recherche insensible à la casse. +func HistoryFilterPrefix(h History, prefix string, insensitive ...bool) (out []string) { + return hfilter(h, prefix, strings.HasPrefix, insensitive...) +} + +// HistoryFilterSuffix retourne les entrées de l’historique terminant par le suffixe donné. +// Il est possible de faire une recherche insensible à la casse. +func HistoryFilterSuffix(h History, suffix string, insensitive ...bool) (out []string) { + return hfilter(h, suffix, strings.HasSuffix, insensitive...) +} + +// HistoryFilter retourne les entrées de l’historique vérifiant la regexp donnée. +func HistoryFilter(h History, r *regexp.Regexp) (out []string) { + l := h.Len() + if l == 0 { + return + } + + for i := 0; i < l; i++ { + if e, ok := h.Index(i).Get(); ok && r.MatchString(e) { + out = append(out, e) + } + } + + return +} + +// IsFocused retourne vrai si le cycler pointe sur un élément existant. +func IsFocused(c Cycler) bool { + l, n := c.Len(), c.Cursor() + return n >= 0 && n < l +} + +// FocusedElement retourne l’élément pointé par le cycler. +func FocusedElement(c Cycler) Option[string] { + return c.Index(c.Cursor()) +} + +// Unfocus enlève le pointage de l’historique. +func Unfocus(h History) { + h.SetCursor(h.Len()) +} diff --git a/shell/console/errors/error.go b/shell/console/errors/error.go new file mode 100644 index 0000000..e84ef6c --- /dev/null +++ b/shell/console/errors/error.go @@ -0,0 +1,29 @@ +package errors + +import ( + "fmt" +) + +type Error struct { + msg string + code int +} + +func (err *Error) Error() string { + return err.msg +} + +func (err *Error) Code() int { + return err.code +} + +func New(code int, msg string) *Error { + return &Error{ + msg: msg, + code: code, + } +} + +func Newf(code int, tmpl string, args ...any) *Error { + return New(code, fmt.Sprintf(tmpl, args...)) +} diff --git a/shell/console/prompt.go b/shell/console/prompt.go new file mode 100644 index 0000000..23ff36c --- /dev/null +++ b/shell/console/prompt.go @@ -0,0 +1,152 @@ +package console + +import ( + "fmt" + "strings" + + "gitea.zaclys.com/bvaudour/gob/convert" + . "gitea.zaclys.com/bvaudour/gob/option" + "gitea.zaclys.com/bvaudour/gob/shell/scanner" +) + +// PromptFunc est une fonction qui affiche une invite de commande et retourne la saisie brute. +type PromptFunc func(string) Result[string] + +// ParsedPromptFunc est une fonction qui affiche une invite de commande et retourne la saisie parsée. +type ParsedPromptFunc[T any] func(string) Result[T] + +// ConvFunc est une fonction qui convertit une chaîne de caractère en un type donné. +type ConvFunc[T any] func(string) Result[T] + +// PromptOf transforme un prompter en fonction promptable. +func PromptOf(p Prompter) PromptFunc { + return p.Prompt +} + +func singleConv[T any](s string) Result[T] { + var value T + if !convert.Convert(s, &value) { + return Err[T](fmt.Errorf("Failed to convert %s", s)) + } + + return Ok(value) +} + +func boolConv(s string) (out Result[bool]) { + s = strings.ToLower(s) + switch s { + case "oui", "o", "yes", "y", "1", "true", "t", "on": + out = Ok(true) + case "non", "n", "no", "0", "false", "f", "off": + out = Ok(false) + default: + out = Err[bool](fmt.Errorf("%s: not a boolean", s)) + } + + return +} + +func strConv(s string) Result[string] { + return Ok(s) +} + +// ParsedPrompt transforme un prompt en prompt de donnée parsée. +func ParsedPrompt[T any](sp PromptFunc, conv ConvFunc[T]) ParsedPromptFunc[T] { + return func(p string) (out Result[T]) { + sout := sp(p) + if v, ok := sout.Ok(); ok { + out = conv(v) + } else if err, ok := sout.Err(); ok { + out = Err[T](err) + } + + return + } +} + +// PromptInt transforme un prompt en prompt d’entier. +func PromptInt(sp PromptFunc) ParsedPromptFunc[int] { + return ParsedPrompt(sp, singleConv[int]) +} + +// PromptUint transforme un prompt en prompt d’entier non signé. +func PromptUint(sp PromptFunc) ParsedPromptFunc[uint] { + return ParsedPrompt(sp, singleConv[uint]) +} + +// PromptFloat transforme un prompt en prompt de nombre décimal. +func PromptFloat(sp PromptFunc) ParsedPromptFunc[float64] { + return ParsedPrompt(sp, singleConv[float64]) +} + +// PromptBool transforme un prompt en prompt de booléen. +// +// Valeurs autorisée : +// - true : O(ui), Y(es), t(rue), 1, on +// - false : N(on), f(alse), 0, off +// La valeur est insensible à la casse. +func PromptBool(sp PromptFunc) ParsedPromptFunc[bool] { + return ParsedPrompt(sp, boolConv) +} + +// MultiplePrompt transforme un prompt en prompt de valeurs multiples. +// Si aucun tokenizer n’est fourni, il considère la chaîne globale +// comme des mots séparés par des blancs. +func MultiplePrompt[T any]( + sp PromptFunc, + conv ConvFunc[T], + t ...scanner.Tokenizer, +) ParsedPromptFunc[[]T] { + return func(p string) (out Result[[]T]) { + sout := sp(p) + if line, ok := sout.Ok(); ok { + var tk scanner.Tokenizer + if len(t) > 0 { + tk = t[0] + } else { + tk = scanner.NewTokenizer(false, false) + } + sc := scanner.NewScanner(strings.NewReader(line), tk) + + var tmp []T + for sc.Scan() { + elem := conv(sc.Text()) + if v, ok := elem.Ok(); ok { + tmp = append(tmp, v) + } else if err, ok := elem.Err(); ok { + return Err[[]T](err) + } + } + out = Ok(tmp) + } else if err, ok := sout.Err(); ok { + out = Err[[]T](err) + } + + return + } +} + +// PromptSlice transform un prompt en prompt de slice de strings. +func PromptSlice(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]string] { + return MultiplePrompt(sp, strConv, t...) +} + +// PromptSliceInt transform un prompt en prompt de slice d’entiers. +func PromptSliceInt(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]int] { + return MultiplePrompt(sp, singleConv[int], t...) +} + +// PromptSliceUint transform un prompt en prompt de slice d’entiers non signés. +func PromptSliceUint(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]uint] { + return MultiplePrompt(sp, singleConv[uint], t...) +} + +// PromptSliceFloat transform un prompt en prompt de slice de décimaux. +func PromptSliceFloat(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]float64] { + return MultiplePrompt(sp, singleConv[float64], t...) +} + +// PromptSliceBool transform un prompt en prompt de slice de booléens. +func PromptSliceBool(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]bool] { + return MultiplePrompt(sp, boolConv, t...) +} diff --git a/shell/console/read/read.go b/shell/console/read/read.go new file mode 100644 index 0000000..dfcb7fb --- /dev/null +++ b/shell/console/read/read.go @@ -0,0 +1,144 @@ +package read + +import ( + "bufio" + "fmt" + "os" + "strings" + + "gitea.zaclys.com/bvaudour/gob/collection" + . "gitea.zaclys.com/bvaudour/gob/option" + "gitea.zaclys.com/bvaudour/gob/shell/console" + "gitea.zaclys.com/bvaudour/gob/shell/scanner" +) + +// String retourne la chaîne de caractères saisie. +func String(prompt string) Result[string] { + fmt.Print(prompt) + sc := bufio.NewScanner(os.Stdin) + + if sc.Scan() { + return Ok(sc.Text()) + } + return Err[string](sc.Err()) +} + +// Int retourne l’entier saisi. +func Int(prompt string) Result[int] { + return console.PromptInt(String)(prompt) +} + +// Uint retourne l’entier non signé saisi. +func Uint(prompt string) Result[uint] { + return console.PromptUint(String)(prompt) +} + +// Float retourne le nombre décimal saisi. +func Float(prompt string) Result[float64] { + return console.PromptFloat(String)(prompt) +} + +// Bool retourne le booléen saisi. +func Bool(prompt string) Result[bool] { + return console.PromptBool(String)(prompt) +} + +// Slice retourne les mots saisis. +func Slice(prompt string, t ...scanner.Tokenizer) Result[[]string] { + return console.PromptSlice(String, t...)(prompt) +} + +// SliceInt retourne les entiers saisis. +func SliceInt(prompt string, t ...scanner.Tokenizer) Result[[]int] { + return console.PromptSliceInt(String, t...)(prompt) +} + +// SliceUint retourne les entiers non signés saisis. +func SliceUint(prompt string, t ...scanner.Tokenizer) Result[[]uint] { + return console.PromptSliceUint(String, t...)(prompt) +} + +// SliceFloat retourne les nombres décimaux saisis. +func SliceFloat(prompt string, t ...scanner.Tokenizer) Result[[]float64] { + return console.PromptSliceFloat(String, t...)(prompt) +} + +// SliceBool retourne les booléens saisis. +func SliceBool(prompt string, t ...scanner.Tokenizer) Result[[]bool] { + return console.PromptSliceBool(String, t...)(prompt) +} + +// Default lance une invite de commande attendant une réponse optionnelle +func Default[T any](prompt string, def T, pp console.ParsedPromptFunc[T]) (result Result[T]) { + if result = pp(prompt); !result.IsOk() { + result = Ok(def) + } + + return +} + +func question[T any](q string, def T, pp console.ParsedPromptFunc[T]) (result T) { + prompt := fmt.Sprintf("%s (default: %v) ", q, def) + + result, _ = Default(prompt, def, pp).Ok() + + return +} + +// Question invite à saisir une chaîne. +// Si aucune chaîne n’est saisie, def est retourné. +func Question(q string, def string) string { + return question(q, def, String) +} + +// QuestionInt invite à saisir un entier. +// Si aucun entier n’est saisi, def est retourné. +func QuestionInt(q string, def int) int { + return question(q, def, Int) +} + +// QuestionUint invite à saisir un entier non signé. +// Si aucun entier non signé n’est saisi, def est retourné. +func QuestionUint(q string, def uint) uint { + return question(q, def, Uint) +} + +// QuestionFloat invite à saisir un nombre décimal. +// Si aucun nombre décimal n’est saisi, def est retourné. +func QuestionFloat(q string, def float64) float64 { + return question(q, def, Float) +} + +// QuestionBool invite à saisir un booléen. +// Si aucun booléen n’est saisi, def est retourné. +func QuestionBool(q string, def bool) bool { + choices := "o/N" + if def { + choices = "O/n" + } + + prompt := fmt.Sprintf("%s [%s] ", q, choices) + if r, ok := String(prompt).Ok(); ok && len(r) > 0 { + switch r[0] { + case 'Y', 'y', 'O', 'o', '1': + return true + case 'N', 'n', '0': + return false + } + } + + return def +} + +// QuestionChoice invite à saisir un chaîne parmi un choix donné. +// Si aucun choix n’est effectué, retourne def. +func QuestionChoice(q string, def string, choices []string) string { + set := collection.NewSet(choices...) + + prompt := fmt.Sprintf("%s [%s] (default: %v) ", q, strings.Join(choices, "|"), def) + if r, ok := String(prompt).Ok(); ok && set.Contains(r) { + return r + } + + return def +}