From 12ddbeb508a1865913e9298487147db013cb33fe Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sun, 15 Sep 2024 00:04:32 -0700 Subject: [PATCH] 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. --- debugger/hexview.go | 190 +++++++++++++++++++++++++++++++++++++++++ debugger/main.go | 24 ++++-- debugger/ui.go | 201 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 32 ++++++- go.sum | 45 ++++++++++ 5 files changed, 481 insertions(+), 11 deletions(-) create mode 100644 debugger/hexview.go create mode 100644 debugger/ui.go diff --git a/debugger/hexview.go b/debugger/hexview.go new file mode 100644 index 0000000..83e9ed9 --- /dev/null +++ b/debugger/hexview.go @@ -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") +} diff --git a/debugger/main.go b/debugger/main.go index d07ad86..3ca8983 100644 --- a/debugger/main.go +++ b/debugger/main.go @@ -19,15 +19,13 @@ func main() { } defer dbg.Close() - fmt.Println("Writing...") if err := dbg.Write(0x42, 123); err != nil { log.Fatalf("writing to memory: %v", err) } - v, err := dbg.Read(0x42) - if err != nil { - log.Fatalf("reading from memory: %v", err) + + if err := UI(dbg); err != nil { + log.Fatal(err) } - fmt.Printf("addr 0: %02x\n", v) } type Debugger struct { @@ -73,7 +71,7 @@ func (d *Debugger) Read(addr int) (byte, error) { } 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 { return 0, err } @@ -97,9 +95,21 @@ func (d *Debugger) Write(addr int, val byte) error { } 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 { return err } 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 +} diff --git a/debugger/ui.go b/debugger/ui.go new file mode 100644 index 0000000..ddb8691 --- /dev/null +++ b/debugger/ui.go @@ -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) +} diff --git a/go.mod b/go.mod index 222199f..e27ebfa 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,33 @@ module git.sentinel65x.com/dave/gary -go 1.22.6 +go 1.23 + +toolchain go1.23.0 require ( - github.com/creack/goselect v0.1.2 // indirect - go.bug.st/serial v1.6.2 // indirect - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.1 + 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 ) diff --git a/go.sum b/go.sum index 2f35f6f..49995f2 100644 --- a/go.sum +++ b/go.sum @@ -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/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/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/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=