concertina

Elecronic concertina
git clone https://git.woozle.org/neale/concertina.git

concertina / pkg / layouts
Neale Pickett  ·  2026-02-17

layouts.go

 1// Package layouts parses text layouts into []Layout.
 2//
 3// Because Go doesn't appear to have any built-in mechanism to output Go source code literals,
 4// it's either this or some intermediate format like JSON or gobs.
 5// This bespoke package is pretty small compared to the alternatives.
 6package layouts
 7
 8import (
 9	_ "embed"
10	"strconv"
11	"strings"
12)
13
14//go:embed layouts.txt
15var layouts_txt string
16
17const (
18	Sides   = 2
19	Rows    = 4
20	Columns = 5
21)
22
23type ParseError struct {
24	line int
25	desc string
26}
27
28func (e *ParseError) Error() string {
29	return "line " + strconv.Itoa(e.line) + ": " + e.desc
30}
31
32type Layout struct {
33	Name  string
34	Notes [Sides][Rows][Columns][2]int8
35}
36
37func ParseLayouts(s string) ([]*Layout, error) {
38	layouts := make([]*Layout, 0)
39	var current *Layout
40
41	row := 0
42	for lineno, line := range strings.Split(s, "\n") {
43		line = strings.TrimSpace(line)
44
45		// Skip blank lines and comments
46		if (line == "") || strings.HasPrefix(line, "#") {
47			continue
48		}
49
50		// Split the line up into fields
51		fields := strings.Fields(line)
52
53		// New layouts are anything other than 11-field lines with | in the middle
54		if (len(fields) != 11) || (fields[5] != "|") {
55			current = &Layout{
56				Name: line,
57			}
58			layouts = append(layouts, current)
59			row = 0
60			continue
61		}
62
63		if row >= Rows {
64			return nil, &ParseError{lineno, "too many rows: " + line}
65		}
66
67		// We can skip the middle | now
68		fields = append(fields[:5], fields[6:]...)
69		for col, button := range fields {
70			side := col / Columns
71			col = col % Columns
72			notes := strings.Split(button, "/")
73
74			// If only one note is listed, push and draw are the same
75			if len(notes) == 1 {
76				notes = append(notes, notes[0])
77			}
78
79			if len(notes) != 2 {
80				return nil, &ParseError{lineno, "invalid button entry"}
81			}
82
83			for i, note := range notes {
84				midi := ABC2MIDI(note)
85				if (midi == -1) && (note != "-") {
86					return nil, &ParseError{lineno, "unrecognized note: " + note}
87				}
88				current.Notes[side][row][col][i] = midi
89			}
90		}
91		row += 1
92	}
93
94	return layouts, nil
95}
96
97func DefaultLayouts() ([]*Layout, error) {
98	return ParseLayouts(layouts_txt)
99}