123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- package hhccli
- import (
- "bufio"
- "context"
- "fmt"
- "git.swzry.com/zry/go-hhc-cli/hhc_ast"
- "git.swzry.com/zry/go-hhc-cli/hhc_common"
- "git.swzry.com/zry/ztermtab"
- "io"
- "runtime"
- "unicode"
- "unicode/utf8"
- )
- const (
- TermInteractiveLogLevel_Fatal TerminalInteractiveDebugLogLevel = 0
- TermInteractiveLogLevel_Error TerminalInteractiveDebugLogLevel = 1
- TermInteractiveLogLevel_Warn TerminalInteractiveDebugLogLevel = 2
- TermInteractiveLogLevel_Info TerminalInteractiveDebugLogLevel = 3
- TermInteractiveLogLevel_Debug TerminalInteractiveDebugLogLevel = 4
- )
- func (lv TerminalInteractiveDebugLogLevel) Name() string {
- switch lv {
- case TermInteractiveLogLevel_Fatal:
- return "FATAL"
- case TermInteractiveLogLevel_Error:
- return "ERROR"
- case TermInteractiveLogLevel_Warn:
- return "WARN"
- case TermInteractiveLogLevel_Info:
- return "INFO"
- case TermInteractiveLogLevel_Debug:
- return "DEBUG"
- default:
- return "???"
- }
- }
- type TerminalInteractiveDebugLogLevel uint8
- type TerminalInteractiveDebugLog func(lv TerminalInteractiveDebugLogLevel, msg ...interface{})
- type TerminalInteractive struct {
- inStream io.Reader
- outStream io.Writer
- errStream io.Writer
- debugWrite TerminalInteractiveDebugLog
- termWidth int
- inBufReader *bufio.Reader
- errCh chan error
- runeCh chan rune
- printResultRuneCh chan rune
- currentPrompt string
- views map[string]CliView
- currentView CliView
- termState hhc_common.TerminalState
- parser *hhc_ast.SDTLineParser
- runesEditor *hhc_common.RunesEditor
- backspaceRune rune
- tcesParser *SimpleTCESParser
- printModePrintLines []string
- printModeLineCount int
- printModeCurrentLine int
- printModeIsHelp bool
- execModeContext *hhc_ast.ExecContext
- }
- type TerminalHandlerInterface interface {
- Reset(iow io.Writer, promptStr string, sdtp *hhc_ast.SDTLineParser)
- ProcessRune(r rune) (end bool, reprint bool)
- GetCmdLinePrintable(termWidth int) []string
- GetCalculatedCleanLineCount() int
- }
- type TerminalInteractiveConfig struct {
- InputStream io.Reader
- OutputStream io.Writer
- ErrorStream io.Writer
- InitialTerminalWidth int
- DebugLogFunction TerminalInteractiveDebugLog
- InitialPrompt string
- BackspaceRune rune
- }
- func nullDebugLogFunc(lv TerminalInteractiveDebugLogLevel, msg ...interface{}) {
- }
- func NewTerminalInteractive(cfg TerminalInteractiveConfig) *TerminalInteractive {
- ti := &TerminalInteractive{
- inStream: cfg.InputStream,
- outStream: cfg.OutputStream,
- errStream: cfg.ErrorStream,
- termWidth: cfg.InitialTerminalWidth,
- inBufReader: bufio.NewReader(cfg.InputStream),
- currentPrompt: cfg.InitialPrompt,
- views: map[string]CliView{},
- termState: hhc_common.TerminalState_Idle,
- backspaceRune: cfg.BackspaceRune,
- tcesParser: NewSimpleTCESParser(),
- }
- if cfg.DebugLogFunction == nil {
- ti.debugWrite = nullDebugLogFunc
- } else {
- ti.debugWrite = cfg.DebugLogFunction
- }
- if ti.currentPrompt == "" {
- ti.currentPrompt = ">"
- }
- if ti.backspaceRune == 0 {
- ti.backspaceRune = '\x7F'
- }
- return ti
- }
- func (ti *TerminalInteractive) RegisterView(viewClassName string, view CliView) {
- ti.views[viewClassName] = view
- }
- func (ti *TerminalInteractive) SetCurrentView(viewClassName string) error {
- v, ok := ti.views[viewClassName]
- if ok {
- ti.currentView = v
- } else {
- ti.currentView = nil
- return fmt.Errorf("invalid vcn '%s'", viewClassName)
- }
- return nil
- }
- func (ti *TerminalInteractive) readRunesRoutine() {
- for {
- rr, _, err := ti.inBufReader.ReadRune()
- if err != nil {
- ti.errCh <- err
- return
- }
- ti.runeCh <- rr
- }
- }
- func (ti *TerminalInteractive) SetPrompt(p string) {
- ti.currentPrompt = p
- }
- func (ti *TerminalInteractive) Run(ctx context.Context) error {
- ti.errCh = make(chan error)
- ti.runeCh = make(chan rune)
- go ti.readRunesRoutine()
- err := ti.gotoStateInput(false)
- if err != nil {
- return err
- }
- for {
- select {
- case <-ctx.Done():
- ti.debugWrite(TermInteractiveLogLevel_Info, "canceled by user")
- return nil
- case err := <-ti.errCh:
- ti.debugWrite(TermInteractiveLogLevel_Error, err.Error())
- return err
- case rr := <-ti.runeCh:
- err := ti.stateMachinePushRune(rr)
- if err != nil {
- return err
- }
- }
- }
- }
- func (ti *TerminalInteractive) stateMachinePushRune(r rune) error {
- for {
- switch ti.termState {
- case hhc_common.TerminalState_Idle:
- return ti.gotoStateInput(false)
- case hhc_common.TerminalState_Input:
- b, err := ti.stateInputPushRune(r)
- if b || err != nil {
- return err
- }
- case hhc_common.TerminalState_TCES:
- b, err := ti.stateTCESPushRune(r)
- if b || err != nil {
- return err
- }
- case hhc_common.TerminalState_Execute:
- if ti.execModeContext != nil {
- ti.execModeContext.WriteRune(r)
- }
- return nil
- case hhc_common.TerminalState_PrintResult:
- b, err := ti.statePrintResultPushRune(r)
- if b || err != nil {
- return err
- }
- default:
- return fmt.Errorf("invalid state for terminal handler state machine: %d", ti.termState)
- }
- }
- }
- func (ti *TerminalInteractive) gotoStateInput(saveHistory bool) error {
- if ti.currentView == nil {
- return fmt.Errorf("no current view specified")
- }
- sdtf := ti.currentView.GetSDTRoot()
- sdtw := hhc_ast.NewSDTWalker(sdtf)
- ti.parser = hhc_ast.NewSDTLineParser(sdtw)
- if ti.runesEditor == nil {
- ti.runesEditor = hhc_common.NewRunesEditor()
- } else {
- ti.runesEditor.ClearCurrentLine(saveHistory)
- }
- _, _ = fmt.Fprint(ti.outStream, "\r\n")
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- ti.termState = hhc_common.TerminalState_Input
- return nil
- }
- func (ti *TerminalInteractive) gotoStateTCES() error {
- ti.termState = hhc_common.TerminalState_TCES
- ti.tcesParser.ResetState()
- return nil
- }
- func (ti *TerminalInteractive) gotoStatePrintResult(s []string, isHelp bool) {
- ti.printModeIsHelp = isHelp
- ti.printModeLineCount = len(s)
- ti.printModeCurrentLine = 0
- ti.printModePrintLines = s
- ti.termState = hhc_common.TerminalState_PrintResult
- _, _ = fmt.Fprint(ti.errStream, "---- More ----")
- ti.printLinesInPrintResultState(20)
- }
- func (ti *TerminalInteractive) gotoStateExecute(ef hhc_ast.ExecuteFunc, ctx *hhc_ast.SDTWalkContext) {
- ti.termState = hhc_common.TerminalState_Execute
- ti.execModeContext = hhc_ast.WrapNewExecContext(ctx, ti.outStream, ti.errStream, ti.termWidth)
- go ti.executeRoutine(ef)
- }
- func (ti *TerminalInteractive) exitStateExecute() {
- if ti.execModeContext != nil {
- nv := ti.execModeContext.GetNextView()
- if nv != "" {
- err := ti.SetCurrentView(nv)
- if err != nil {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% command line system error in change view: %s\r\n", err.Error())
- }
- }
- wcp, np := ti.execModeContext.GetNextPrompt()
- if wcp {
- ti.SetPrompt(np)
- }
- if ti.execModeContext.WillGotoPrintResultMode() {
- res := ti.execModeContext.GetResultPrintLines()
- _, _ = fmt.Fprintf(ti.errStream, "\r\n")
- ti.gotoStatePrintResult(res, false)
- } else {
- err := ti.gotoStateInput(false)
- if err != nil {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% command line system error: %s\r\n", err.Error())
- }
- }
- ti.execModeContext = nil
- } else {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% broken command (this is a bug... or may be a feature?)\r\n")
- err := ti.gotoStateInput(false)
- if err != nil {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% command line system error: %s\r\n", err.Error())
- }
- }
- }
- func (ti *TerminalInteractive) executeRoutine(ef hhc_ast.ExecuteFunc) {
- if ti.execModeContext != nil {
- if ef != nil {
- err := ti.protectedExecute(ef, ti.execModeContext)
- if err != nil {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% broken command (this is a bug... or may be a feature?)\r\n")
- _, _ = fmt.Fprintf(ti.errStream, "command line execute error: %s\r\n", err.Error())
- }
- ti.exitStateExecute()
- } else {
- ti.exitStateExecute()
- }
- } else {
- ti.exitStateExecute()
- }
- }
- func (ti *TerminalInteractive) protectedExecute(ef hhc_ast.ExecuteFunc, ctx *hhc_ast.ExecContext) error {
- var err error
- defer func() {
- rec := recover()
- switch rec.(type) {
- case runtime.Error:
- err = rec.(runtime.Error)
- break
- case error:
- err = rec.(error)
- break
- }
- }()
- ef(ctx.GetUserContext())
- return err
- }
- func (ti *TerminalInteractive) printLinesInPrintResultState(nline int) {
- if ti.printModePrintLines == nil {
- if ti.printModeIsHelp {
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return
- } else {
- _ = ti.gotoStateInput(true)
- return
- }
- }
- isend := false
- _, _ = fmt.Fprint(ti.errStream, "\r\033[K")
- for i := 0; i < nline; i++ {
- if ti.printModeCurrentLine < ti.printModeLineCount {
- _, _ = fmt.Fprintf(ti.errStream, " %s\r\n", ti.printModePrintLines[ti.printModeCurrentLine])
- ti.printModeCurrentLine++
- }
- if ti.printModeCurrentLine == ti.printModeLineCount {
- isend = true
- break
- }
- }
- if isend {
- _, _ = fmt.Fprint(ti.errStream, "\r\n")
- ti.printModePrintLines = nil
- if ti.printModeIsHelp {
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return
- } else {
- _ = ti.gotoStateInput(true)
- return
- }
- } else {
- _, _ = fmt.Fprint(ti.errStream, "---- More ----")
- }
- }
- func (ti *TerminalInteractive) stateInputPushRune(r rune) (bool, error) {
- if utf8.ValidRune(r) {
- if unicode.IsControl(r) {
- return ti.stateInputPushControlRune(r)
- }
- if unicode.IsGraphic(r) {
- return ti.stateInputPushGraphicRune(r)
- }
- }
- return true, nil
- }
- func (ti *TerminalInteractive) stateInputPushControlRune(r rune) (bool, error) {
- switch r {
- case '\x03':
- err := ti.gotoStateInput(false)
- if err != nil {
- return true, err
- }
- return true, nil
- case ti.backspaceRune:
- if ti.runesEditor != nil {
- ti.runesEditor.Backspace()
- ti.runesEditor.RefreshTerminal(ti.outStream, ti.currentPrompt, ti.termWidth, false)
- }
- return true, nil
- case '\x1b':
- err := ti.gotoStateTCES()
- if err != nil {
- return true, err
- }
- return true, nil
- case '\x09':
- ti.parser.Reset()
- rs := ti.runesEditor.Peek()
- ti.parser.Parse(rs)
- if ti.parser.HasError() {
- return true, nil
- }
- hi, tok := ti.parser.GetHelpListForTabComplete()
- if len(hi) == 1 {
- if hi[0].IsArg {
- return true, nil
- }
- suf := hhc_common.GetCompleteSuffix(hi[0].Name, tok)
- ti.runesEditor.InsertString(suf)
- ti.runesEditor.RefreshTerminal(ti.outStream, ti.currentPrompt, ti.termWidth, false)
- } else {
- ttp := ztermtab.NewTerminalTablePrinter(ti.termWidth, " ", " | ", "")
- ttp.AddColumn("", 10, 30, false)
- ttp.AddColumn("", 0, 0, false)
- ttp.SetAutoWidthColumn(1)
- for _, v := range hi {
- ttp.AddRow([]string{v.Name, v.Description})
- }
- _, _ = fmt.Fprint(ti.errStream, "\r\n")
- ti.gotoStatePrintResult(ttp.RenderToTerminalLines(), true)
- }
- return true, nil
- case '\r':
- if len(ti.runesEditor.Peek()) <= 0 {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n")
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return true, nil
- }
- ti.parser.Reset()
- rs := ti.runesEditor.Enter()
- ti.parser.Parse(rs)
- ok, ef := ti.parser.TryGetRunFunc()
- if ti.parser.HasError() {
- e, p := ti.parser.GetError()
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% %s at position %d\r\n", hhc_common.FirstToUpper(e.EES()), p)
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return true, nil
- }
- if ok {
- if ef == nil {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% broken command (this is a bug... or may be a feature?)\r\n")
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return true, nil
- } else {
- ti.gotoStateExecute(ef, ti.parser.GetWalkContext())
- return true, nil
- }
- } else {
- _, _ = fmt.Fprintf(ti.errStream, "\r\n %% incomplete command\r\n")
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return true, nil
- }
- }
- return true, nil
- }
- func (ti *TerminalInteractive) stateInputPushGraphicRune(r rune) (bool, error) {
- if ti.runesEditor != nil {
- ti.runesEditor.InsertRune(r)
- ti.runesEditor.RefreshTerminal(ti.outStream, ti.currentPrompt, ti.termWidth, false)
- }
- if r == '?' {
- ti.parser.Reset()
- rs := ti.runesEditor.Peek()
- ti.parser.Parse(rs)
- if !ti.parser.HelpTriggerd() {
- return true, nil
- }
- _, _ = fmt.Fprint(ti.errStream, "\r\n")
- if ti.parser.HasError() {
- e, p := ti.parser.GetError()
- _, _ = fmt.Fprintf(ti.errStream, " %% %s at position %d\r\n", hhc_common.FirstToUpper(e.EES()), p)
- ti.runesEditor.Backspace()
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- return true, nil
- }
- if ti.parser.WillPrintHelp() {
- ti.runesEditor.Backspace()
- hi := ti.parser.GetHelpMessage()
- ttp := ztermtab.NewTerminalTablePrinter(ti.termWidth, " ", " | ", "")
- ttp.AddColumn("", 10, 30, false)
- ttp.AddColumn("", 0, 0, false)
- ttp.SetAutoWidthColumn(1)
- for _, v := range hi {
- ttp.AddRow([]string{v.Name, v.Description})
- }
- ti.gotoStatePrintResult(ttp.RenderToTerminalLines(), true)
- return true, nil
- }
- }
- return true, nil
- }
- func (ti *TerminalInteractive) stateTCESPushRune(r rune) (bool, error) {
- b, res := ti.tcesParser.PushRune(r)
- if b {
- ti.termState = hhc_common.TerminalState_Input
- if ti.runesEditor == nil {
- return true, nil
- }
- switch res {
- case SimpleTCESParserResult_LeftArrow:
- ti.runesEditor.MoveInsertPosition(-1)
- break
- case SimpleTCESParserResult_RightArrow:
- ti.runesEditor.MoveInsertPosition(1)
- break
- case SimpleTCESParserResult_UpArrow:
- ti.runesEditor.LoadPreviousHistory()
- break
- case SimpleTCESParserResult_DownArrow:
- ti.runesEditor.LoadNextHistory()
- break
- case SimpleTCESParserResult_HomeKey:
- ti.runesEditor.MoveInsertPositionToHome()
- break
- case SimpleTCESParserResult_EndKey:
- ti.runesEditor.MoveInsertPositionToEnd()
- break
- case SimpleTCESParserResult_DelKey:
- ti.runesEditor.Delete()
- break
- default:
- break
- }
- ti.runesEditor.RefreshTerminal(ti.outStream, ti.currentPrompt, ti.termWidth, false)
- }
- return true, nil
- }
- func (ti *TerminalInteractive) statePrintResultPushRune(r rune) (bool, error) {
- switch r {
- case '\r':
- ti.printLinesInPrintResultState(1)
- break
- case ' ':
- ti.printLinesInPrintResultState(20)
- break
- case '\x03':
- _, _ = fmt.Fprint(ti.errStream, "\r\n")
- ti.printModePrintLines = nil
- if ti.printModeIsHelp {
- ti.termState = hhc_common.TerminalState_Input
- ti.runesEditor.PeekIntoTerminal(ti.currentPrompt, ti.termWidth, ti.outStream)
- } else {
- _ = ti.gotoStateInput(true)
- }
- break
- }
- return true, nil
- }
- func (ti *TerminalInteractive) SetTerminalWidth(w int) {
- ti.termWidth = w
- }
- func (ti *TerminalInteractive) GetTerminalWidth() int {
- return ti.termWidth
- }
|