gary/debugger/hexview.go

191 lines
4.1 KiB
Go

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")
}