Skip to content

Commit 22fdf92

Browse files
committed
work on more advanced category UI
categories are displayed in left pane, and parts in right
1 parent 155adc4 commit 22fdf92

File tree

5 files changed

+767
-2
lines changed

5 files changed

+767
-2
lines changed

csv_data.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package main
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
)
11+
12+
// CSVFile represents a single CSV file with its headers and data
13+
type CSVFile struct {
14+
Name string
15+
Path string
16+
Headers []string
17+
Rows [][]string
18+
}
19+
20+
// CSVFileCollection represents all CSV files loaded from a directory
21+
type CSVFileCollection struct {
22+
Files []*CSVFile
23+
}
24+
25+
// loadCSVRaw loads a CSV file without struct mapping, preserving all columns
26+
func loadCSVRaw(filePath string) (*CSVFile, error) {
27+
file, err := os.Open(filePath)
28+
if err != nil {
29+
return nil, fmt.Errorf("error opening file %s: %v", filePath, err)
30+
}
31+
defer file.Close()
32+
33+
reader := csv.NewReader(file)
34+
reader.Comma = ','
35+
reader.LazyQuotes = true
36+
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
37+
38+
// Read headers
39+
headers, err := reader.Read()
40+
if err != nil {
41+
return nil, fmt.Errorf("error reading headers from %s: %v", filePath, err)
42+
}
43+
44+
// Trim whitespace from headers
45+
for i := range headers {
46+
headers[i] = strings.TrimSpace(headers[i])
47+
}
48+
49+
// Read all rows
50+
var rows [][]string
51+
lineNum := 1 // Start at 1 since headers are line 0
52+
for {
53+
row, err := reader.Read()
54+
if err == io.EOF {
55+
break
56+
}
57+
if err != nil {
58+
// Skip malformed rows and continue
59+
fmt.Printf("Warning: error reading row %d from %s: %v\n", lineNum, filePath, err)
60+
lineNum++
61+
continue
62+
}
63+
rows = append(rows, row)
64+
lineNum++
65+
}
66+
67+
return &CSVFile{
68+
Name: filepath.Base(filePath),
69+
Path: filePath,
70+
Headers: headers,
71+
Rows: rows,
72+
}, nil
73+
}
74+
75+
// loadAllCSVFiles loads all CSV files from a directory
76+
func loadAllCSVFiles(dir string) (*CSVFileCollection, error) {
77+
collection := &CSVFileCollection{
78+
Files: []*CSVFile{},
79+
}
80+
81+
files, err := filepath.Glob(filepath.Join(dir, "*.csv"))
82+
if err != nil {
83+
return nil, fmt.Errorf("error finding CSV files in directory %s: %v", dir, err)
84+
}
85+
86+
for _, filePath := range files {
87+
csvFile, err := loadCSVRaw(filePath)
88+
if err != nil {
89+
// Log error but continue loading other files
90+
fmt.Printf("Warning: error loading CSV file %s: %v\n", filePath, err)
91+
continue
92+
}
93+
collection.Files = append(collection.Files, csvFile)
94+
}
95+
96+
if len(collection.Files) == 0 {
97+
return nil, fmt.Errorf("no valid CSV files found in directory %s", dir)
98+
}
99+
100+
return collection, nil
101+
}
102+
103+
// GetCombinedPartmaster returns all parts from all CSV files as a partmaster
104+
func (c *CSVFileCollection) GetCombinedPartmaster() (partmaster, error) {
105+
pm := partmaster{}
106+
107+
for _, file := range c.Files {
108+
// Try to parse each file as partmaster format
109+
filePM, err := c.parseFileAsPartmaster(file)
110+
if err != nil {
111+
// Skip files that don't match partmaster format
112+
continue
113+
}
114+
pm = append(pm, filePM...)
115+
}
116+
117+
return pm, nil
118+
}
119+
120+
// parseFileAsPartmaster attempts to parse a CSV file as partmaster format
121+
func (c *CSVFileCollection) parseFileAsPartmaster(file *CSVFile) (partmaster, error) {
122+
pm := partmaster{}
123+
124+
// Find column indices for partmaster fields
125+
ipnIdx := -1
126+
descIdx := -1
127+
footprintIdx := -1
128+
valueIdx := -1
129+
mfrIdx := -1
130+
mpnIdx := -1
131+
datasheetIdx := -1
132+
priorityIdx := -1
133+
checkedIdx := -1
134+
135+
for i, header := range file.Headers {
136+
switch header {
137+
case "IPN":
138+
ipnIdx = i
139+
case "Description":
140+
descIdx = i
141+
case "Footprint":
142+
footprintIdx = i
143+
case "Value":
144+
valueIdx = i
145+
case "Manufacturer":
146+
mfrIdx = i
147+
case "MPN":
148+
mpnIdx = i
149+
case "Datasheet":
150+
datasheetIdx = i
151+
case "Priority":
152+
priorityIdx = i
153+
case "Checked":
154+
checkedIdx = i
155+
}
156+
}
157+
158+
// Must have at least IPN column to be valid
159+
if ipnIdx == -1 {
160+
return nil, fmt.Errorf("no IPN column found")
161+
}
162+
163+
// Parse rows
164+
for _, row := range file.Rows {
165+
if len(row) == 0 || len(row) <= ipnIdx {
166+
continue
167+
}
168+
169+
line := &partmasterLine{}
170+
171+
// Parse IPN
172+
ipnVal, err := newIpn(row[ipnIdx])
173+
if err != nil {
174+
continue // Skip invalid IPNs
175+
}
176+
line.IPN = ipnVal
177+
178+
// Parse other fields if they exist
179+
if descIdx >= 0 && len(row) > descIdx {
180+
line.Description = row[descIdx]
181+
}
182+
if footprintIdx >= 0 && len(row) > footprintIdx {
183+
line.Footprint = row[footprintIdx]
184+
}
185+
if valueIdx >= 0 && len(row) > valueIdx {
186+
line.Value = row[valueIdx]
187+
}
188+
if mfrIdx >= 0 && len(row) > mfrIdx {
189+
line.Manufacturer = row[mfrIdx]
190+
}
191+
if mpnIdx >= 0 && len(row) > mpnIdx {
192+
line.MPN = row[mpnIdx]
193+
}
194+
if datasheetIdx >= 0 && len(row) > datasheetIdx {
195+
line.Datasheet = row[datasheetIdx]
196+
}
197+
if priorityIdx >= 0 && len(row) > priorityIdx {
198+
// Parse priority as int, default to 0 if invalid
199+
var priority int
200+
fmt.Sscanf(row[priorityIdx], "%d", &priority)
201+
line.Priority = priority
202+
}
203+
if checkedIdx >= 0 && len(row) > checkedIdx {
204+
line.Checked = row[checkedIdx]
205+
}
206+
207+
pm = append(pm, line)
208+
}
209+
210+
return pm, nil
211+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
toolchain go1.24.4
66

77
require (
8+
github.com/charmbracelet/bubbles v0.21.0
89
github.com/charmbracelet/bubbletea v1.3.5
910
github.com/charmbracelet/lipgloss v1.1.0
1011
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
@@ -16,7 +17,6 @@ require (
1617
require (
1718
github.com/atotto/clipboard v0.1.4 // indirect
1819
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19-
github.com/charmbracelet/bubbles v0.21.0 // indirect
2020
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
2121
github.com/charmbracelet/x/ansi v0.8.0 // indirect
2222
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
@@ -30,6 +30,7 @@ require (
3030
github.com/muesli/cancelreader v0.2.2 // indirect
3131
github.com/muesli/termenv v0.16.0 // indirect
3232
github.com/rivo/uniseg v0.4.7 // indirect
33+
github.com/sahilm/fuzzy v0.1.1 // indirect
3334
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3435
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect
3536
golang.org/x/sync v0.13.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
22
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
33
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
44
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5+
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
6+
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
57
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
68
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
79
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
@@ -14,6 +16,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll
1416
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
1517
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
1618
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
19+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
20+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
1721
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
1822
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
1923
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -24,6 +28,8 @@ github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40
2428
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
2529
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2630
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
31+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
32+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
2733
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
2834
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
2935
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -52,6 +58,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
5258
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
5359
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
5460
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
61+
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
62+
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
5563
github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk=
5664
github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
5765
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func main() {
145145

146146
// If no flags were provided, show the TUI
147147
if len(os.Args) == 1 {
148-
err := runTUI(*flagPMDir)
148+
err := runTUINew(*flagPMDir)
149149
if err != nil {
150150
log.Fatal("Error running TUI: ", err)
151151
}

0 commit comments

Comments
 (0)