src

Go monorepo.
Log | Files | Refs

render.go (2632B)


      1 package editor
      2 
      3 import (
      4 	"bytes"
      5 	"fmt"
      6 	"path"
      7 	"strings"
      8 
      9 	"github.com/mattn/go-runewidth"
     10 
     11 	"code.dwrz.net/src/pkg/color"
     12 	"code.dwrz.net/src/pkg/terminal"
     13 )
     14 
     15 func (e *Editor) render(msg *Message) error {
     16 	size, err := e.terminal.Size()
     17 	if err != nil {
     18 		return fmt.Errorf("failed to get terminal size: %w", err)
     19 	}
     20 
     21 	var (
     22 		buf    bytes.Buffer
     23 		cursor = e.active.Cursor()
     24 		bars   = 2
     25 		height = int(size.Rows) - bars
     26 		width  = int(size.Columns)
     27 		output = e.active.Render(height, width)
     28 	)
     29 
     30 	// Move the cursor to the top left.
     31 	buf.Write([]byte(terminal.CursorHide))
     32 	buf.Write([]byte(terminal.CursorTopLeft))
     33 
     34 	// Print each line.
     35 	for _, line := range output.Lines {
     36 		buf.Write([]byte(terminal.EraseLine))
     37 		buf.WriteString(line)
     38 		buf.WriteString("\r\n")
     39 	}
     40 
     41 	// Draw the status bar.
     42 	buf.Write([]byte(terminal.EraseLine))
     43 	buf.WriteString(e.statusBar(width, cursor.Line(), cursor.Glyph()))
     44 	buf.WriteString("\r\n")
     45 
     46 	// Draw the message bar.
     47 	buf.Write([]byte(terminal.EraseLine))
     48 	buf.WriteString(e.messageBar(msg, width))
     49 
     50 	// Set the cursor.
     51 	buf.Write([]byte(
     52 		fmt.Sprintf(terminal.SetCursorFmt, output.Line, output.Glyph)),
     53 	)
     54 	buf.Write([]byte(terminal.CursorShow))
     55 
     56 	e.out.Write(buf.Bytes())
     57 
     58 	return nil
     59 }
     60 
     61 // TODO: show a character cursor, not the terminal cursor.
     62 func (e *Editor) statusBar(width, y, x int) string {
     63 	var bar strings.Builder
     64 
     65 	bar.WriteString(color.Inverse)
     66 
     67 	// Icon
     68 	icon := "文 "
     69 	bar.WriteString(icon)
     70 	width -= runewidth.StringWidth(icon)
     71 
     72 	// Calculate the length of the cursor, so we can determine how much
     73 	// space we have left for the name of the buffer.
     74 	cursor := fmt.Sprintf(" %d:%d", y, x)
     75 	width -= runewidth.StringWidth(cursor)
     76 
     77 	// Filename.
     78 	// TODO: handle long filenames (shorten filepath).
     79 	name := path.Base(e.active.Name())
     80 	nw := runewidth.StringWidth(name)
     81 	if nw <= width {
     82 		bar.WriteString(name)
     83 		width -= nw
     84 	} else {
     85 		for _, r := range name {
     86 			rw := runewidth.RuneWidth(r)
     87 			if width-rw >= 0 {
     88 				bar.WriteRune(r)
     89 				width -= rw
     90 			} else {
     91 				break
     92 			}
     93 		}
     94 	}
     95 
     96 	// Add empty spaces to the end of the line.
     97 	for i := width; i >= 0; i-- {
     98 		bar.WriteRune(' ')
     99 	}
    100 
    101 	// Cursor
    102 	bar.WriteString(cursor)
    103 
    104 	bar.WriteString(color.Reset)
    105 
    106 	return bar.String()
    107 }
    108 
    109 // TODO: handle messages that are too long for one line.
    110 // TODO: should status bar render independently?
    111 func (e *Editor) messageBar(msg *Message, width int) string {
    112 	switch {
    113 	case msg == nil:
    114 		return fmt.Sprintf("%*s", width, " ")
    115 	case len(msg.Text) > width:
    116 		return fmt.Sprintf("%*s", width, msg.Text)
    117 	default:
    118 		return fmt.Sprintf("%s %*s", msg.Text, width-len(msg.Text), " ")
    119 	}
    120 
    121 }