debugger: start of a debugging TUI for GARY

Uses the serial debug module and currently only works with
hardware/ulx3s, probably only on my specific machine where the
USB serial port is mapped _just so_. But it does work. Very WIP
unclean code, but checkpointing because it can hex view and hexedit
correctly.
This commit is contained in:
David Anderson 2024-09-15 00:04:32 -07:00
parent eb2bb40fd3
commit 12ddbeb508
5 changed files with 481 additions and 11 deletions

190
debugger/hexview.go Normal file
View File

@ -0,0 +1,190 @@
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")
}

View File

@ -19,15 +19,13 @@ func main() {
} }
defer dbg.Close() defer dbg.Close()
fmt.Println("Writing...")
if err := dbg.Write(0x42, 123); err != nil { if err := dbg.Write(0x42, 123); err != nil {
log.Fatalf("writing to memory: %v", err) log.Fatalf("writing to memory: %v", err)
} }
v, err := dbg.Read(0x42)
if err != nil { if err := UI(dbg); err != nil {
log.Fatalf("reading from memory: %v", err) log.Fatal(err)
} }
fmt.Printf("addr 0: %02x\n", v)
} }
type Debugger struct { type Debugger struct {
@ -73,7 +71,7 @@ func (d *Debugger) Read(addr int) (byte, error) {
} }
packet := encode(addr, false, 0) packet := encode(addr, false, 0)
fmt.Printf("Writing: %02x %02x %02x %02x\n", packet[0], packet[1], packet[2], packet[3]) //fmt.Printf("Writing: %02x %02x %02x %02x\n", packet[0], packet[1], packet[2], packet[3])
if _, err := d.port.Write(packet[:]); err != nil { if _, err := d.port.Write(packet[:]); err != nil {
return 0, err return 0, err
} }
@ -97,9 +95,21 @@ func (d *Debugger) Write(addr int, val byte) error {
} }
packet := encode(addr, true, val) packet := encode(addr, true, val)
fmt.Printf("Writing: %02x %02x %02x %02x\n", packet[0], packet[1], packet[2], packet[3]) //fmt.Printf("Writing: %02x %02x %02x %02x\n", packet[0], packet[1], packet[2], packet[3])
if _, err := d.port.Write(packet[:]); err != nil { if _, err := d.port.Write(packet[:]); err != nil {
return err return err
} }
return nil return nil
} }
func (d *Debugger) Dump(startAddr int, count int) ([]byte, error) {
ret := make([]byte, count)
var err error
for i := range ret {
ret[i], err = d.Read(startAddr + i)
if err != nil {
return nil, err
}
}
return ret, nil
}

201
debugger/ui.go Normal file
View File

