package main import ( "fmt" "math" "strings" "unicode" tea "github.com/charmbracelet/bubbletea" lip "github.com/charmbracelet/lipgloss" "github.com/creachadair/mds/slice" ) type HexViewUpdateMem struct { addr int bytes []byte } type HexView struct { AddrStyle lip.Style ZeroStyle lip.Style HexStyle lip.Style write func(int, byte) tea.Cmd firstAddr int // top of viewport bytes []byte selectedAddr int editing bool editNibble int // 0 or 1 newByte byte } func NewHexView(size int, writeByte func(addr int, val byte) tea.Cmd) HexView { st := lip.NewStyle() ret := HexView{ AddrStyle: st, ZeroStyle: st, HexStyle: st, write: writeByte, bytes: make([]byte, size), } return ret } func (m HexView) Width() int { hexdumpWidth := 16*3 + 2 // including spaces between dquads addrWidth := m.addrNibbles() // Hex nibbles needed to represent all addrs return addrWidth + 4 + hexdumpWidth } func (m HexView) SelectedAddr() int { return m.selectedAddr } func (m HexView) addrFormat() string { return fmt.Sprintf("%%%dx", m.addrNibbles()) } func (m HexView) addrNibbles() int { return int(math.Ceil(math.Log2(float64(len(m.bytes)-1)))) / 4 } func (m HexView) Update(msg tea.Msg) (HexView, tea.Cmd) { if m.editing { return m.updateEditMode(msg) } else { return m.updateViewMode(msg) } } func (m HexView) updateEditMode(msg tea.Msg) (HexView, tea.Cmd) { switch msg := msg.(type) { case HexViewUpdateMem: copy(m.bytes[msg.addr:], msg.bytes) case tea.KeyMsg: switch msg.Type { case tea.KeyLeft: if m.editNibble == 1 { m.editNibble = 0 } case tea.KeyRight: if m.editNibble == 0 { m.editNibble = 1 } case tea.KeyEsc: m.editing = false case tea.KeyEnter: m.editing = false return m, m.write(m.selectedAddr, m.newByte) case tea.KeyRunes: if unicode.In(msg.Runes[0], unicode.ASCII_Hex_Digit) { nibble := hexToNibble(msg.Runes[0]) if m.editNibble == 0 { m.newByte = (nibble << 4) + (m.newByte & 0xF) m.editNibble++ } else { m.newByte = (m.newByte & 0xF0) + nibble } } } } return m, nil } func hexToNibble(r rune) byte { switch { case r >= '0' && r <= '9': return byte(r - '0') case r >= 'a' && r <= 'f': return byte(r - 'a' + 10) case r >= 'A' && r <= 'F': return byte(r - 'A' + 10) } panic("invalid hex rune") } func (m HexView) updateViewMode(msg tea.Msg) (HexView, tea.Cmd) { switch msg := msg.(type) { case HexViewUpdateMem: copy(m.bytes[msg.addr:], msg.bytes) case tea.KeyMsg: switch msg.String() { case "up": if m.selectedAddr >= 16 { m.selectedAddr -= 16 } case "down": if m.selectedAddr < len(m.bytes)-16 { m.selectedAddr += 16 } case "left": if m.selectedAddr > 0 { m.selectedAddr-- } case "right": if m.selectedAddr < len(m.bytes)-1 { m.selectedAddr++ } case "pgdown": // TODO case "pgup": // TODO case "w": m.editing = true m.editNibble = 0 m.newByte = m.bytes[m.selectedAddr] } } return m, nil } func (m HexView) View(height int) string { maxLen := 16 * height endAddr := min(len(m.bytes), m.firstAddr+maxLen) var ret strings.Builder addrFormat := m.addrFormat() for line, bytes := range slice.Chunks(m.bytes[m.firstAddr:endAddr], 16) { lineAddr := m.firstAddr + (16 * line) ret.WriteString(m.AddrStyle.Render(fmt.Sprintf(addrFormat, lineAddr))) ret.WriteString(" ") for i, b := range bytes { byteAddr := lineAddr + i if i == 8 { ret.WriteString(" ") } else if i != 0 { ret.WriteByte(' ') } if m.editing && byteAddr == m.selectedAddr { st := m.HexStyle.Underline(true) s1, s2 := st.Reverse(true), st if m.editNibble == 1 { s1, s2 = s2, s1 } b = m.newByte ret.WriteString(s1.Render(fmt.Sprintf("%01x", b>>4))) ret.WriteString(s2.Render(fmt.Sprintf("%01x", b&0xF))) } else { st := m.HexStyle if m.selectedAddr == byteAddr { st = m.HexStyle.Reverse(true) } else if b == 0 { st = m.ZeroStyle } ret.WriteString(st.Render(fmt.Sprintf("%02x", b))) } } ret.WriteByte('\n') } return strings.TrimRight(ret.String(), "\n") }