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 }