diff --git a/.gitignore b/.gitignore index b8a949a..ddefe29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor/ result +snapshots/ snapshot.png + diff --git a/db.go b/db.go new file mode 100644 index 0000000..7214766 --- /dev/null +++ b/db.go @@ -0,0 +1,4 @@ +package main + +type DB struct { +} diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..0011334 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE snapshots ( + id INTEGER PRIMARY KEY, -- 64-bit snowflake + sid TEXT NOT NULL UNIQUE, -- base62 encoded id + created_at INTEGER NOT NULL, + branch TEXT NOT NULL, + git_hash TEXT NOT NULL, + committed BOOL NOT NULL, + path TEXT NOT NULL +); + +CREATE UNIQUE INDEX idx_snapshots_sid ON snapshots(sid); + diff --git a/flake.nix b/flake.nix index 505c37c..e215b6c 100644 --- a/flake.nix +++ b/flake.nix @@ -50,30 +50,36 @@ in { - packages.default = pkgs.buildGoModule { - pname = "sumi"; - version = "0.1.0"; + packages.default = pkgs.buildGoModule { + pname = "sumi"; + version = "0.1.0"; - src = pkgs.lib.cleanSourceWith { - src = ./.; - filter = path: type: - let base = builtins.baseNameOf path; - in base != "vendor" && base != ".git"; - }; + src = pkgs.lib.cleanSourceWith { + src = ./.; + filter = + path: type: + let + base = builtins.baseNameOf path; + in + base != "vendor" && base != ".git"; + }; - env.CGO_ENABLED = 1; + env.CGO_ENABLED = 1; - nativeBuildInputs = nativeDeps; - buildInputs = raylibDeps; + nativeBuildInputs = nativeDeps; + buildInputs = raylibDeps; - vendorHash = "sha256-teooSdWKQ08cYn/yWMZ8JKuo4rGnV5QOt2Zxzp34Q+I="; + #vendorHash = "sha256-HDfllPEKJZOtkSoasS1yDCyZrWihlkBVRstLkF8AHd0="; - # use this every time there's vendor changeO - # vendorHash = pkgs.lib.fakeHash; + # use this every time there's vendor changeO + vendorHash = pkgs.lib.fakeHash; - ldflags = [ "-s" "-w" ]; - doCheck = false; - }; + ldflags = [ + "-s" + "-w" + ]; + doCheck = false; + }; devShells.default = pkgs.mkShell { # Tools you want while hacking @@ -82,6 +88,7 @@ gopls delve gotools + sqlite ]; nativeBuildInputs = nativeDeps; diff --git a/go.mod b/go.mod index be30ba1..2ab5218 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gen2brain/raylib-go/raylib v0.55.1 github.com/go-git/go-git/v6 v6.0.0-20251212081956-e83cbb9651e8 github.com/ojrac/opensimplex-go v1.0.2 + modernc.org/sqlite v1.40.1 ) require ( @@ -15,17 +16,25 @@ require ( github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.7.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sergi/go-diff v1.4.0 // indirect golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 3dd961b..62df516 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -33,6 +35,10 @@ github.com/go-git/go-git/v6 v6.0.0-20251212081956-e83cbb9651e8 h1:9PLPn/icZJaDXE github.com/go-git/go-git/v6 v6.0.0-20251212081956-e83cbb9651e8/go.mod h1:XY/p4VJq0DwOVAAs+58NpHcQrqwHDEzMv4g8MBK7ZVA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -40,12 +46,18 @@ github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxaWvSzs= github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -54,19 +66,52 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= +modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/ids/ids.go b/internal/ids/ids.go new file mode 100644 index 0000000..15f0849 --- /dev/null +++ b/internal/ids/ids.go @@ -0,0 +1,93 @@ +// k ordered id generator +package ids + +import ( + "errors" + "sync" + "time" +) + +const ( + // 2024-07-03T00:00:00Z + customEpochMs int64 = 1719964800000 + + seqBits = 8 + workerBits = 16 + timeBits = 40 + + seqMask = (1 << seqBits) - 1 // 0xFF + workerMask = (1 << workerBits) - 1 // 0xFFFF + timeMask = (int64(1) << timeBits) - 1 // low 40 bits + + workerShift = seqBits + timeShift = workerBits + seqBits +) + +// Generator is safe for concurrent use. +type Generator struct { + workerID uint64 + mu sync.Mutex + lastMs int64 + sequence uint64 +} + +func NewGenerator(workerID uint32) (*Generator, error) { + if workerID > workerMask { + return nil, errors.New("workerID too large for 16 bits") + } + return &Generator{workerID: uint64(workerID)}, nil +} + +func (g *Generator) Next() (uint64, error) { + nowMs := time.Now().UTC().UnixMilli() + delta := nowMs - customEpochMs + if delta < 0 { + return 0, errors.New("time is before custom epoch") + } + if delta > timeMask { + return 0, errors.New("timestamp overflow (40-bit ms range exceeded)") + } + + g.mu.Lock() + defer g.mu.Unlock() + + if delta == g.lastMs { + g.sequence = (g.sequence + 1) & seqMask + if g.sequence == 0 { + // sequence wrapped; wait for next millisecond + for delta == g.lastMs { + nowMs = time.Now().UTC().UnixMilli() + delta = nowMs - customEpochMs + } + } + } else { + g.sequence = 0 + g.lastMs = delta + } + + id := (uint64(delta) << timeShift) | + ((g.workerID & workerMask) << workerShift) | + (g.sequence & seqMask) + + return id, nil +} + +// ---- Base62 ---- + +const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +func Base62Encode(x uint64) string { + if x == 0 { + return "0" + } + var buf [11]byte // 62^11 > 2^64, so max 11 chars + i := len(buf) + for x > 0 { + r := x % 62 + x /= 62 + i-- + buf[i] = base62Alphabet[r] + } + return string(buf[i:]) +} + diff --git a/main.go b/main.go index a267b79..341a2ee 100644 --- a/main.go +++ b/main.go @@ -1,46 +1,13 @@ package main import ( - "fmt" - "math" + "log" + "os" "github.com/gen2brain/raylib-go/raylib" "github.com/ojrac/opensimplex-go" - "github.com/go-git/go-git/v6" + "math" ) -func HeadHash(repoPath string) (string, error) { - r, err := git.PlainOpen(repoPath) - if err != nil { - return "", err - } - - ref, err := r.Head() - if err != nil { - return "", err - } - - return ref.Hash().String(), nil -} - -func IsDirty(repoPath string) (bool, error) { - r, err := git.PlainOpen(repoPath) - if err != nil { - return false, err - } - - wt, err := r.Worktree() - if err != nil { - return false, err - } - - status, err := wt.Status() - if err != nil { - return false, err - } - - return !status.IsClean(), nil -} - func clamp01(v float64) float64 { if v < 0 { return 0 @@ -74,11 +41,11 @@ func curve(order int, length float64, angle float64) { rl.DrawLine(0, 0, len, 0, rl.Black) rl.Translatef(float32(len), 0, 0) } else { - curve(order - 1, length/2, -angle) + curve(order-1, length/2, -angle) rl.Rotatef(float32(angle), 0, 0, 1) - curve(order - 1, length/2, angle) + curve(order-1, length/2, angle) rl.Rotatef(float32(angle), 0, 0, 1) - curve(order - 1, length/2, -angle) + curve(order-1, length/2, -angle) } } @@ -87,11 +54,22 @@ func main() { const ( screenWidth = 1200 screenHeight = 700 + snapshotsDir = "snapshots" ) + os.MkdirAll(snapshotsDir, 0755) + + log := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) + + storage, err := NewStorage(snapshotsDir) + if err != nil { + log.Printf("Error loading storage: %v\n", err) + os.Exit(1) + } + rl.InitWindow(screenWidth, screenHeight, "sumi sierpinski arrow") - var camera = rl.Camera2D { + var camera = rl.Camera2D{ Target: rl.Vector2{X: 0, Y: 0}, Offset: rl.Vector2{X: float32(screenWidth) / 2, Y: float32(screenHeight) / 2}, Rotation: 0, @@ -103,8 +81,7 @@ func main() { angles := make([]float32, 1000) noise := opensimplex.NewNormalized(0) for i := range len(angles) { - angles[i] = float32(noise.Eval2(float64(i)*0.05, 0.00)) * 0.1 - 0.05 - fmt.Printf("angles[%d] = %.2f\n", i, angles[i]) + angles[i] = float32(noise.Eval2(float64(i)*0.05, 0.00))*0.1 - 0.05 } frameNum := 0 @@ -161,10 +138,10 @@ func main() { // initial transform by halfway again through angle array - angleIndex := frameNum%len(angles) + angleIndex := frameNum % len(angles) angle := angles[angleIndex] - initAngle := angles[(angleIndex + len(angles)/2)%len(angles)] + initAngle := angles[(angleIndex+len(angles)/2)%len(angles)] rl.Rotatef(2500*initAngle, 0, 0, 1) rl.Translatef(100*initAngle, 100*initAngle, 0) @@ -174,50 +151,36 @@ func main() { rl.Translatef(float32(stepSize), 0, 0) rl.Rotatef(angle, 0, 0, 1) angleIndex++ - angleIndex = angleIndex%len(angles) + angleIndex = angleIndex % len(angles) angle += angles[angleIndex] } rl.PopMatrix() - rl.PushMatrix() //rl.Translatef(-screenWidth/2, screenHeight/2, 0) - sierpinskiArrow(9,800) + sierpinskiArrow(9, 800) rl.PopMatrix() /* - rl.PushMatrix() - rl.Translatef(0, 25*50, 0) - rl.Rotatef(90, 1, 0, 0) - rl.DrawGrid(100, 50) - rl.PopMatrix() + rl.PushMatrix() + rl.Translatef(0, 25*50, 0) + rl.Rotatef(90, 1, 0, 0) + rl.DrawGrid(100, 50) + rl.PopMatrix() */ rl.EndMode2D() if rl.IsKeyDown(rl.KeySpace) { - //rl.TakeScreenshot("snapshot.png") + img := rl.LoadImageFromScreen() defer rl.UnloadImage(img) - rl.ExportImage(*img, "snapshot.png") - dflag, err := IsDirty(".") - - if err == nil { - - if dflag { - fmt.Printf("working tree is dirty\n") - } else { - fmt.Printf("working tree is clean\n") - } - - hash, err := HeadHash(".") - if err == nil { - fmt.Printf("HEAD -> %s\n", hash) - } + if _, err := storage.Save(img); err != nil { + log.Printf("Error saving snapshot: %v\n", err) } } @@ -228,4 +191,3 @@ func main() { rl.CloseWindow() } - diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..b1fc9bf --- /dev/null +++ b/storage.go @@ -0,0 +1,206 @@ +package main + +import ( + "fmt" + "log" + "github.com/d2fn/sumi/internal/ids" + "github.com/go-git/go-git/v6" + //"github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v6/plumbing/object" + "github.com/gen2brain/raylib-go/raylib" + "os" + "path/filepath" + "time" + "database/sql" + _ "modernc.org/sqlite" // pure Go, Nix-friendly +) + +type Storage struct { + repoRoot string + snapshotsDir string + gen *ids.Generator + db *sql.DB + log *log.Logger +} + +func NewStorage(snapshotsDir string) (*Storage, error) { + log := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) + gen, _ := ids.NewGenerator(82) + db, err := OpenDB(filepath.Join(snapshotsDir, "snapshots.db"), log) + if err != nil { + return nil, err + } + s := Storage { + repoRoot: ".", + snapshotsDir: snapshotsDir, + gen: gen, + db: db, + } + return &s, nil +} + +func OpenDB(path string, log *log.Logger) (*sql.DB, error) { + log.Printf("Opening sqlite db") + first := false + if _, err := os.Stat(path); os.IsNotExist(err) { + log.Printf("Database not found, initializing") + first = true + } + + db, err := sql.Open("sqlite", path) + if err != nil { + log.Printf("Error opening database at %s", path) + return nil, err + } + + if first { + log.Printf("Initializing empty db with schema", path) + if err := initSchema(db); err != nil { + db.Close() + return nil, err + } + log.Printf("Error initializing schema: %v", err) + } + + return db, nil +} + +func initSchema(db *sql.DB) error { + schema, err := os.ReadFile("db/schema.sql") + if err != nil { + return err + } + + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.Exec(string(schema)); err != nil { + return err + } + + return tx.Commit() +} + +func (s *Storage) Save(img *rl.Image) (string, error) { + id, _ := s.gen.Next() + kid := ids.Base62Encode(id) + path := filepath.Join(s.snapshotsDir, kid) + os.MkdirAll(path, 0755) + + snapshotPng := filepath.Join(path, fmt.Sprintf("%s.png", kid)) + + rl.ExportImage(*img, snapshotPng) + + hash, branch, committed, err := CommitAllIfDirty(s.repoRoot, "automated snapshot") + + if err != nil { + s.log.Printf("Error getting working tree in a known clean state: %v", err) + } else { + s.log.Printf("Created commit %s on %s for snapshot %s", hash, branch, kid) + } + + _, err = s.db.Exec(` + INSERT INTO snapshots (id, sid, created_at, branch, git_hash, committed, path) + VALUES (?, ?, ?, ?, ?, ?) + `, + id, + kid, + time.Now().UnixMilli(), + branch, + hash, + committed, + path, + ) + + if err != nil { + s.log.Printf("Error inserting snapshot row into db: %v\n", err) + } + + s.log.Printf("Saved snapshot to %s\n", path) + + return path, nil +} + +func HeadHash(repoPath string) (string, error) { + r, err := git.PlainOpen(repoPath) + if err != nil { + return "", err + } + + ref, err := r.Head() + if err != nil { + return "", err + } + + return ref.Hash().String(), nil +} + +func IsDirty(repoPath string) (bool, error) { + r, err := git.PlainOpen(repoPath) + if err != nil { + return false, err + } + + wt, err := r.Worktree() + if err != nil { + return false, err + } + + status, err := wt.Status() + if err != nil { + return false, err + } + + return !status.IsClean(), nil +} + +func CommitAllIfDirty(repoPath, message string) (commitHash string, branch string, committed bool, err error) { + r, err := git.PlainOpen(repoPath) + if err != nil { + return + } + + // Determine branch (may be empty if detached) + ref, err := r.Head() + if err != nil { + return + } + if ref.Name().IsBranch() { + branch = ref.Name().Short() + } + + wt, err := r.Worktree() + if err != nil { + return + } + + status, err := wt.Status() + if err != nil { + return + } + + if status.IsClean() { + return "", branch, false, nil + } + + // Stage everything (git add -A) + if err = wt.AddWithOptions(&git.AddOptions{All: true}); err != nil { + return + } + + hash, err := wt.Commit(message, &git.CommitOptions{ + Author: &object.Signature{ + Name: "sumi", + Email: "sumi@local", + When: time.Now(), + }, + }) + if err != nil { + return + } + + return hash.String(), branch, true, nil +}