191 lines
4.1 KiB
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")
|
|
}
|