feat: Sample Tag Viewer — read-only tag browser for AKAI disk images
Backend: - extract.go: listTags() navigates to volume, runs akaiutil lstags - extract.go: parseTags() parses tag name/index from lstags output - serve.go: GET /api/disk/partitions — list partitions on an ISO - serve.go: GET /api/disk/volumes — list volumes on a partition - serve.go: GET /api/volume/tags — list tags on a volume Web UI: - Tag Viewer section with ISO path input and Browse button - Partition selector buttons - Volume grid with click-to-select - Tag chips with color-coded dots (20-color palette) Tests: - 6 parseTags tests (basic, spaces, defaults, no tags, empty, no-type) - 1 listTags integration test - 4 API endpoint tests (partitions, missing-ISO, missing-params, tags) Karpathy: read-only view first — no write operations yet (settagi, clrtagi)
This commit is contained in:
+53
-2
@@ -71,8 +71,8 @@ var runAkaiutil = func(isoPath, commands string) (string, error) {
|
||||
}
|
||||
|
||||
type volInfo struct {
|
||||
Part string
|
||||
Name string
|
||||
Part string `json:"part"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func listPartitions(isoPath string) ([]string, error) {
|
||||
@@ -210,3 +210,54 @@ var extractISO = func(isoPath, outDir string) (string, error) {
|
||||
}
|
||||
return fmt.Sprintf("Total WAV files: %d (%s)", total, elapsed), nil
|
||||
}
|
||||
|
||||
type TagInfo struct {
|
||||
Index int `json:"index"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func listTags(isoPath, part, volName string) ([]TagInfo, error) {
|
||||
var cmds strings.Builder
|
||||
fmt.Fprintf(&cmds, "df\n")
|
||||
fmt.Fprintf(&cmds, "cd /disk0/%s\n", part)
|
||||
nav := strings.ReplaceAll(volName, " ", "_")
|
||||
fmt.Fprintf(&cmds, "cd /disk0/%s/%s\n", part, nav)
|
||||
fmt.Fprintf(&cmds, "lstags\n")
|
||||
cmds.WriteString("q\n")
|
||||
|
||||
out, err := runAkaiutil(isoPath, cmds.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list tags: %w", err)
|
||||
}
|
||||
|
||||
return parseTags(out), nil
|
||||
}
|
||||
|
||||
func parseTags(output string) []TagInfo {
|
||||
reTag := regexp.MustCompile(`^\s*(\d+)\s+(\S.*)`)
|
||||
|
||||
var tags []TagInfo
|
||||
inTags := false
|
||||
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
trimmed := strings.TrimRight(line, " \r")
|
||||
if strings.Contains(trimmed, "tag name") {
|
||||
inTags = true
|
||||
continue
|
||||
}
|
||||
if !inTags {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "---") && len(tags) > 0 {
|
||||
break
|
||||
}
|
||||
if m := reTag.FindStringSubmatch(trimmed); m != nil {
|
||||
idx, _ := strconv.Atoi(m[1])
|
||||
name := strings.TrimSpace(m[2])
|
||||
if name != "" {
|
||||
tags = append(tags, TagInfo{Index: idx, Name: name})
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user