221 lines
4.9 KiB
Go
221 lines
4.9 KiB
Go
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")
|
|
}
|