Initial, working but only with viewport height adjusted a little bit
This commit is contained in:
1
.direnv/flake-profile
Symbolic link
1
.direnv/flake-profile
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
flake-profile-3-link
|
||||||
1
.direnv/flake-profile-3-link
Symbolic link
1
.direnv/flake-profile-3-link
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/nix/store/x4pks9q49xlz3by0czlhc7cr3624k6yf-nix-shell-env
|
||||||
56
cmd/pybug/main.go
Normal file
56
cmd/pybug/main.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "fmt"
|
||||||
|
// "log/slog"
|
||||||
|
// "time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
b "git.pablu.de/pablu/pybug/internal/bridge"
|
||||||
|
"git.pablu.de/pablu/pybug/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||||
|
|
||||||
|
//
|
||||||
|
// fmt.Println("Started bridge")
|
||||||
|
//
|
||||||
|
// err = bridge.Breakpoint("test.py", 5)
|
||||||
|
// bridge.OnBreakpoint("test.py", 5, func() {
|
||||||
|
// locals, err := bridge.Locals()
|
||||||
|
// if err != nil {
|
||||||
|
// slog.Error("Encountered error on callback", "error", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for key, val := range locals {
|
||||||
|
// slog.Info("found local variable", "key", key, "value", val)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// bridge.Continue()
|
||||||
|
//
|
||||||
|
// time.Sleep(5 * time.Second)
|
||||||
|
//
|
||||||
|
// bridge.Continue()
|
||||||
|
//
|
||||||
|
// err = bridge.Wait()
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelError)
|
||||||
|
|
||||||
|
bridge := b.NewBridge("test.py")
|
||||||
|
err := bridge.Start()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ui.Run(bridge)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
flake.lock
generated
Normal file
59
flake.lock
generated
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774273680,
|
||||||
|
"narHash": "sha256-a++tZ1RQsDb1I0NHrFwdGuRlR5TORvCEUksM459wKUA=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "fdc7b8f7b30fdbedec91b71ed82f36e1637483ed",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
22
flake.nix
Normal file
22
flake.nix
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
python3
|
||||||
|
basedpyright
|
||||||
|
isort
|
||||||
|
black
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
34
go.mod
Normal file
34
go.mod
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module git.pablu.de/pablu/pybug
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.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.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
60
go.sum
Normal file
60
go.sum
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
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 v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
273
internal/bridge/client.go
Normal file
273
internal/bridge/client.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bridge struct {
|
||||||
|
stdin io.Writer
|
||||||
|
stdout *bufio.Reader
|
||||||
|
|
||||||
|
cmd *exec.Cmd
|
||||||
|
|
||||||
|
input chan string
|
||||||
|
|
||||||
|
outputLock *sync.RWMutex
|
||||||
|
output []chan string
|
||||||
|
|
||||||
|
registry map[string]chan string
|
||||||
|
registryLock *sync.RWMutex
|
||||||
|
|
||||||
|
breakpoints map[string][]int
|
||||||
|
|
||||||
|
callbacksLock *sync.RWMutex
|
||||||
|
callbacks map[string]map[int]func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBridge(path string) *Bridge {
|
||||||
|
cmd := exec.Command(
|
||||||
|
"python",
|
||||||
|
"-u",
|
||||||
|
"python/pybug_runtime.py",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
return &Bridge{
|
||||||
|
cmd: cmd,
|
||||||
|
input: make(chan string),
|
||||||
|
registry: make(map[string]chan string),
|
||||||
|
registryLock: &sync.RWMutex{},
|
||||||
|
breakpoints: make(map[string][]int),
|
||||||
|
|
||||||
|
callbacksLock: &sync.RWMutex{},
|
||||||
|
callbacks: make(map[string]map[int]func()),
|
||||||
|
|
||||||
|
output: make([]chan string, 0),
|
||||||
|
outputLock: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Start() error {
|
||||||
|
var err error
|
||||||
|
b.stdin, err = b.cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// b.cmd.Stdout = os.Stdout
|
||||||
|
|
||||||
|
reader, err := b.cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.stdout = bufio.NewReader(reader)
|
||||||
|
|
||||||
|
err = b.cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go b.readLoop()
|
||||||
|
go b.writeLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Subscribe() chan string {
|
||||||
|
b.outputLock.Lock()
|
||||||
|
defer b.outputLock.Unlock()
|
||||||
|
|
||||||
|
c := make(chan string)
|
||||||
|
b.output = append(b.output, c)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Locals() (map[string]any, error) {
|
||||||
|
requestId, cmd := makeCommand(LocalsCommand, map[string]any{})
|
||||||
|
|
||||||
|
c := b.sendCommand(requestId, cmd)
|
||||||
|
|
||||||
|
obj := <-c
|
||||||
|
|
||||||
|
var m map[string]any
|
||||||
|
err := json.Unmarshal([]byte(obj), &m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars, ok := m["vars"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("could not extract vars from response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return vars, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Breakpoint(file string, line int) error {
|
||||||
|
// Check if breakpoint already exists here
|
||||||
|
|
||||||
|
requestId, cmd := makeCommand(BreakCommand, map[string]any{
|
||||||
|
"file": file,
|
||||||
|
"line": line,
|
||||||
|
})
|
||||||
|
|
||||||
|
c := b.sendCommand(requestId, cmd)
|
||||||
|
|
||||||
|
obj := <-c
|
||||||
|
|
||||||
|
var m map[string]any
|
||||||
|
err := json.Unmarshal([]byte(obj), &m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m["status"] != "ok" {
|
||||||
|
return fmt.Errorf("error occured on break, err: %s", m["error"])
|
||||||
|
}
|
||||||
|
|
||||||
|
b.breakpoints[file] = append(b.breakpoints[file], line)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Continue() {
|
||||||
|
_, cmd := makeCommand(ContinueCommand, map[string]any{})
|
||||||
|
|
||||||
|
b.sendCommandNoResponse(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) sendCommandNoResponse(command string) {
|
||||||
|
b.input <- command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) sendCommand(requestId string, command string) chan string {
|
||||||
|
b.registryLock.Lock()
|
||||||
|
defer b.registryLock.Unlock()
|
||||||
|
|
||||||
|
channel := make(chan string)
|
||||||
|
|
||||||
|
b.registry[requestId] = channel
|
||||||
|
b.input <- command
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) writeLoop() {
|
||||||
|
slog.Debug("started writeLoop")
|
||||||
|
defer slog.Debug("writeLoop exited")
|
||||||
|
for {
|
||||||
|
cmd := <-b.input
|
||||||
|
|
||||||
|
slog.Info("Received command", "cmd", cmd)
|
||||||
|
|
||||||
|
_, err := b.stdin.Write([]byte(cmd + "\n"))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error occured while writing to stdin", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Command written", "cmd", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) readLoop() {
|
||||||
|
slog.Debug("started readLoop")
|
||||||
|
defer slog.Debug("readLoop exited")
|
||||||
|
for {
|
||||||
|
slog.Debug("reading string from stdout waiting for newline")
|
||||||
|
line, err := b.stdout.ReadString('\n')
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
slog.Error("Error occured while reading from stdout", "error", err)
|
||||||
|
continue
|
||||||
|
} else if errors.Is(err, io.EOF) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg map[string]any
|
||||||
|
err = json.Unmarshal([]byte(line), &msg)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("read line from stdout", "line", line)
|
||||||
|
|
||||||
|
b.outputLock.RLock()
|
||||||
|
for _, c := range b.output {
|
||||||
|
c <- line
|
||||||
|
}
|
||||||
|
b.outputLock.RUnlock()
|
||||||
|
|
||||||
|
} else if requestId, ok := msg["request_id"].(string); ok {
|
||||||
|
b.registryLock.RLock()
|
||||||
|
c, ok := b.registry[requestId]
|
||||||
|
b.registryLock.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
slog.Error("Could not find requestId in registry", "requestId", requestId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c <- line
|
||||||
|
} else {
|
||||||
|
// TODO: set to stopped
|
||||||
|
if event, ok := msg["event"]; !ok || event != "stopped" {
|
||||||
|
slog.Warn("received unkown event", "msg", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := msg["file"].(string)
|
||||||
|
line, ok := toInt(msg["line"])
|
||||||
|
if !ok {
|
||||||
|
slog.Error("could not convert line to int", "line", msg["line"])
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("received stopped event")
|
||||||
|
b.callbacksLock.RLock()
|
||||||
|
if callback, ok := b.callbacks[file][line]; ok {
|
||||||
|
slog.Info("found callback, now running", "file", file, "line", line)
|
||||||
|
go callback()
|
||||||
|
}
|
||||||
|
b.callbacksLock.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(v any) (int, bool) {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case int:
|
||||||
|
return x, true
|
||||||
|
case int8:
|
||||||
|
return int(x), true
|
||||||
|
case int16:
|
||||||
|
return int(x), true
|
||||||
|
case int32:
|
||||||
|
return int(x), true
|
||||||
|
case int64:
|
||||||
|
return int(x), true
|
||||||
|
case float32:
|
||||||
|
return int(x), true
|
||||||
|
case float64:
|
||||||
|
return int(x), true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) OnBreakpoint(file string, line int, callback func()) {
|
||||||
|
b.callbacksLock.Lock()
|
||||||
|
defer b.callbacksLock.Unlock()
|
||||||
|
|
||||||
|
if f, ok := b.callbacks[file]; ok {
|
||||||
|
f[line] = callback
|
||||||
|
} else {
|
||||||
|
b.callbacks[file] = map[int]func(){
|
||||||
|
line: callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bridge) Wait() error {
|
||||||
|
return b.cmd.Wait()
|
||||||
|
}
|
||||||
42
internal/bridge/protocol.go
Normal file
42
internal/bridge/protocol.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"maps"
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContinueCommand CommandType = "continue"
|
||||||
|
BreakCommand CommandType = "break"
|
||||||
|
LocalsCommand CommandType = "locals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeCommand(cmd CommandType, values map[string]any) (requestId string, request string) {
|
||||||
|
m := make(map[string]any, len(values)+2)
|
||||||
|
maps.Copy(m, values)
|
||||||
|
|
||||||
|
requestId = randString(8)
|
||||||
|
|
||||||
|
m["cmd"] = cmd
|
||||||
|
m["request_id"] = requestId
|
||||||
|
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to marshal command " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestId, string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
func randString(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
114
python/pybug_runtime.py
Normal file
114
python/pybug_runtime.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import bdb
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from types import FrameType
|
||||||
|
|
||||||
|
|
||||||
|
class PyBugBridgeDebugger(bdb.Bdb):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.waiting = False
|
||||||
|
|
||||||
|
def send(self, msg: dict):
|
||||||
|
sys.stdout.write(json.dumps(msg) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def recv(self) -> dict:
|
||||||
|
return json.loads(sys.stdin.readline())
|
||||||
|
|
||||||
|
def user_line(self, frame: FrameType):
|
||||||
|
# print("TRACE:", frame.f_code.co_filename, frame.f_lineno)
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"event": "stopped",
|
||||||
|
"file": frame.f_code.co_filename,
|
||||||
|
"line": frame.f_lineno,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.interaction_loop(frame)
|
||||||
|
|
||||||
|
def interaction_loop(self, frame: FrameType):
|
||||||
|
while True:
|
||||||
|
cmd = self.recv()
|
||||||
|
|
||||||
|
match cmd["cmd"]:
|
||||||
|
case "continue":
|
||||||
|
return self.set_continue()
|
||||||
|
case "step":
|
||||||
|
return self.set_step()
|
||||||
|
case "next":
|
||||||
|
return self.set_next(frame)
|
||||||
|
case "locals":
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"request_id": cmd["request_id"],
|
||||||
|
"event": "locals",
|
||||||
|
"vars": {k: repr(v) for k, v in frame.f_locals.items()},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "eval":
|
||||||
|
try:
|
||||||
|
result = eval(cmd["expr"], frame.f_globals, frame.f_locals)
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"request_id": cmd["request_id"],
|
||||||
|
"event": "eval",
|
||||||
|
"status": "ok",
|
||||||
|
"value": repr(result),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"request_id": cmd["request_id"],
|
||||||
|
"event": "eval",
|
||||||
|
"status": "error",
|
||||||
|
"value": str(e),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "break":
|
||||||
|
err = self.set_break(cmd["file"], cmd["line"])
|
||||||
|
if err:
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"request_id": cmd["request_id"],
|
||||||
|
"event": "break",
|
||||||
|
"status": "error",
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.send(
|
||||||
|
{
|
||||||
|
"request_id": cmd["request_id"],
|
||||||
|
"event": "break",
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: pydbug_runtime <script.py>")
|
||||||
|
|
||||||
|
script = sys.argv[1]
|
||||||
|
sys.argv = sys.argv[1:]
|
||||||
|
|
||||||
|
dbg = PyBugBridgeDebugger()
|
||||||
|
|
||||||
|
with open(script, "rb") as f:
|
||||||
|
code = compile(f.read(), script, "exec")
|
||||||
|
|
||||||
|
globals_dict = {
|
||||||
|
"__name__": "__main__",
|
||||||
|
"__file__": script,
|
||||||
|
"__package__": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
dbg.set_step()
|
||||||
|
dbg.run(code, globals_dict, {})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
test.py
Normal file
10
test.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
def main():
|
||||||
|
print("hello world")
|
||||||
|
x = 50
|
||||||
|
|
||||||
|
for i in range(x):
|
||||||
|
print(i)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
57
ui/model.go
Normal file
57
ui/model.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.pablu.de/pablu/pybug/internal/bridge"
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
currentFile string
|
||||||
|
cursor int
|
||||||
|
text string
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
lineCount int
|
||||||
|
bridge *bridge.Bridge
|
||||||
|
|
||||||
|
listenBridge chan string
|
||||||
|
|
||||||
|
messages []string
|
||||||
|
viewport viewport.Model
|
||||||
|
|
||||||
|
breakpoints map[string][]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(b *bridge.Bridge, file string, text string) Model {
|
||||||
|
c := b.Subscribe()
|
||||||
|
|
||||||
|
vp := viewport.New(0, 0)
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
currentFile: file,
|
||||||
|
text: text,
|
||||||
|
cursor: 0,
|
||||||
|
lineCount: len(strings.Split(text, "\n")),
|
||||||
|
bridge: b,
|
||||||
|
breakpoints: make(map[string][]int),
|
||||||
|
listenBridge: c,
|
||||||
|
messages: make([]string, 0),
|
||||||
|
viewport: vp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenBridge(ch <-chan string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
msg := <-ch
|
||||||
|
return StdoutMsg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StdoutMsg string
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return ListenBridge(m.listenBridge)
|
||||||
|
}
|
||||||
21
ui/program.go
Normal file
21
ui/program.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.pablu.de/pablu/pybug/internal/bridge"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(bridge *bridge.Bridge) error {
|
||||||
|
buf, err := os.ReadFile("test.py")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewModel(bridge, "test.py", string(buf))
|
||||||
|
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
_, err = p.Run()
|
||||||
|
return err
|
||||||
|
}
|
||||||
50
ui/update.go
Normal file
50
ui/update.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
return m.HandleKeyMsg(msg)
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
case StdoutMsg:
|
||||||
|
m.messages = append(m.messages, string(msg))
|
||||||
|
return m, ListenBridge(m.listenBridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) HandleKeyMsg(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch key.String() {
|
||||||
|
case "q", "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "k":
|
||||||
|
m.cursor = clamp(0, m.height, m.cursor-1)
|
||||||
|
case "j":
|
||||||
|
m.cursor = clamp(0, min(m.height, m.lineCount-1), m.cursor+1)
|
||||||
|
case "b":
|
||||||
|
m.bridge.Breakpoint(m.currentFile, m.cursor)
|
||||||
|
if file, ok := m.breakpoints[m.currentFile]; ok {
|
||||||
|
m.breakpoints[m.currentFile] = append(file, m.cursor+1)
|
||||||
|
} else {
|
||||||
|
m.breakpoints[m.currentFile] = []int{
|
||||||
|
m.cursor + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "c":
|
||||||
|
m.bridge.Continue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(minimum, maximum, val int) int {
|
||||||
|
val = max(minimum, val)
|
||||||
|
val = min(maximum, val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
56
ui/view.go
Normal file
56
ui/view.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2/formatters"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
var panelStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
lexer := lexers.Get("python")
|
||||||
|
style := styles.Get("monokai")
|
||||||
|
formatter := formatters.Get("terminal16m")
|
||||||
|
|
||||||
|
iterator, _ := lexer.Tokenise(nil, m.text)
|
||||||
|
formatter.Format(&buf, style, iterator)
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
|
||||||
|
lines := strings.Split(buf.String(), "\n")
|
||||||
|
breakpoints := m.breakpoints[m.currentFile]
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
breakpoint := " "
|
||||||
|
cursor := " "
|
||||||
|
|
||||||
|
if slices.Contains(breakpoints, i+1) {
|
||||||
|
breakpoint = "O"
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == m.cursor {
|
||||||
|
cursor = ">"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&out, "%-4d%s%s %s\n", i+1, breakpoint, cursor, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
frameW, frameH := panelStyle.GetFrameSize()
|
||||||
|
topHeight := m.height * 70 / 100
|
||||||
|
|
||||||
|
code := panelStyle.Width(m.width - frameW).Height(topHeight - frameH).Render(out.String())
|
||||||
|
m.viewport.Height = m.height - (topHeight + frameH)
|
||||||
|
m.viewport.Width = m.width - frameW
|
||||||
|
m.viewport.SetContent(strings.Join(m.messages, ""))
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
output := panelStyle.Width(m.viewport.Width).Height(m.viewport.Height).Render(m.viewport.View())
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Top, code, output)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user