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(height int) 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") }