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 }