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") } 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 }