package canvas import ( "fmt" "path" "github.com/mattn/go-runewidth" "code.chimeric.al/dwrz/wen/internal/editor/buffer" "code.chimeric.al/dwrz/wen/internal/editor/prompt" "code.chimeric.al/dwrz/wen/pkg/color" "code.chimeric.al/dwrz/wen/pkg/terminal" ) // Bars is the number of terminal rows reserved at the bottom of the screen // for the status bar and message bar. const Bars = 2 func (c *Canvas) Render(b *buffer.Buffer, prompt *prompt.Prompt) error { c.mu.Lock() defer c.mu.Unlock() size, err := c.terminal.Size() if err != nil { return fmt.Errorf("failed to get terminal size: %w", err) } var ( cursor = b.Cursor() height = int(size.Rows) - Bars width = int(size.Columns) output = b.Render(height, width) ) // Move the cursor to the top left. c.buf.WriteString(terminal.CursorHide) c.buf.WriteString(terminal.CursorTopLeft) // Print each line. for _, line := range output.Lines { c.buf.WriteString(terminal.EraseLine) c.buf.WriteString(line) c.buf.WriteString("\r\n") } // Draw the status bar. c.statusBar(b, width, cursor.Line(), cursor.Glyph()) // Draw the message bar. c.messageBar(prompt, width) // Set the cursor. if prompt.IsPrompt() { c.buf.WriteString(fmt.Sprintf( terminal.SetCursorFmt, size.Rows, runewidth.StringWidth(prompt.Text())+prompt.Cursor()+2, )) } else { c.buf.WriteString(fmt.Sprintf( terminal.SetCursorFmt, output.Line, output.Glyph, )) } c.buf.WriteString(terminal.CursorShow) c.out.Write(c.buf.Bytes()) c.buf.Reset() return nil } func (c *Canvas) statusBar(b *buffer.Buffer, width, y, x int) { c.buf.WriteString(terminal.EraseLine) c.buf.WriteString(color.Inverse) // Icon icon := "文 " c.buf.WriteString(icon) width -= runewidth.StringWidth(icon) // Calculate the length of the cursor, so we can determine how much // space we have left for the name of the buffer. cursor := fmt.Sprintf(" %d:%d", y, x) width -= runewidth.StringWidth(cursor) // Filename. // TODO: handle long filenames (shorten filepath). name := path.Base(b.Name()) nw := runewidth.StringWidth(name) if nw <= width { c.buf.WriteString(name) width -= nw } else { for _, r := range name { rw := runewidth.RuneWidth(r) if width-rw >= 0 { c.buf.WriteRune(r) width -= rw } else { break } } } // Add empty spaces to the end of the line. for i := width; i > 0; i-- { c.buf.WriteRune(' ') } // Cursor c.buf.WriteString(cursor) c.buf.WriteString(color.Reset) c.buf.WriteString("\r\n") } // TODO: handle messages that are too long for one line. func (c *Canvas) messageBar(p *prompt.Prompt, width int) { c.buf.WriteString(terminal.EraseLine) var ( text = p.Text() line = p.Line(0, width) // TODO: handle offset. output string ) switch { case text == "" && line == "": output = "" case line == "": output = text default: output = fmt.Sprintf("%s %s", text, line) } for _, r := range output { rw := runewidth.RuneWidth(r) if width-rw < 0 { break } c.buf.WriteRune(r) width -= rw } for i := width; i > 0; i-- { c.buf.WriteRune(' ') } }