// startReadLine prepares the terminal for the editor. func (ed *Editor) startReadLine() error { ed.activeMutex.Lock() defer ed.activeMutex.Unlock() ed.active = true savedTermios, err := setupTerminal(ed.file) if err != nil { return err } ed.savedTermios = savedTermios _, width := sys.GetWinsize(int(ed.file.Fd())) // Turn on autowrap, write lackEOL along with enough padding to fill the // whole screen. If the cursor was in the first column, we end up in the // same line (just off the line boundary); otherwise we are now in the next // line. We now rewind to the first column and erase anything there. The // final effect is that a lackEOL gets written if and only if the cursor // was not in the first column. fmt.Fprintf(ed.file, "\033[?7h%s%*s\r \r", lackEOL, width-util.Wcwidth(lackEOLRune), "") // Turn off autowrap. The edito has its own wrapping mechanism. Doing // wrapping manually means that when the actual width of some characters // are greater than what our wcwidth implementation tells us, characters at // the end of that line gets hidden -- compared to pushed to the next line, // which is more disastrous. ed.file.WriteString("\033[?7l") // Turn on SGR-style mouse tracking. //ed.file.WriteString("\033[?1000;1006h") // Enable bracketed paste. ed.file.WriteString("\033[?2004h") return nil }
// startReadLine prepares the terminal for the editor. func (ed *Editor) startReadLine() error { savedTermios, err := setupTerminal(ed.file) if err != nil { return err } ed.savedTermios = savedTermios _, width := sys.GetWinsize(int(ed.file.Fd())) // Turn on autowrap, write lackEOL along with enough padding to fill the // whole screen. If the cursor was in the first column, we end up in the // same line (just off the line boundary); otherwise we are now in the next // line. We now rewind to the first column and erase anything there. The // final effect is that a lackEOL gets written if and only if the cursor // was not in the first column. // // After that, we turn off autowrap. The editor has its own wrapping // mechanism. fmt.Fprintf(ed.file, "\033[?7h%s%*s\r \r\033[?7l", lackEOL, width-WcWidth(lackEOLRune), "") return nil }
// refresh redraws the line editor. The dot is passed as an index into text; // the corresponding position will be calculated. func (w *writer) refresh(es *editorState) error { height, width := sys.GetWinsize(int(w.file.Fd())) var bufLine, bufMode, bufTips, bufListing, buf *buffer // bufLine b := newBuffer(width) bufLine = b b.newlineWhenFull = true b.writes(es.prompt, styleForPrompt) if b.line() == 0 && b.col*2 < b.width { b.indent = b.col } // i keeps track of number of bytes written. i := 0 comp := es.completion hasComp := comp != nil && comp.current != -1 nowAt := func(i int) { if es.dot == i { if hasComp { // Put the current completion candidate. candSource := comp.candidates[comp.current].source b.writes(candSource.text, candSource.style+styleForCompleted) } b.dot = b.cursor() } } nowAt(0) tokens: for _, token := range es.tokens { for _, r := range token.Text { b.write(r, styleForType[token.Type]+token.MoreStyle) i += utf8.RuneLen(r) nowAt(i) if es.mode == modeHistory && i == len(es.history.prefix) { break tokens } } } if es.mode == modeHistory { // Put the rest of current history, position the cursor at the // end of the line, and finish writing h := es.history b.writes(h.line[len(h.prefix):], styleForCompletedHistory) b.dot = b.cursor() } // Write rprompt padding := b.width - b.col - WcWidths(es.rprompt) if padding >= 1 { b.newlineWhenFull = false b.writePadding(padding, "") b.writes(es.rprompt, styleForRPrompt) } // bufMode if es.mode != modeInsert { b := newBuffer(width) bufMode = b text := "" switch es.mode { case modeCommand: text = "COMMAND" case modeCompletion: text = fmt.Sprintf("COMPLETING %s", comp.completer) case modeNavigation: text = "NAVIGATING" case modeHistory: text = fmt.Sprintf("HISTORY #%d", es.history.current) } b.writes(TrimWcWidth(" "+text+" ", width), styleForMode) } // bufTips // TODO tips is assumed to contain no newlines. if len(es.tips) > 0 { b := newBuffer(width) bufTips = b b.writes(TrimWcWidth(strings.Join(es.tips, ", "), width), styleForTip) } hListing := 0 // Trim lines and determine the maximum height for bufListing switch { case height >= lines(bufLine, bufMode, bufTips): hListing = height - lines(bufLine, bufMode, bufTips) case height >= lines(bufLine, bufTips): bufMode, bufListing = nil, nil case height >= lines(bufLine): bufTips, bufMode, bufListing = nil, nil, nil case height >= 1: bufTips, bufMode, bufListing = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine+1-height, dotLine+1) default: // Broken terminal. Still try to render one line of bufLine. bufTips, bufMode, bufListing = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine, dotLine+1) } // Render bufListing under the maximum height constraint nav := es.navigation if hListing > 0 && comp != nil || nav != nil { b := newBuffer(width) bufListing = b // Completion listing if comp != nil { // Layout candidates in multiple columns cands := comp.candidates // First decide the shape (# of rows and columns) colWidth := 0 margin := completionListingColMargin for _, cand := range cands { width := WcWidths(cand.menu.text) if colWidth < width { colWidth = width } } cols := (b.width + margin) / (colWidth + margin) if cols == 0 { cols = 1 } lines := CeilDiv(len(cands), cols) es.completionLines = lines // Determine the window to show. low, high := findWindow(lines, comp.current%lines, hListing) for i := low; i < high; i++ { if i > low { b.newline() } for j := 0; j < cols; j++ { k := j*lines + i if k >= len(cands) { break } style := cands[k].menu.style if k == comp.current { style += styleForCurrentCompletion } text := cands[k].menu.text if j > 0 { b.writePadding(margin, "") } b.writes(ForceWcWidth(text, colWidth), style) } } } // Navigation listing if nav != nil { margin := navigationListingColMargin var ratioParent, ratioCurrent, ratioPreview int if nav.dirPreview != nil { ratioParent = 15 ratioCurrent = 40 ratioPreview = 45 } else { ratioParent = 15 ratioCurrent = 75 // Leave some space at the right side } w := width - margin*2 wParent := w * ratioParent / 100 wCurrent := w * ratioCurrent / 100 wPreview := w * ratioPreview / 100 b := renderNavColumn(nav.parent, wParent, hListing) bufListing = b bCurrent := renderNavColumn(nav.current, wCurrent, hListing) b.extendHorizontal(bCurrent, wParent, margin) if wPreview > 0 { bPreview := renderNavColumn(nav.dirPreview, wPreview, hListing) b.extendHorizontal(bPreview, wParent+wCurrent+margin, margin) } } } // Combine buffers (reusing bufLine) buf = bufLine buf.extend(bufMode) buf.extend(bufTips) buf.extend(bufListing) return w.commitBuffer(buf) }
// refresh redraws the line editor. The dot is passed as an index into text; // the corresponding position will be calculated. func (w *writer) refresh(bs *editorState) error { winsize := sys.GetWinsize(int(w.file.Fd())) width, height := int(winsize.Col), int(winsize.Row) var bufLine, bufMode, bufTips, bufListing, buf *buffer // bufLine b := newBuffer(width) bufLine = b b.newlineWhenFull = true b.writes(bs.prompt, attrForPrompt) if b.line() == 0 && b.col*2 < b.width { b.indent = b.col } // i keeps track of number of bytes written. i := 0 if bs.dot == 0 { b.dot = b.cursor() } comp := bs.completion var suppress = false tokens: for _, token := range bs.tokens { for _, r := range token.Val { if suppress && i < comp.end { // Silence the part that is being completed } else { b.write(r, attrForType[token.Typ]) } i += utf8.RuneLen(r) if comp != nil && comp.current != -1 && i == comp.start { // Put the current candidate and instruct text up to comp.end // to be suppressed. The cursor should be placed correctly // (i.e. right after the candidate) for _, part := range comp.candidates[comp.current].parts { attr := attrForType[comp.typ] if part.completed { attr += attrForCompleted } b.writes(part.text, attr) } suppress = true } if bs.mode == modeHistory && i == len(bs.history.prefix) { break tokens } if bs.dot == i { b.dot = b.cursor() } } } if bs.mode == modeHistory { // Put the rest of current history, position the cursor at the // end of the line, and finish writing h := bs.history b.writes(h.line[len(h.prefix):], attrForCompletedHistory) b.dot = b.cursor() } // Write rprompt padding := b.width - b.col - WcWidths(bs.rprompt) if padding >= 1 { b.newlineWhenFull = false b.writePadding(padding, "") b.writes(bs.rprompt, attrForRprompt) } // bufMode if bs.mode != modeInsert { b := newBuffer(width) bufMode = b text := "" switch bs.mode { case modeCommand: text = "Command" case modeCompletion: text = fmt.Sprintf("Completing %s", bs.line[comp.start:comp.end]) case modeNavigation: text = "Navigating" case modeHistory: text = fmt.Sprintf("History #%d", bs.history.current) } b.writes(TrimWcWidth(text, width), attrForMode) } // bufTips // TODO tips is assumed to contain no newlines. if len(bs.tips) > 0 { b := newBuffer(width) bufTips = b b.writes(TrimWcWidth(strings.Join(bs.tips, ", "), width), attrForTip) } hListing := 0 // Trim lines and determine the maximum height for bufListing switch { case height >= lines(bufLine, bufMode, bufTips): hListing = height - lines(bufLine, bufMode, bufTips) case height >= lines(bufLine, bufTips): bufMode, bufListing = nil, nil case height >= lines(bufLine): bufTips, bufMode, bufListing = nil, nil, nil case height >= 1: bufTips, bufMode, bufListing = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine+1-height, dotLine+1) default: // Broken terminal. Still try to render one line of bufLine. bufTips, bufMode, bufListing = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine, dotLine+1) } // Render bufListing under the maximum height constraint nav := bs.navigation if hListing > 0 && comp != nil || nav != nil { b := newBuffer(width) bufListing = b // Completion listing if comp != nil { // Layout candidates in multiple columns cands := comp.candidates // First decide the shape (# of rows and columns) colWidth := 0 margin := completionListingColMargin for _, cand := range cands { width := WcWidths(cand.text) if colWidth < width { colWidth = width } } cols := (b.width + margin) / (colWidth + margin) if cols == 0 { cols = 1 } lines := CeilDiv(len(cands), cols) bs.completionLines = lines // Determine the window to show. low, high := findWindow(lines, comp.current%lines, hListing) for i := low; i < high; i++ { if i > low { b.newline() } for j := 0; j < cols; j++ { k := j*lines + i if k >= len(cands) { continue } attr := cands[k].attr if k == comp.current { attr += attrForCurrentCompletion } text := cands[k].text b.writes(ForceWcWidth(text, colWidth), attr) b.writePadding(margin, "") } } } // Navigation listing if nav != nil { margin := navigationListingColMargin var ratioParent, ratioCurrent, ratioPreview int if nav.dirPreview != nil { ratioParent = 15 ratioCurrent = 40 ratioPreview = 45 } else { ratioParent = 15 ratioCurrent = 75 // Leave some space at the right side } w := width - margin*2 wParent := w * ratioParent / 100 wCurrent := w * ratioCurrent / 100 wPreview := w * ratioPreview / 100 b := renderNavColumn(nav.parent, wParent, hListing) bufListing = b bCurrent := renderNavColumn(nav.current, wCurrent, hListing) b.extendHorizontal(bCurrent, wParent, margin) if wPreview > 0 { bPreview := renderNavColumn(nav.dirPreview, wPreview, hListing) b.extendHorizontal(bPreview, wParent+wCurrent+margin, margin) } } } // Combine buffers (reusing bufLine) buf = bufLine buf.extend(bufMode) buf.extend(bufTips) buf.extend(bufListing) return w.commitBuffer(buf) }
// refresh redraws the line editor. The dot is passed as an index into text; // the corresponding position will be calculated. func (w *writer) refresh(es *editorState, fullRefresh bool) error { height, width := sys.GetWinsize(int(w.file.Fd())) mode := es.mode.Mode() var bufNoti, bufLine, bufMode, bufTips, bufListing, buf *buffer // butNoti if len(es.notifications) > 0 { bufNoti = newBuffer(width) bufNoti.writes(strings.Join(es.notifications, "\n"), "") es.notifications = nil } // bufLine b := newBuffer(width) bufLine = b b.newlineWhenFull = true b.writeStyleds(es.prompt) if b.line() == 0 && b.col*2 < b.width { b.indent = b.col } // i keeps track of number of bytes written. i := 0 // nowAt is called at every rune boundary. nowAt := func(i int) { if mode == modeCompletion && i == es.completion.begin { c := es.completion.selectedCandidate() b.writes(c.text, styleForCompleted) } if i == es.dot { b.dot = b.cursor() } } nowAt(0) tokens: for _, token := range es.tokens { for _, r := range token.Text { if mode == modeCompletion && es.completion.begin <= i && i <= es.completion.end { // Do nothing. This part is replaced by the completion candidate. } else { b.write(r, joinStyle(styleForType[token.Type], token.MoreStyle)) } i += utf8.RuneLen(r) nowAt(i) if mode == modeHistory && i == len(es.hist.prefix) { break tokens } } } if mode == modeHistory { // Put the rest of current history, position the cursor at the // end of the line, and finish writing h := es.hist b.writes(h.line[len(h.prefix):], styleForCompletedHistory) b.dot = b.cursor() } // Write rprompt padding := b.width - b.col for _, s := range es.rprompt { padding -= util.Wcswidth(s.text) } if padding >= 1 { b.newlineWhenFull = false b.writePadding(padding, "") b.writeStyleds(es.rprompt) } // bufMode bufMode = es.mode.ModeLine(width) // bufTips // TODO tips is assumed to contain no newlines. if len(es.tips) > 0 { bufTips = newBuffer(width) bufTips.writes(strings.Join(es.tips, "\n"), styleForTip) } hListing := 0 // Trim lines and determine the maximum height for bufListing // TODO come up with a UI to tell the user that something is not shown. switch { case height >= lines(bufNoti, bufLine, bufMode, bufTips): hListing = height - lines(bufLine, bufMode, bufTips) case height >= lines(bufNoti, bufLine, bufTips): bufMode = nil case height >= lines(bufNoti, bufLine): bufMode = nil if bufTips != nil { bufTips.trimToLines(0, height-lines(bufNoti, bufLine)) } case height >= lines(bufLine): bufTips, bufMode = nil, nil if bufNoti != nil { n := len(bufNoti.cells) bufNoti.trimToLines(n-(height-lines(bufLine)), n) } case height >= 1: bufNoti, bufTips, bufMode = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine+1-height, dotLine+1) default: // Broken terminal. Still try to render one line of bufLine. bufNoti, bufTips, bufMode = nil, nil, nil dotLine := bufLine.dot.line bufLine.trimToLines(dotLine, dotLine+1) } // bufListing. if hListing > 0 { if lister, ok := es.mode.(Lister); ok { bufListing = lister.List(width, hListing) } // XXX When in completion mode, we re-render the mode line, since the // scrollbar in the mode line depends on completion.lastShown which is // only known after the listing has been rendered. Since rendering the // scrollbar never adds additional lines to bufMode, we may do this // without recalculating the layout. if mode == modeCompletion { bufMode = es.mode.ModeLine(width) } } if logWriterDetail { Logger.Printf("bufLine %d, bufMode %d, bufTips %d, bufListing %d", lines(bufLine), lines(bufMode), lines(bufTips), lines(bufListing)) } // Combine buffers (reusing bufLine) buf = bufLine buf.extend(bufMode, mode == modeLocation || mode == modeHistoryListing || (mode == modeCompletion && es.completion.filtering) || (mode == modeNavigation && es.navigation.filtering)) buf.extend(bufTips, false) buf.extend(bufListing, false) return w.commitBuffer(bufNoti, buf, fullRefresh) }