diff options
| -rw-r--r-- | internal/anki/apkg_generator.go | 31 | ||||
| -rw-r--r-- | internal/anki/apkg_generator_test.go | 8 | ||||
| -rw-r--r-- | internal/cli/command.go | 6 | ||||
| -rw-r--r-- | internal/config/home.go | 22 | ||||
| -rw-r--r-- | internal/config/home_test.go | 25 | ||||
| -rw-r--r-- | internal/gui/app.go | 16 | ||||
| -rw-r--r-- | internal/processor/processor.go | 6 |
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 |
