Module shell (3)

This commit is contained in:
Benjamin VAUDOUR 2023-10-07 21:13:39 +02:00
parent 8e5ad41b11
commit 75f0c1350b
13 changed files with 2644 additions and 0 deletions

46
option/choice.go Normal file
View file

@ -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()
}

View file

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

View file

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

View file

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

338
shell/console/atom/input.go Normal file
View file

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

401
shell/console/atom/key.go Normal file
View file

@ -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) // ^[<BACK>
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,
}
)

View file

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

555
shell/console/atom/state.go Normal file
View file

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

View file

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

175
shell/console/console.go Normal file
View file

@ -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())
}

View file

@ -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...))
}

152
shell/console/prompt.go Normal file
View file

@ -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...)
}

144
shell/console/read/read.go Normal file
View file

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