gary/debugger/hexview.go

221 lines
4.9 KiB
Go
Raw Normal View History

package main
import (
"fmt"
"math"
"strings"
"unicode"
"git.sentinel65x.com/dave/gary/debugger/memory"
tea "github.com/charmbracelet/bubbletea"
lip "github.com/charmbracelet/lipgloss"
"github.com/creachadair/mds/slice"
)
type HexViewSetHeight struct {
Height int
}
type HexView struct {
AddrStyle lip.Style
ZeroStyle lip.Style
HexStyle lip.Style
mem *memory.Memory
commit func() tea.Cmd
height int
firstAddr int // top of viewport
selectedAddr int
editing bool
editNibble int // 0 or 1
}
func NewHexView(mem *memory.Memory, commit func() tea.Cmd) HexView {
st := lip.NewStyle()
ret := HexView{
AddrStyle: st,
ZeroStyle: st,
HexStyle: st,
mem: mem,
commit: commit,
}
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) moveSelection(delta int) bool {
newAddr := m.selectedAddr + delta
if newAddr >= 0 && newAddr < m.mem.Len() {
m.selectedAddr = newAddr
start, end := m.VisibleBytes()
newFirstLine := newAddr / 16
if newAddr < start {
newFirstLine = newFirstLine - (m.height * 4 / 5)
newFirstLine = max(newFirstLine, 0)
m.firstAddr = newFirstLine * 16
} else if newAddr >= end {
newFirstLine = newFirstLine - (m.height * 1 / 5)
newFirstLine = min(newFirstLine, (m.mem.Len()/16)-m.height)
m.firstAddr = newFirstLine * 16
}
return true
}
return false
}
func (m HexView) VisibleBytes() (start, end int) {
return m.firstAddr, m.firstAddr + (m.height * 16)
}
func (m HexView) addrFormat() string {
return fmt.Sprintf("%%%dx", m.addrNibbles())
}
func (m HexView) addrNibbles() int {
return int(math.Ceil(math.Log2(float64(m.mem.Len())*8))) / 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 HexViewSetHeight:
m.height = msg.Height
case tea.KeyMsg:
switch msg.Type {
case tea.KeyLeft:
if m.editNibble == 1 {
m.editNibble = 0
} else if m.moveSelection(-1) {
m.editNibble = 1
}
case tea.KeyRight:
if m.editNibble == 0 {
m.editNibble = 1
} else if m.moveSelection(1) {
m.editNibble = 0
}
case tea.KeyUp:
m.moveSelection(-16)
case tea.KeyDown:
m.moveSelection(16)
case tea.KeyEsc:
m.editing = false
case tea.KeyEnter:
m.editing = false
return m, m.commit()
case tea.KeyRunes:
if unicode.In(msg.Runes[0], unicode.ASCII_Hex_Digit) {
nibble := hexToNibble(msg.Runes[0])
prev := m.mem.At(m.selectedAddr).Value
if m.editNibble == 0 {
m.mem.Write(m.selectedAddr, (nibble<<4)+(prev&0xF))
m.editNibble++
} else {
m.mem.Write(m.selectedAddr, (prev&0xF0)+nibble)
if m.moveSelection(1) {
m.editNibble = 0
}
}
}
}
}
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 HexViewSetHeight:
m.height = msg.Height
case tea.KeyMsg:
switch msg.String() {
case "up":
m.moveSelection(-16)
case "down":
m.moveSelection(16)
case "left":
m.moveSelection(-1)
case "right":
m.moveSelection(1)
case "pgdown":
m.moveSelection(m.height * 16)
case "pgup":
m.moveSelection(-m.height * 16)
case "w":
m.editing = true
m.editNibble = 0
}
}
return m, nil
}
func (m HexView) View() string {
startAddr, endAddr := m.VisibleBytes()
bytes := m.mem.Slice(startAddr, endAddr)
var ret strings.Builder
addrFormat := m.addrFormat()
for line, bytes := range slice.Chunks(bytes, 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.Bold(true)
s1, s2 := st.Reverse(true), st
if m.editNibble == 1 {
s1, s2 = s2, s1
}
ret.WriteString(s1.Render(fmt.Sprintf("%01x", b.Value>>4)))
ret.WriteString(s2.Render(fmt.Sprintf("%01x", b.Value&0xF)))
} else {
st := m.HexStyle
if m.selectedAddr == byteAddr {
st = m.HexStyle.Reverse(true)
} else if !b.Valid || b.Value == 0 {
st = m.ZeroStyle
}
if b.Changed {
st = st.Underline(true).Bold(true)
}
ret.WriteString(st.Render(b.Hex()))
}
}
ret.WriteByte('\n')
}
return strings.TrimRight(ret.String(), "\n")
}