diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/archive/archive.go | 26 | ||||
| -rw-r--r-- | internal/gui/app.go | 23 | ||||
| -rw-r--r-- | internal/models/lister.go | 8 | ||||
| -rw-r--r-- | internal/story/runner.go | 8 |
4 files changed, 59 insertions, 6 deletions
diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 9d58935..8ea7e7a 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -7,8 +7,24 @@ import ( "time" ) -// ArchiveCards moves the cards directory to an archive with timestamp -func ArchiveCards(cardsDir string) error { +// Archiver moves the cards directory into a timestamped archive folder under +// the parent state directory. Implementations are typically injected at +// composition roots (cmd, GUI) so callers depend on this abstraction rather +// than package-level functions alone. +type Archiver interface { + ArchiveCards(cardsDir string) error +} + +// DefaultArchiver implements Archiver using the local filesystem. +type DefaultArchiver struct{} + +// ArchiveCards implements Archiver. +func (DefaultArchiver) ArchiveCards(cardsDir string) error { + return archiveCards(cardsDir) +} + +// archiveCards moves the cards directory to an archive with timestamp. +func archiveCards(cardsDir string) error { // Check if cards directory exists if _, err := os.Stat(cardsDir); os.IsNotExist(err) { return fmt.Errorf("cards directory does not exist: %s", cardsDir) @@ -44,3 +60,9 @@ func ArchiveCards(cardsDir string) error { fmt.Printf("Cards directory archived to: %s\n", archivePath) return nil } + +// ArchiveCards archives cards using DefaultArchiver. It exists for call sites +// that do not use dependency injection (e.g. tests and legacy scripts). +func ArchiveCards(cardsDir string) error { + return (DefaultArchiver{}).ArchiveCards(cardsDir) +} diff --git a/internal/gui/app.go b/internal/gui/app.go index 6c1cf21..0c821d4 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -30,6 +30,12 @@ import ( "codeberg.org/snonux/totalrecall/internal/translation" ) +// App is the runnable GUI application constructed at the composition root +// (cmd/totalrecall). Callers invoke Run() to start the Fyne event loop. +type App interface { + Run() +} + // Application represents the main GUI application type Application struct { // Fyne components @@ -88,6 +94,7 @@ type Application struct { // Configuration config *Config + archiver archive.Archiver audioConfig *audio.Config phoneticFetcher *phonetic.Fetcher translator *translation.Translator @@ -144,6 +151,9 @@ type Config struct { // constructing new instances from the provider/key fields above. PhoneticFetcher *phonetic.Fetcher Translator *translation.Translator + // Archiver moves the cards directory to a timestamped archive; nil uses + // archive.DefaultArchiver. + Archiver archive.Archiver } // DefaultConfig returns default GUI configuration @@ -170,13 +180,17 @@ func DefaultConfig() *Config { } } -// New creates a new GUI application -// New constructs and returns a fully initialised Application for the given config. +// New constructs and returns a fully initialised App for the given config. // A nil config receives all defaults. The Fyne application and UI are created here; // callers should call Run() to start the event loop. -func New(config *Config) *Application { +func New(config *Config) App { config = applyConfigDefaults(config) + arch := config.Archiver + if arch == nil { + arch = archive.DefaultArchiver{} + } + if err := os.MkdirAll(config.OutputDir, 0755); err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to create output directory %q: %v\n", config.OutputDir, err) } @@ -188,6 +202,7 @@ func New(config *Config) *Application { a := &Application{ app: myApp, config: config, + archiver: arch, ctx: ctx, cancel: cancel, savedCards: make([]anki.Card, 0), @@ -1479,7 +1494,7 @@ func (a *Application) performArchive() { } cardsDir := filepath.Join(home, ".local", "state", "totalrecall", "cards") - if err := archive.ArchiveCards(cardsDir); err != nil { + if err := a.archiver.ArchiveCards(cardsDir); err != nil { dialog.ShowError(err, a.window) return } diff --git a/internal/models/lister.go b/internal/models/lister.go index 2f05988..08f4f0d 100644 --- a/internal/models/lister.go +++ b/internal/models/lister.go @@ -14,6 +14,12 @@ import ( "codeberg.org/snonux/totalrecall/internal/httpctx" ) +// ModelLister lists available OpenAI and Gemini models to the configured +// writer. *Lister satisfies this interface. +type ModelLister interface { + ListAvailableModels() error +} + type openAIModelLister interface { ListModels(context.Context) (openai.ModelsList, error) } @@ -32,6 +38,8 @@ type Lister struct { out io.Writer } +var _ ModelLister = (*Lister)(nil) + // NewLister creates a new model lister. func NewLister(openAIKey, geminiKey string, out io.Writer) *Lister { lister := &Lister{ diff --git a/internal/story/runner.go b/internal/story/runner.go index c9571a6..07e0e12 100644 --- a/internal/story/runner.go +++ b/internal/story/runner.go @@ -9,6 +9,12 @@ import ( "codeberg.org/snonux/totalrecall/internal/batch" ) +// StoryRunner runs the vocabulary story generation pipeline from a batch file +// path. *Runner satisfies this interface. +type StoryRunner interface { + Run(batchFile string) error +} + // ttsTodoContent is written to story_tts_todo.txt as a fallback when Gemini TTS // narration fails or no API key is available. It documents the original // ElevenLabs integration placeholder for reference. @@ -184,6 +190,8 @@ func (r *Runner) Run(batchFile string) error { return r.handleNarration(result.StoryText, slug, comicsDir) } +var _ StoryRunner = (*Runner)(nil) + // drawComicPages generates all 12 comic pages and assembles them into a PDF. // panelScript carries the explicit per-panel visual descriptions from Gemini so // each panel illustrates the correct story beat in narrative order. |
