Module shell (3)
This commit is contained in:
parent
8e5ad41b11
commit
75f0c1350b
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
|
@ -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--
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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...))
|
||||||
|
}
|
|
@ -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...)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue