summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-02 17:52:13 +0300
committerPaul Buetow <paul@buetow.org>2026-04-02 17:52:13 +0300
commit11a3e62f17433a7df0e6a77a688321d94a736778 (patch)
treeada0f94c074a3e290ea993179fbf645a40d7ab48 /internal
parentf3d057fec20aeef584cb6340c6a002280a019f15 (diff)
task 003: handle home-dir and APKG marshal errors
Diffstat (limited to 'internal')
-rw-r--r--internal/anki/apkg_generator.go31
-rw-r--r--internal/anki/apkg_generator_test.go8
-rw-r--r--internal/cli/command.go6
-rw-r--r--internal/config/home.go22
-rw-r--r--internal/config/home_test.go25
-rw-r--r--internal/gui/app.go16
-rw-r--r--internal/processor/processor.go6
7 files changed, 104 insertions, 10 deletions
diff --git a/internal/anki/apkg_generator.go b/internal/anki/apkg_generator.go
index 86b28df..c3f489a 100644
--- a/internal/anki/apkg_generator.go
+++ b/internal/anki/apkg_generator.go
@@ -244,14 +244,20 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error {
"extendRev": 50,
},
}
- decksJSON, _ := json.Marshal(decks)
+ decksJSON, err := marshalJSON("decks", decks)
+ if err != nil {
+ return err
+ }
// Create model (note type) configuration
models := map[string]interface{}{
fmt.Sprintf("%d", g.modelID): g.createNoteTypeConfig(),
fmt.Sprintf("%d", g.modelIDBgBg): g.createBgBgNoteTypeConfig(),
}
- modelsJSON, _ := json.Marshal(models)
+ modelsJSON, err := marshalJSON("models", models)
+ if err != nil {
+ return err
+ }
// Default configuration
conf := map[string]interface{}{
@@ -270,7 +276,10 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error {
"curModel": fmt.Sprintf("%d", g.modelID),
"dayLearnFirst": false,
}
- confJSON, _ := json.Marshal(conf)
+ confJSON, err := marshalJSON("conf", conf)
+ if err != nil {
+ return err
+ }
// Deck options
dconf := map[string]interface{}{
@@ -311,10 +320,13 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error {
"replayq": true,
},
}
- dconfJSON, _ := json.Marshal(dconf)
+ dconfJSON, err := marshalJSON("dconf", dconf)
+ if err != nil {
+ return err
+ }
query := `INSERT INTO col VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
- _, err := db.Exec(query,
+ _, err = db.Exec(query,
1, // id
now, // crt
now*1000, // mod
@@ -332,6 +344,15 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error {
return err
}
+func marshalJSON(name string, value any) ([]byte, error) {
+ data, err := json.Marshal(value)
+ if err != nil {
+ return nil, fmt.Errorf("marshal %s: %w", name, err)
+ }
+
+ return data, nil
+}
+
// createNoteTypeConfig creates the note type configuration
func (g *APKGGenerator) createNoteTypeConfig() map[string]interface{} {
return map[string]interface{}{
diff --git a/internal/anki/apkg_generator_test.go b/internal/anki/apkg_generator_test.go
index 4f2db18..68d828d 100644
--- a/internal/anki/apkg_generator_test.go
+++ b/internal/anki/apkg_generator_test.go
@@ -63,6 +63,7 @@ func TestAPKGAddCard(t *testing.T) {
t.Errorf("Expected Bulgarian 'ябълка', got '%s'", gen.cards[0].Bulgarian)
}
}
+
func TestMediaFiles(t *testing.T) {
gen := NewAPKGGenerator("Test Deck")
@@ -82,6 +83,7 @@ func TestMediaFiles(t *testing.T) {
t.Errorf("Expected mediaFiles['image.jpg'] = 1, got %d", gen.mediaFiles["image.jpg"])
}
}
+
func TestGenerateAPKG(t *testing.T) {
tempDir := t.TempDir()
@@ -213,3 +215,9 @@ func TestCreateDatabase(t *testing.T) {
t.Errorf("Expected 1 note, got %d", noteCount)
}
}
+
+func TestMarshalJSONReturnsErrorForUnsupportedValue(t *testing.T) {
+ if _, err := marshalJSON("bad", make(chan int)); err == nil {
+ t.Fatal("marshalJSON() error = nil, want error")
+ }
+}
diff --git a/internal/cli/command.go b/internal/cli/command.go
index 2c7ee04..09c1758 100644
--- a/internal/cli/command.go
+++ b/internal/cli/command.go
@@ -12,6 +12,7 @@ import (
"codeberg.org/snonux/totalrecall/internal"
"codeberg.org/snonux/totalrecall/internal/audio"
+ appconfig "codeberg.org/snonux/totalrecall/internal/config"
)
// CreateRootCommand creates and configures the root cobra command
@@ -49,7 +50,10 @@ Batch file formats:
func setupFlags(cmd *cobra.Command, flags *Flags) {
// Set default output directory to match GUI mode
- home, _ := os.UserHomeDir()
+ home, err := appconfig.HomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
defaultOutputDir := filepath.Join(home, ".local", "state", "totalrecall", "cards")
// Global flags
diff --git a/internal/config/home.go b/internal/config/home.go
new file mode 100644
index 0000000..8089b6e
--- /dev/null
+++ b/internal/config/home.go
@@ -0,0 +1,22 @@
+package config
+
+import (
+ "fmt"
+ "os"
+)
+
+var userHomeDir = os.UserHomeDir
+
+// HomeDir returns the user's home directory.
+//
+// It falls back to "." when the home directory cannot be resolved so callers
+// can still build a safe relative path instead of joining against an empty
+// string.
+func HomeDir() (string, error) {
+ homeDir, err := userHomeDir()
+ if err != nil {
+ return ".", fmt.Errorf("resolve home directory: %w", err)
+ }
+
+ return homeDir, nil
+}
diff --git a/internal/config/home_test.go b/internal/config/home_test.go
new file mode 100644
index 0000000..bc7f5ea
--- /dev/null
+++ b/internal/config/home_test.go
@@ -0,0 +1,25 @@
+package config
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestHomeDirReturnsFallbackWhenResolutionFails(t *testing.T) {
+ oldUserHomeDir := userHomeDir
+ t.Cleanup(func() {
+ userHomeDir = oldUserHomeDir
+ })
+
+ userHomeDir = func() (string, error) {
+ return "", errors.New("boom")
+ }
+
+ homeDir, err := HomeDir()
+ if err == nil {
+ t.Fatal("HomeDir() error = nil, want error")
+ }
+ if homeDir != "." {
+ t.Fatalf("HomeDir() homeDir = %q, want %q", homeDir, ".")
+ }
+}
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 60bc2e1..f9d941d 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -24,6 +24,7 @@ import (
"codeberg.org/snonux/totalrecall/internal/anki"
"codeberg.org/snonux/totalrecall/internal/archive"
"codeberg.org/snonux/totalrecall/internal/audio"
+ appconfig "codeberg.org/snonux/totalrecall/internal/config"
"codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/phonetic"
"codeberg.org/snonux/totalrecall/internal/translation"
@@ -135,7 +136,10 @@ const (
// DefaultConfig returns default GUI configuration
func DefaultConfig() *Config {
- homeDir, _ := os.UserHomeDir()
+ homeDir, err := appconfig.HomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
// Use XDG Base Directory specification for state data
outputDir := filepath.Join(homeDir, ".local", "state", "totalrecall", "cards")
audioDefaults := audio.DefaultProviderConfig()
@@ -1362,7 +1366,10 @@ func (a *Application) onExportToAnki() {
deckNameEntry.SetPlaceHolder("Bulgarian Vocabulary")
// Export directory selection
- homeDir, _ := os.UserHomeDir()
+ homeDir, err := appconfig.HomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
defaultExportDir := homeDir // Changed from Downloads to home directory
selectedDir := defaultExportDir
@@ -1533,7 +1540,10 @@ func (a *Application) onArchive() {
// Function to perform the archive
performArchive := func() {
// Get the cards directory path
- home, _ := os.UserHomeDir()
+ home, err := appconfig.HomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
cardsDir := filepath.Join(home, ".local", "state", "totalrecall", "cards")
// Archive the cards
diff --git a/internal/processor/processor.go b/internal/processor/processor.go
index 2343120..5b426bb 100644
--- a/internal/processor/processor.go
+++ b/internal/processor/processor.go
@@ -16,6 +16,7 @@ import (
"codeberg.org/snonux/totalrecall/internal/audio"
"codeberg.org/snonux/totalrecall/internal/batch"
"codeberg.org/snonux/totalrecall/internal/cli"
+ appconfig "codeberg.org/snonux/totalrecall/internal/config"
"codeberg.org/snonux/totalrecall/internal/gui"
"codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/phonetic"
@@ -676,7 +677,10 @@ func (p *Processor) RunGUIMode() error {
// Only set OutputDir if it was explicitly provided via flag
// Check if the outputDir is different from the default
- home, _ := os.UserHomeDir()
+ home, err := appconfig.HomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
+ }
defaultOutputDir := filepath.Join(home, "Downloads")
if p.flags.OutputDir != defaultOutputDir {
// User explicitly set a different output directory