mirror of
https://github.com/wisplite/parchment.git
synced 2026-06-27 13:47:08 -05:00
initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
Parchment is an app development toolkit for the Cardstock operating system. It handles app uploading and packaging automatically.
|
||||||
|
|
||||||
|
The project is currently in an unfinished state. It currently supports packaging apps in the custom .cpkg (Cardstock Package) format, and can automatically detect an attached Cardputer and put it in dev mode. The actual file transfer protocol is currently unimplemented, and Parchment does not currently handle initializing a Cardstock application in the way a development system like NPM can. These features are planned.
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"log"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileData struct {
|
||||||
|
name string
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type cpkg struct {
|
||||||
|
packageName string
|
||||||
|
files []fileData
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCpkg(packageName string) *cpkg {
|
||||||
|
return &cpkg{
|
||||||
|
packageName: packageName,
|
||||||
|
files: make([]fileData, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cpkg) AddFile(fsys fs.FS, path string) error {
|
||||||
|
file, err := fs.ReadFile(fsys, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.files = append(c.files, fileData{name: path, content: file})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpkg file header format:
|
||||||
|
// 1 byte: filename length
|
||||||
|
// n bytes: filename
|
||||||
|
// 4 bytes: file size (uint32)
|
||||||
|
func (c *cpkg) createCpkgFileHeader(file fileData) []byte {
|
||||||
|
headerBytes := make([]byte, 0)
|
||||||
|
headerBytes = append(headerBytes, byte(len(file.name)))
|
||||||
|
headerBytes = append(headerBytes, []byte(file.name)...)
|
||||||
|
|
||||||
|
// file size (uint32 - 4 bytes)
|
||||||
|
fileSizeBytes := make([]byte, 4)
|
||||||
|
binary.LittleEndian.PutUint32(fileSizeBytes, uint32(len(file.content)))
|
||||||
|
headerBytes = append(headerBytes, fileSizeBytes...)
|
||||||
|
|
||||||
|
return headerBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpkg header format:
|
||||||
|
// 4 bytes: magic number "Cpkg"
|
||||||
|
// 2 bytes: version
|
||||||
|
// 4 bytes: file count
|
||||||
|
func (c *cpkg) createCpkgHeader() []byte {
|
||||||
|
headerBytes := make([]byte, 0)
|
||||||
|
headerBytes = append(headerBytes, []byte("Cpkg")...)
|
||||||
|
|
||||||
|
// version (uint16 - 2 bytes)
|
||||||
|
versionBytes := make([]byte, 2)
|
||||||
|
binary.LittleEndian.PutUint16(versionBytes, 1)
|
||||||
|
headerBytes = append(headerBytes, versionBytes...)
|
||||||
|
|
||||||
|
// file count (uint32 - 4 bytes)
|
||||||
|
fileCountBytes := make([]byte, 4)
|
||||||
|
binary.LittleEndian.PutUint32(fileCountBytes, uint32(len(c.files)))
|
||||||
|
headerBytes = append(headerBytes, fileCountBytes...)
|
||||||
|
|
||||||
|
return headerBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cpkg) Create() (bool, error) {
|
||||||
|
file, err := os.Create(c.packageName + ".cpkg")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Write main header
|
||||||
|
header := c.createCpkgHeader()
|
||||||
|
n, err := file.Write(header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to write header: %w", err)
|
||||||
|
}
|
||||||
|
if n != len(header) {
|
||||||
|
return false, fmt.Errorf("incomplete header write: wrote %d of %d bytes", n, len(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write each file
|
||||||
|
for i, fileData := range c.files {
|
||||||
|
// Write file header
|
||||||
|
fileHeader := c.createCpkgFileHeader(fileData)
|
||||||
|
n, err := file.Write(fileHeader)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to write header for file %d (%s): %w", i, fileData.name, err)
|
||||||
|
}
|
||||||
|
if n != len(fileHeader) {
|
||||||
|
return false, fmt.Errorf("incomplete header write for file %d (%s): wrote %d of %d bytes", i, fileData.name, n, len(fileHeader))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file content
|
||||||
|
n, err = file.Write(fileData.content)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to write content for file %d (%s): %w", i, fileData.name, err)
|
||||||
|
}
|
||||||
|
if n != len(fileData.content) {
|
||||||
|
return false, fmt.Errorf("incomplete content write for file %d (%s): wrote %d of %d bytes", i, fileData.name, n, len(fileData.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Wrote file %d: %s (%d bytes)", i, fileData.name, len(fileData.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cpkg) DebugPrint() {
|
||||||
|
log.Println("Package Name:", c.packageName)
|
||||||
|
idx := 0
|
||||||
|
for _, file := range c.files {
|
||||||
|
log.Println("File " + strconv.Itoa(idx) + " header")
|
||||||
|
log.Println(c.createCpkgFileHeader(file))
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cpkg) Unpack(fsys fs.FS) error {
|
||||||
|
file, err := os.Open(c.packageName + ".cpkg")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read main header (10 bytes)
|
||||||
|
header := make([]byte, 10)
|
||||||
|
_, err = file.Read(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify magic number
|
||||||
|
magicNumber := string(header[:4])
|
||||||
|
if magicNumber != "Cpkg" {
|
||||||
|
return fmt.Errorf("invalid cpkg file: wrong magic number")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify version
|
||||||
|
version := binary.LittleEndian.Uint16(header[4:6])
|
||||||
|
if version != 1 {
|
||||||
|
return fmt.Errorf("unsupported cpkg file version: %d", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file count
|
||||||
|
fileCount := binary.LittleEndian.Uint32(header[6:10])
|
||||||
|
log.Printf("Unpacking %d files from %s.cpkg", fileCount, c.packageName)
|
||||||
|
|
||||||
|
// Clear existing files array
|
||||||
|
c.files = make([]fileData, 0, fileCount)
|
||||||
|
|
||||||
|
// Read each file sequentially
|
||||||
|
for i := 0; i < int(fileCount); i++ {
|
||||||
|
// Read filename length (1 byte)
|
||||||
|
fileNameLengthBuf := make([]byte, 1)
|
||||||
|
n, err := file.Read(fileNameLengthBuf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read filename length for file %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
return fmt.Errorf("incomplete read of filename length for file %d: got %d bytes, expected 1", i, n)
|
||||||
|
}
|
||||||
|
fileNameLength := fileNameLengthBuf[0]
|
||||||
|
|
||||||
|
// Read filename
|
||||||
|
fileNameBuf := make([]byte, fileNameLength)
|
||||||
|
n, err = file.Read(fileNameBuf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read filename for file %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if n != int(fileNameLength) {
|
||||||
|
return fmt.Errorf("incomplete read of filename for file %d: got %d bytes, expected %d", i, n, fileNameLength)
|
||||||
|
}
|
||||||
|
fileName := string(fileNameBuf)
|
||||||
|
|
||||||
|
// Read file size (4 bytes)
|
||||||
|
fileSizeBuf := make([]byte, 4)
|
||||||
|
n, err = file.Read(fileSizeBuf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file size for file %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if n != 4 {
|
||||||
|
return fmt.Errorf("incomplete read of file size for file %d: got %d bytes, expected 4", i, n)
|
||||||
|
}
|
||||||
|
fileSize := binary.LittleEndian.Uint32(fileSizeBuf)
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
fileContent := make([]byte, fileSize)
|
||||||
|
n, err = file.Read(fileContent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read content for file %d (%s): %w", i, fileName, err)
|
||||||
|
}
|
||||||
|
if n != int(fileSize) {
|
||||||
|
return fmt.Errorf("incomplete read of content for file %d (%s): got %d bytes, expected %d", i, fileName, n, fileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Read file %d: %s (%d bytes)", i, fileName, fileSize)
|
||||||
|
c.files = append(c.files, fileData{name: fileName, content: fileContent})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write files to filesystem in subdirectory with the package name
|
||||||
|
err = os.MkdirAll(c.packageName, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fileData := range c.files {
|
||||||
|
outputPath := filepath.Join(c.packageName, fileData.name)
|
||||||
|
|
||||||
|
// Create parent directories if they don't exist
|
||||||
|
parentDir := filepath.Dir(outputPath)
|
||||||
|
err = os.MkdirAll(parentDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory for file %s: %w", fileData.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(outputPath, fileData.content, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", fileData.name, err)
|
||||||
|
}
|
||||||
|
log.Printf("Wrote file: %s", outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully unpacked %d files to %s/", len(c.files), c.packageName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.bug.st/serial"
|
||||||
|
"go.bug.st/serial/enumerator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Device manages the serial connection to the Cardputer.
|
||||||
|
type Device struct {
|
||||||
|
PortName string
|
||||||
|
Port serial.Port
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDevice creates a new Device instance.
|
||||||
|
func NewDevice() *Device {
|
||||||
|
return &Device{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindCardputer searches for a connected Cardputer by VID.
|
||||||
|
func (d *Device) FindCardputer() (string, error) {
|
||||||
|
ports, err := enumerator.GetDetailedPortsList()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, port := range ports {
|
||||||
|
if port.VID == "303a" { // Cardputer VID
|
||||||
|
d.PortName = port.Name
|
||||||
|
return port.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Cardputer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect opens the serial port and requests dev mode.
|
||||||
|
func (d *Device) Connect() error {
|
||||||
|
if d.PortName == "" {
|
||||||
|
return fmt.Errorf("no port name set")
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := &serial.Mode{
|
||||||
|
BaudRate: 115200,
|
||||||
|
}
|
||||||
|
port, err := serial.Open(d.PortName, mode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Port = port
|
||||||
|
d.Connected = true
|
||||||
|
|
||||||
|
// Send dev mode request
|
||||||
|
// 0xAA 0x04 0x00 0x00 0x00 is the command sequence for dev mode
|
||||||
|
_, err = d.Port.Write([]byte{0xAA, 0x04, 0x00, 0x00, 0x00})
|
||||||
|
if err != nil {
|
||||||
|
d.Close()
|
||||||
|
return fmt.Errorf("failed to send dev mode request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow some time for the device to respond or settle if needed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the serial port.
|
||||||
|
func (d *Device) Close() error {
|
||||||
|
if d.Port != nil {
|
||||||
|
err := d.Port.Close()
|
||||||
|
d.Port = nil
|
||||||
|
d.Connected = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sends data to the device.
|
||||||
|
func (d *Device) Write(data []byte) (int, error) {
|
||||||
|
if !d.Connected || d.Port == nil {
|
||||||
|
return 0, fmt.Errorf("device not connected")
|
||||||
|
}
|
||||||
|
return d.Port.Write(data)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
module github.com/wisplite/parchment
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // 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.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
go.bug.st/serial v1.6.4 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +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.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
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.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
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.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
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.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.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
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=
|
||||||
|
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=
|
||||||
|
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||||
|
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||||
|
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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.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=
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Setup logging to file
|
||||||
|
f, err := tea.LogToFile("debug.log", "debug")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("fatal:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Initialize Device manager
|
||||||
|
device := NewDevice()
|
||||||
|
|
||||||
|
// Initial search for device
|
||||||
|
// We do this synchronously at startup for convenience,
|
||||||
|
// but the UI could also handle re-scanning.
|
||||||
|
portName, err := device.FindCardputer()
|
||||||
|
|
||||||
|
// Initialize UI Model
|
||||||
|
m := InitialModel(device)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
m.Status = StatusFound
|
||||||
|
log.Printf("Found Cardputer at %s", portName)
|
||||||
|
} else {
|
||||||
|
log.Printf("Cardputer not found at startup: %v", err)
|
||||||
|
// We start in Searching state, user might plug it in later if we implemented auto-scan,
|
||||||
|
// but for now it just shows searching or error.
|
||||||
|
// Since our basic implementation does a one-time scan at start,
|
||||||
|
// if it fails, we might want to let the user retry or just show the state.
|
||||||
|
// The current InitialModel defaults to StatusSearching.
|
||||||
|
// To match original behavior (sort of), if we don't find it, we stay in Searching/Error.
|
||||||
|
// Actually, let's just leave it as StatusSearching or set Error if we want to be explicit.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and run the Bubble Tea program
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
log.Fatal("Error running program:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PackageJSON struct {
|
||||||
|
Pkg string `json:"pkg"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Packager is a class that helps create cpkg files and handle the upload process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func loadPackageJSON(fsys fs.FS) (*PackageJSON, error) {
|
||||||
|
packageJSON, err := fs.ReadFile(fsys, "package.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkgData := &PackageJSON{}
|
||||||
|
err = json.Unmarshal(packageJSON, pkgData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkgData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCpkg(fsys fs.FS, pkgData *PackageJSON) (*cpkg, error) {
|
||||||
|
pkg := NewCpkg(pkgData.Pkg)
|
||||||
|
|
||||||
|
// Walk through all directories recursively
|
||||||
|
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories themselves (we only want files)
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip certain files
|
||||||
|
fileName := d.Name()
|
||||||
|
if fileName == "debug.log" || strings.HasSuffix(fileName, ".cpkg") {
|
||||||
|
log.Println("Skipping file:", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file with full relative path
|
||||||
|
log.Println("Adding file:", path)
|
||||||
|
err = pkg.AddFile(fsys, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
success, err := pkg.Create()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppStatus represents the current state of the application.
|
||||||
|
type AppStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusSearching AppStatus = iota
|
||||||
|
StatusFound
|
||||||
|
StatusConnecting
|
||||||
|
StatusConnected
|
||||||
|
StatusError
|
||||||
|
StatusUploading
|
||||||
|
StatusUploaded
|
||||||
|
StatusPromptUnpack
|
||||||
|
StatusUnpacking
|
||||||
|
StatusUnpacked
|
||||||
|
)
|
||||||
|
|
||||||
|
// UIModel is the Bubble Tea model for the application.
|
||||||
|
type UIModel struct {
|
||||||
|
Device *Device
|
||||||
|
Status AppStatus
|
||||||
|
ErrorMsg string
|
||||||
|
TextInput textinput.Model
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom messages for Bubble Tea commands
|
||||||
|
type connectMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type uploadMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type unpackMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitialModel creates the initial state of the model.
|
||||||
|
func InitialModel(device *Device) UIModel {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "package-name"
|
||||||
|
ti.CharLimit = 256
|
||||||
|
ti.Width = 40
|
||||||
|
|
||||||
|
return UIModel{
|
||||||
|
Device: device,
|
||||||
|
Status: StatusSearching,
|
||||||
|
TextInput: ti,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the model.
|
||||||
|
func (m UIModel) Init() tea.Cmd {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles messages and updates the model.
|
||||||
|
func (m UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.Width = msg.Width
|
||||||
|
m.Height = msg.Height
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
// Ensure device is closed on exit
|
||||||
|
if m.Device != nil {
|
||||||
|
m.Device.Close()
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input based on current status
|
||||||
|
if m.Status == StatusPromptUnpack {
|
||||||
|
return m.updatePromptUnpack(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.updateMain(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UIModel) updatePromptUnpack(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
if m.Device.Connected {
|
||||||
|
m.Status = StatusConnected
|
||||||
|
} else {
|
||||||
|
m.Status = StatusFound
|
||||||
|
}
|
||||||
|
m.TextInput.Reset()
|
||||||
|
return m, nil
|
||||||
|
case "enter":
|
||||||
|
filename := m.TextInput.Value()
|
||||||
|
if filename != "" {
|
||||||
|
m.Status = StatusUnpacking
|
||||||
|
m.TextInput.Reset()
|
||||||
|
return m, debugUnpackCmd(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.TextInput, cmd = m.TextInput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UIModel) updateMain(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "c", "enter":
|
||||||
|
if m.Status == StatusFound {
|
||||||
|
m.Status = StatusConnecting
|
||||||
|
return m, connectCmd(m.Device)
|
||||||
|
}
|
||||||
|
case "u":
|
||||||
|
if m.Status == StatusConnected {
|
||||||
|
m.Status = StatusUploading
|
||||||
|
return m, uploadCmd(m.Device)
|
||||||
|
}
|
||||||
|
case "d":
|
||||||
|
if m.Status == StatusFound || m.Status == StatusConnected || m.Status == StatusUploaded || m.Status == StatusUnpacked {
|
||||||
|
m.Status = StatusPromptUnpack
|
||||||
|
m.TextInput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case connectMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.Status = StatusError
|
||||||
|
m.ErrorMsg = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.Status = StatusConnected
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case uploadMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.Status = StatusError
|
||||||
|
m.ErrorMsg = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.Status = StatusUploaded
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case unpackMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.Status = StatusError
|
||||||
|
m.ErrorMsg = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.Status = StatusUnpacked
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the application UI.
|
||||||
|
func (m UIModel) View() string {
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
titleStyle := lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
|
Background(lipgloss.Color("#7D56F4")).
|
||||||
|
Padding(0, 1).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Bold(true)
|
||||||
|
errorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Bold(true)
|
||||||
|
successStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Bold(true)
|
||||||
|
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Bold(true)
|
||||||
|
descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#A0A0A0"))
|
||||||
|
|
||||||
|
// Header
|
||||||
|
s.WriteString(titleStyle.Render("Parchment - Cardstock Dev Server"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
|
||||||
|
// Content
|
||||||
|
content := strings.Builder{}
|
||||||
|
|
||||||
|
switch m.Status {
|
||||||
|
case StatusSearching:
|
||||||
|
content.WriteString(statusStyle.Render("⏳ Searching for Cardputer..."))
|
||||||
|
|
||||||
|
case StatusFound:
|
||||||
|
content.WriteString(successStyle.Render("✓ Cardputer found"))
|
||||||
|
content.WriteString(fmt.Sprintf(" at %s\n\n", m.Device.PortName))
|
||||||
|
content.WriteString(keyStyle.Render("[c] / [enter]") + descStyle.Render(" Connect and enter dev mode\n"))
|
||||||
|
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack (local)\n"))
|
||||||
|
|
||||||
|
case StatusConnecting:
|
||||||
|
content.WriteString(statusStyle.Render("⏳ Connecting and requesting dev mode..."))
|
||||||
|
|
||||||
|
case StatusConnected:
|
||||||
|
content.WriteString(successStyle.Render("✓ Connected to Cardputer!"))
|
||||||
|
content.WriteString(fmt.Sprintf("\n\nPort: %s\n", m.Device.PortName))
|
||||||
|
content.WriteString("Device is in developer mode.\n\n")
|
||||||
|
content.WriteString(keyStyle.Render("[u]") + descStyle.Render(" Upload package from current directory\n"))
|
||||||
|
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack (local)\n"))
|
||||||
|
|
||||||
|
case StatusUploading:
|
||||||
|
content.WriteString(statusStyle.Render("⏳ Uploading package to device..."))
|
||||||
|
|
||||||
|
case StatusUploaded:
|
||||||
|
content.WriteString(successStyle.Render("✓ Package uploaded successfully!"))
|
||||||
|
content.WriteString(fmt.Sprintf("\n\nPort: %s\n", m.Device.PortName))
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Debug pack/unpack\n"))
|
||||||
|
|
||||||
|
case StatusPromptUnpack:
|
||||||
|
content.WriteString(statusStyle.Render("Debug Unpack"))
|
||||||
|
content.WriteString("\n\nEnter the filename to unpack (without .cpkg extension):\n\n")
|
||||||
|
content.WriteString(m.TextInput.View())
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
content.WriteString(keyStyle.Render("[enter]") + descStyle.Render(" Unpack\n"))
|
||||||
|
content.WriteString(keyStyle.Render("[esc]") + descStyle.Render(" Cancel\n"))
|
||||||
|
|
||||||
|
case StatusUnpacking:
|
||||||
|
content.WriteString(statusStyle.Render("⏳ Debug packing and unpacking..."))
|
||||||
|
|
||||||
|
case StatusUnpacked:
|
||||||
|
content.WriteString(successStyle.Render("✓ Debug pack/unpack completed!"))
|
||||||
|
content.WriteString("\n\nCheck debug.log for details.\n")
|
||||||
|
content.WriteString(keyStyle.Render("[d]") + descStyle.Render(" Run again\n"))
|
||||||
|
|
||||||
|
case StatusError:
|
||||||
|
content.WriteString(errorStyle.Render("✗ Error: "))
|
||||||
|
content.WriteString(m.ErrorMsg)
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
if m.Device.Connected {
|
||||||
|
content.WriteString(keyStyle.Render("[u]") + descStyle.Render(" Retry upload\n"))
|
||||||
|
} else {
|
||||||
|
content.WriteString(keyStyle.Render("[c]") + descStyle.Render(" Retry connection\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render content with some margin
|
||||||
|
s.WriteString(lipgloss.NewStyle().Margin(1, 0, 1, 0).Render(content.String()))
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
s.WriteString("\n")
|
||||||
|
footer := lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render("Press [q] or [ctrl+c] to quit")
|
||||||
|
s.WriteString(footer)
|
||||||
|
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(s.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
|
||||||
|
func connectCmd(d *Device) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
if err := d.Connect(); err != nil {
|
||||||
|
return connectMsg{err: err}
|
||||||
|
}
|
||||||
|
return connectMsg{err: nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadCmd(d *Device) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
// Mock upload for now or implement actual upload logic using Device
|
||||||
|
// We'll use the existing logic from main.go adapted here
|
||||||
|
fsys := os.DirFS(".")
|
||||||
|
pkgData, err := loadPackageJSON(fsys)
|
||||||
|
if err != nil {
|
||||||
|
return uploadMsg{err: err}
|
||||||
|
}
|
||||||
|
if pkgData == nil {
|
||||||
|
return uploadMsg{err: fmt.Errorf("package.json not found")}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Loading package data:", pkgData)
|
||||||
|
pkg, err := createCpkg(fsys, pkgData)
|
||||||
|
if err != nil {
|
||||||
|
return uploadMsg{err: err}
|
||||||
|
}
|
||||||
|
pkg.DebugPrint()
|
||||||
|
|
||||||
|
// TODO: Actually send the package over serial using d.Write()
|
||||||
|
// For now we just simulate success as per original code
|
||||||
|
|
||||||
|
return uploadMsg{err: nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugUnpackCmd(filename string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
fsys := os.DirFS(".")
|
||||||
|
|
||||||
|
packageName := strings.TrimSuffix(filename, ".cpkg")
|
||||||
|
log.Printf("Attempting to unpack: %s.cpkg", packageName)
|
||||||
|
|
||||||
|
if _, err := os.Stat(packageName + ".cpkg"); os.IsNotExist(err) {
|
||||||
|
return unpackMsg{err: fmt.Errorf("file not found: %s.cpkg", packageName)}
|
||||||
|
}
|
||||||
|
|
||||||
|
unpackPkg := NewCpkg(packageName)
|
||||||
|
err := unpackPkg.Unpack(fsys)
|
||||||
|
if err != nil {
|
||||||
|
return unpackMsg{err: fmt.Errorf("failed to unpack: %w", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Debug unpack completed successfully")
|
||||||
|
return unpackMsg{err: nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user