653 lines
14 KiB
Go
653 lines
14 KiB
Go
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()
|
||
ClearEndOfScreen()
|
||
|
||
if n > 0 {
|
||
NewLines(n)
|
||
MoveLineUp(n)
|
||
SaveCursorPosition()
|
||
}
|
||
|
||
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()
|
||
}
|
||
|
||
// SavedBuffer retouren la représentation du buffer sauvegardé, si celui-ci existe.
|
||
func (st *State) SavedBuffer() (out option.Option[string]) {
|
||
if buf, ok := st.savedBuffer.Get(); ok {
|
||
out = option.Some(buf.String())
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// Width retourne la largeur du terminal.
|
||
func (st *State) Width() int {
|
||
return st.dim.Width()
|
||
}
|
||
|
||
// Height retourn la hauteur du terminal.
|
||
func (st *State) Height() int {
|
||
return st.dim.Height()
|
||
}
|
||
|
||
// ToggleReplace se place en mode remplacement si on était on mode insertion
|
||
// et vice-versa.
|
||
func (st *State) ToggleReplace() {
|
||
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()
|
||
}
|
||
|
||
// SetCursor déplace le curseur à la position donnée
|
||
func (st *State) SetCursor(cursor int) bool {
|
||
return st.buffer.SetCursor(cursor)
|
||
}
|
||
|
||
func (st *State) rem(cb func() option.Option[string]) (ok bool) {
|
||
var yank string
|
||
if yank, ok = cb().Get(); ok {
|
||
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
|
||
}
|
||
|
||
// GetProposal retourne une suggestion de saisie pour la saisie en cours
|
||
// basée sur l’historique.
|
||
func (st *State) GetProposal(insensitive ...bool) (proposal string) {
|
||
candidates := console.HistoryFilterPrefix(st.history, st.buffer.String(), insensitive...)
|
||
if l := len(candidates); l > 0 {
|
||
proposal = candidates[l-1]
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// SearchMotiveNext remonte dans l’historique de recherche.
|
||
// L’historique de recherche est initialisée avec le motif du buffer.
|
||
func (st *State) SearchMotiveNext(insensitive ...bool) (ok bool) {
|
||
var hs console.History
|
||
|
||
if hs, ok = st.historySearch.Get(); !ok {
|
||
motive := st.buffer.String()
|
||
hs = NewHistory(false)
|
||
candidates := console.HistoryFilterMotive(st.history, motive, insensitive...)
|
||
|
||
for _, h := range candidates {
|
||
hs.Append(h)
|
||
}
|
||
|
||
hs.SetCursor(-1)
|
||
st.historySearch = option.Some(hs)
|
||
}
|
||
|
||
if ok = hs.Next(); ok {
|
||
st.UnfocusYank()
|
||
st.UnfocusHistory()
|
||
st.SaveBuffer()
|
||
|
||
var h string
|
||
if h, ok = console.FocusedElement(hs).Get(); ok {
|
||
st.SetBuffer(h)
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// SearchMotivePrev redescend dans l’historique de recherche.
|
||
// L’historique de recherche est initialisée avec le motif du buffer.
|
||
func (st *State) SearchMotivePrev(insensitive ...bool) (ok bool) {
|
||
var hs console.History
|
||
|
||
if hs, ok = st.historySearch.Get(); !ok {
|
||
prefix := st.buffer.String()
|
||
hs = NewHistory(false)
|
||
candidates := console.HistoryFilterMotive(st.history, prefix, insensitive...)
|
||
|
||
for _, h := range candidates {
|
||
hs.Append(h)
|
||
}
|
||
|
||
st.historySearch = option.Some(hs)
|
||
}
|
||
if ok = hs.Prev(); ok {
|
||
st.UnfocusYank()
|
||
st.UnfocusHistory()
|
||
st.SaveBuffer()
|
||
|
||
var h string
|
||
if h, ok = console.FocusedElement(hs).Get(); ok {
|
||
st.SetBuffer(h)
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// SearchHistoryNext remonte dans l’historique de recherche.
|
||
// L’historique de recherche est initialisée avec le préfixe du buffer.
|
||
func (st *State) SearchHistoryNext(insensitive ...bool) (ok bool) {
|
||
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
|
||
}
|
||
|
||
// GetPrompt retourne le prompt configuré.
|
||
func (st *State) GetPrompt() string {
|
||
return st.prompt
|
||
}
|