Example #1
0
// 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
}
Example #2
0
// 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
}
Example #3
0
// 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)
}
Example #4
0
// 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)
}
Example #5
0
// 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)
}