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