star/bed/tui/tui_window.go
2025-04-26 19:33:14 +08:00

226 lines
6.0 KiB
Go

package tui
import (
"cmp"
"fmt"
"github.com/gdamore/tcell"
"b612.me/apps/b612/bed/mode"
"b612.me/apps/b612/bed/state"
)
type tuiWindow struct {
region region
screen tcell.Screen
}
func (ui *tuiWindow) getTextDrawer() *textDrawer {
return &textDrawer{region: ui.region, screen: ui.screen}
}
func (ui *tuiWindow) setCursor(line, offset int) {
ui.screen.ShowCursor(ui.region.left+offset, ui.region.top+line)
}
func offsetStyleWidth(s *state.WindowState) int {
threshold := int64(0xfffff)
for i := range 10 {
if s.Length <= threshold {
return 6 + i
}
threshold = (threshold << 4) | 0x0f
}
return 16
}
func (ui *tuiWindow) drawWindow(s *state.WindowState, active bool) {
height, width := ui.region.height-2, s.Width
cursorPos := int(s.Cursor - s.Offset)
cursorLine := cursorPos / width
offsetStyleWidth := offsetStyleWidth(s)
eis := s.EditedIndices
for 0 < len(eis) && eis[1] <= s.Offset {
eis = eis[2:]
}
editedColor := tcell.ColorLightSeaGreen
d := ui.getTextDrawer()
var k int
for i := range height {
d.addTop(1).setLeft(0).setOffset(0)
d.setString(
fmt.Sprintf(" %0*x", offsetStyleWidth, s.Offset+int64(i*width)),
tcell.StyleDefault.Bold(i == cursorLine),
)
d.setLeft(offsetStyleWidth + 3)
for j := range width {
b, style := byte(0), tcell.StyleDefault
if s.Pending && i*width+j == cursorPos {
b, style = s.PendingByte, tcell.StyleDefault.Foreground(editedColor)
if s.Mode != mode.Replace {
k--
}
} else if k >= s.Size {
if k == cursorPos {
d.setOffset(3*j+1).setByte(' ', tcell.StyleDefault.Underline(!active || s.FocusText))
d.setOffset(3*width+j+3).setByte(' ', tcell.StyleDefault.Underline(!active || !s.FocusText))
}
k++
continue
} else {
b = s.Bytes[k]
pos := int64(k) + s.Offset
if 0 < len(eis) && eis[0] <= pos && pos < eis[1] {
style = tcell.StyleDefault.Foreground(editedColor)
} else if 0 < len(eis) && eis[1] <= pos {
eis = eis[2:]
}
if s.VisualStart >= 0 && s.Cursor < s.Length &&
(s.VisualStart <= pos && pos <= s.Cursor ||
s.Cursor <= pos && pos <= s.VisualStart) {
style = style.Underline(true)
}
}
style1, style2 := style, style
if i*width+j == cursorPos {
style1 = style1.Reverse(active && !s.FocusText).Bold(
!active || s.FocusText).Underline(!active || s.FocusText)
style2 = style2.Reverse(active && s.FocusText).Bold(
!active || !s.FocusText).Underline(!active || !s.FocusText)
}
d.setOffset(3*j+1).setByte(hex[b>>4], style1)
d.setOffset(3*j+2).setByte(hex[b&0x0f], style1)
d.setOffset(3*width+j+3).setByte(prettyByte(b), style2)
k++
}
d.setOffset(-2).setByte(' ', tcell.StyleDefault)
d.setOffset(-1).setByte('|', tcell.StyleDefault)
d.setOffset(0).setByte(' ', tcell.StyleDefault)
d.addLeft(3*width).setByte(' ', tcell.StyleDefault)
d.setOffset(1).setByte('|', tcell.StyleDefault)
d.setOffset(2).setByte(' ', tcell.StyleDefault)
}
i := int(s.Cursor % int64(width))
if active {
if s.FocusText {
ui.setCursor(cursorLine+1, 3*width+i+6+offsetStyleWidth)
} else if s.Pending {
ui.setCursor(cursorLine+1, 3*i+5+offsetStyleWidth)
} else {
ui.setCursor(cursorLine+1, 3*i+4+offsetStyleWidth)
}
}
ui.drawHeader(s, offsetStyleWidth)
ui.drawScrollBar(s, height, 4*width+7+offsetStyleWidth)
ui.drawFooter(s, offsetStyleWidth)
}
const hex = "0123456789abcdef"
func (ui *tuiWindow) drawHeader(s *state.WindowState, offsetStyleWidth int) {
style := tcell.StyleDefault.Underline(true)
d := ui.getTextDrawer().setLeft(-1)
cursor := int(s.Cursor % int64(s.Width))
for range offsetStyleWidth + 2 {
d.addLeft(1).setByte(' ', style)
}
d.addLeft(1).setByte('|', style)
for i := range s.Width {
d.addLeft(1).setByte(' ', style)
d.addLeft(1).setByte(" 123456789abcdef"[i>>4], style.Bold(cursor == i))
d.addLeft(1).setByte(hex[i&0x0f], style.Bold(cursor == i))
}
d.addLeft(1).setByte(' ', style)
d.addLeft(1).setByte('|', style)
for range s.Width + 3 {
d.addLeft(1).setByte(' ', style)
}
}
func (ui *tuiWindow) drawScrollBar(s *state.WindowState, height, left int) {
stateSize := s.Size
if s.Cursor+1 == s.Length && s.Cursor == s.Offset+int64(s.Size) {
stateSize++
}
total := int64((stateSize + s.Width - 1) / s.Width)
length := max((s.Length+int64(s.Width)-1)/int64(s.Width), 1)
size := max(total*total/length, 1)
pad := (total*total + length - length*size - 1) / max(total-size+1, 1)
top := (s.Offset / int64(s.Width) * total) / (length - pad)
d := ui.getTextDrawer().setLeft(left)
for i := range height {
var b byte
if int(top) <= i && i < int(top+size) {
b = '#'
} else {
b = '|'
}
d.addTop(1).setByte(b, tcell.StyleDefault)
}
}
func (ui *tuiWindow) drawFooter(s *state.WindowState, offsetStyleWidth int) {
var modified string
if s.Modified {
modified = " : +"
}
b := s.Bytes[int(s.Cursor-s.Offset)]
left := fmt.Sprintf(" %s%s%s : 0x%02x : '%s'",
prettyMode(s.Mode), cmp.Or(s.Name, "[No name]"), modified, b, prettyRune(b))
right := fmt.Sprintf("%[1]d/%[2]d : 0x%0[3]*[1]x/0x%0[3]*[2]x : %.2[4]f%% ",
s.Cursor, s.Length, offsetStyleWidth, float64(s.Cursor*100)/float64(max(s.Length, 1)))
line := fmt.Sprintf("%s %*s", left, max(ui.region.width-len(left)-2, 0), right)
ui.getTextDrawer().setTop(ui.region.height-1).setString(line, tcell.StyleDefault.Reverse(true))
}
func prettyByte(b byte) byte {
switch {
case 0x20 <= b && b < 0x7f:
return b
default:
return 0x2e
}
}
func prettyRune(b byte) string {
switch b {
case 0x07:
return "\\a"
case 0x08:
return "\\b"
case 0x09:
return "\\t"
case 0x0a:
return "\\n"
case 0x0b:
return "\\v"
case 0x0c:
return "\\f"
case 0x0d:
return "\\r"
case 0x27:
return "\\'"
default:
if b < 0x20 {
return fmt.Sprintf("\\x%02x", b)
} else if b < 0x7f {
return string(rune(b))
} else {
return fmt.Sprintf("\\u%04x", b)
}
}
}
func prettyMode(m mode.Mode) string {
switch m {
case mode.Insert:
return "[INSERT] "
case mode.Replace:
return "[REPLACE] "
case mode.Visual:
return "[VISUAL] "
default:
return ""
}
}