@ -0,0 +1,201 @@
package main
import (
"fmt"
"strings"
"unicode"
tea "github.com/charmbracelet/bubbletea"
lip "github.com/charmbracelet/lipgloss"
)
func UI(dbg *Debugger) error {
p := tea.NewProgram(initialModel(dbg)) //failed{0, 0, true}, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return err
}
return nil
}
var (
amber = lip.Color("#ffb00")
slate = lip.Color("235")
text = lip.NewStyle().Foreground(amber).Background(slate)
faintText = text.Faint(true)
box = text.Border(lip.NormalBorder(), true, true, true, true).
BorderForeground(amber).
BorderBackground(slate)
)
type msgErr struct {
err error
}
type msgUpdateStatus struct {
status string
}
type debugger struct {
width int
height int
dbg *Debugger
hex HexView
lastErr error
bottomMsg string
}
func initialModel(dbg *Debugger) debugger {
ret := debugger{
width: 0,
height: 0,
dbg: dbg,
bottomMsg: "",
}
hex := NewHexView(128*1024, ret.writeByte)
hex.AddrStyle = text
hex.ZeroStyle = faintText
hex.HexStyle = text
ret.hex = hex
return ret
}
func (m debugger) Init() tea.Cmd {
return m.dumpMemory(0x200)
}
func staticMsg(msg tea.Msg) tea.Cmd {
return func() tea.Msg {
return msg
}
}
func (m debugger) dumpMemory(count int) tea.Cmd {
var ret []tea.Cmd
for i := 0; i < count; i += 16 {
ret = append(ret, func() tea.Msg {
mem, err := m.dbg.Dump(i, 16)
if err != nil {
return msgErr{err}
}
return HexViewUpdateMem{i, mem}
})
}
return tea.Sequence(ret...)
}
func (m debugger) writeByte(addr int, val byte) tea.Cmd {
return tea.Sequence(
staticMsg(msgUpdateStatus{"Writing..."}),
func() tea.Msg {
if err := m.dbg.Write(addr, val); err != nil {
return msgUpdateStatus{fmt.Sprintf("Write failed: %v", err)}
}
return tea.BatchMsg{
staticMsg(HexViewUpdateMem{addr, []byte{val}}),
staticMsg(msgUpdateStatus{"Written!"}),
}
},
m.dumpMemory(0x200),
)
}
func (m debugger) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
case "r":
return m, m.dumpMemory(0x200)
}
case msgErr:
m.lastErr = msg.err
return m, nil
case msgUpdateStatus:
m.bottomMsg = msg.status
}
var cmd tea.Cmd
m.hex, cmd = m.hex.Update(msg)
return m, cmd
}
// func (m debugger) values(addr int) string {
// var v [4]byte
// copy(v[:], m.vram[addr:])
// var out strings.Builder
// out.WriteString(text.Render("Bin: "))
// out.WriteString(renderBytes(v[:], "%08b", "_"))
// out.WriteByte('\n')
// out.WriteString(text.Render("Hex: "))
// out.WriteString(renderBytes(v[:], "%02x", "_"))
// out.WriteByte('\n')
// out.WriteString(text.Render("Dec: "))
// out.WriteString(faintText.Render("["))
// out.WriteString(dimZero(fmt.Sprintf("%3d", v[0])))
// out.WriteString(faintText.Render("] ["))
// out.WriteString(dimZero(fmt.Sprintf("%5d", binary.BigEndian.Uint16(v[:]))))
// out.WriteString(faintText.Render("] ["))
// out.WriteString(dimZero(fmt.Sprintf("%10d", binary.BigEndian.Uint32(v[:]))))
// out.WriteString(faintText.Render("]"))
// return out.String()
// }
func (m debugger) View() string {
if m.width < 80 || m.height < 20 {
return lip.Place(m.width, m.height, lip.Center, lip.Center, "Please embiggen your terminal")
}
statusBar := text.Width(m.width).Height(1).Align(lip.Center)
topSegment := statusBar.Width(m.width / 3).Reverse(true)
hexBox := lip.NewStyle().Border(lip.NormalBorder()).BorderForeground(amber).BorderBackground(slate).Padding(1, 2)
hexHeight := m.height - 2 - hexBox.GetVerticalFrameSize()
hex := hexBox.Render(m.hex.View(hexHeight))
topLeft := text.Reverse(true).Bold(true).Render("Addr: ") + text.Reverse(true).Render(fmt.Sprintf("0x%-5x", m.hex.SelectedAddr()))
topLeft = topSegment.Padding(0, 1).Align(lip.Left).Render(topLeft)
topMid := topSegment.Bold(true).Render("GARY Debugger")
topRight := topSegment.Render("")
topStatus := lip.JoinHorizontal(lip.Top, topLeft, topMid, topRight)
bottomStatus := statusBar.Render(m.bottomMsg)
top := lip.JoinVertical(lip.Center, topStatus, hex, bottomStatus)
return top
}
func renderByte(b byte, format string) string {
ret := fmt.Sprintf(format, b)
if b == 0 {
return faintText.Render(ret)
}
return text.Render(ret)
}
func renderBytes(bs []byte, format string, sep string) string {
var s strings.Builder
for i, b := range bs {
s.WriteString(renderByte(b, format))
if i < len(bs)-1 {
s.WriteString(faintText.Render(sep))
}
}
return s.String()
}
func dimZero(s string) string {
allZero := true
for _, r := range s {
if !unicode.IsSpace(r) && r != '0' {
allZero = false
break
}
}
if allZero {
return faintText.Render(s)
}
return text.Render(s)
}

32
go.mod
View File

@ -1,9 +1,33 @@
module git.sentinel65x.com/dave/gary module git.sentinel65x.com/dave/gary
go 1.22.6 go 1.23
toolchain go1.23.0
require ( require (
github.com/creack/goselect v0.1.2 // indirect github.com/charmbracelet/bubbles v0.20.0
go.bug.st/serial v1.6.2 // indirect github.com/charmbracelet/bubbletea v1.1.1
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect github.com/charmbracelet/lipgloss v0.13.0
github.com/creachadair/mds v0.21.2
go.bug.st/serial v1.6.2
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.3.8 // indirect
) )

45
go.sum
View File

@ -1,6 +1,51 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/creachadair/mds v0.21.2 h1:D5130qi/kqmu+gGUQyDNOhrocGQp075ziTCgttxhh3k=
github.com/creachadair/mds v0.21.2/go.mod h1:1ltMWZd9yXhaHEoZwBialMaviWVUpRPvMwVP7saFAzM=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=