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 nest 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 lensemble 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 nest 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 na 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 lincrément donné.
// Si lincré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 dun caractère vers la gauche.
func (b *Buffer) Left() (ok bool) {
return b.Move(-1)
}
// Right déplace le curseur dun 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 nest
// 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 lautocomplé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 à lindex 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 sil 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 lhistorique des saisies.
type History struct {
Cycler
}
// Append ajoute une entrée dans lhistorique.
func (h *History) Append(data string) {
h.Cycler.Append(data)
h.cursor = h.Len()
}
// Clear efface lhistorique.
func (h *History) Clear() {
h.Cycler.Clear()
h.cursor = 0
}
// SetCursor positionne le pointeur de lhistorique.
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 lhistorique
// 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 lhistorique
// 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 lhistorique à partir dun fichier
// et retourne le nombre dentré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 lhistorique dans un fichier
// et retourne le nombre dentré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 lhistorique 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 dune 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 jusquau 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 lhistorique de laffichage.
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 dun 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 linitialisation 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 lenregistrement du buffer même sil 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 dhistorique.
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 lhistorique.
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 sassure 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 lhistorique.
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 lhistorique.
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 lhistorique à partir dun fichier.
func (st *State) LoadHistory(r io.Reader) option.Result[int] {
return st.history.Read(r)
}
// SaveHistory persiste lhistorique dans un fichier.
func (st *State) SaveHistory(w io.Writer) option.Result[int] {
return st.history.Write(w)
}
// ClearHistory efface lhistorique.
func (st *State) ClearHistory() {
st.history.Clear()
}
// AppendHistory ajoute une entrée dans lhistorique.
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 lhistorique.
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 lhistorique.
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 lhistorique de recherche.
// Lhistorique 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 lhistorique de recherche.
// Lhistorique 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 lhistorique, 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 dune liste.
type Cycler interface {
Len() int // Retourne la longueur de la liste
Cursor() int // Retourne lindex de lentrée pointée
Index(int) Option[string] // Retourne lentrée à lindex donné
SetCursor(int) bool // Déplace lentrée pointée à lindex 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 lentrée suivante
Prev() bool // Pointe vers lentrée précédente
}
// History est une interface pour gérer lhistorique 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 lhistorique.
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 nest fourni, le message derreur
// 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
// lerreur fournie.
func ExitWithError(err Error, w ...io.Writer) {
Exit(err.Code(), err.Error(), w...)
}
// HistoryList retourne le contenu de lhistorique.
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 lhistorique 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 lhistorique 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 lhistorique 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 lhistorique 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 lhistorique.
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 dentier.
func PromptInt(sp PromptFunc) ParsedPromptFunc[int] {
return ParsedPrompt(sp, singleConv[int])
}
// PromptUint transforme un prompt en prompt dentier 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 nest 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 dentiers.
func PromptSliceInt(sp PromptFunc, t ...scanner.Tokenizer) ParsedPromptFunc[[]int] {
return MultiplePrompt(sp, singleConv[int], t...)
}
// PromptSliceUint transform un prompt en prompt de slice dentiers 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 lentier saisi.
func Int(prompt string) Result[int] {
return console.PromptInt(String)(prompt)
}
// Uint retourne lentier 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 nest saisie, def est retourné.
func Question(q string, def string) string {
return question(q, def, String)
}
// QuestionInt invite à saisir un entier.
// Si aucun entier nest 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é nest 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 nest 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 nest 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 nest 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
}