// Package main implements the ML TUI package main import ( "log" "os" "os/signal" "path/filepath" "syscall" tea "github.com/charmbracelet/bubbletea" "github.com/jfraeys/fetch_ml/cmd/tui/internal/config" "github.com/jfraeys/fetch_ml/cmd/tui/internal/controller" "github.com/jfraeys/fetch_ml/cmd/tui/internal/model" "github.com/jfraeys/fetch_ml/cmd/tui/internal/services" "github.com/jfraeys/fetch_ml/cmd/tui/internal/view" "github.com/jfraeys/fetch_ml/internal/auth" "github.com/jfraeys/fetch_ml/internal/logging" ) // AppModel represents the main application model for the TUI. type AppModel struct { state model.State controller *controller.Controller } // Init initializes the TUI application. func (m AppModel) Init() tea.Cmd { return m.controller.Init() } // Update handles application updates and messages. func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newState, cmd := m.controller.Update(msg, m.state) m.state = newState return m, cmd } // View renders the TUI interface. func (m AppModel) View() string { return view.Render(m.state) } func main() { // Redirect logs to file to prevent TUI disruption homeDir, _ := os.UserHomeDir() logDir := filepath.Join(homeDir, ".ml", "logs") os.MkdirAll(logDir, 0755) logFile, logErr := os.OpenFile(filepath.Join(logDir, "tui.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if logErr == nil { log.SetOutput(logFile) defer logFile.Close() } // Parse authentication flags authFlags := auth.ParseAuthFlags() if err := auth.ValidateFlags(authFlags); err != nil { log.Fatalf("Authentication flag error: %v", err) } // Get API key from various sources apiKey := auth.GetAPIKeyFromSources(authFlags) var ( cfg *config.Config cliConfig *config.CLIConfig cliConfPath string ) configFlag := authFlags.ConfigFile // Only support TOML configuration var err error cliConfig, cliConfPath, err = config.LoadCLIConfig(configFlag) if err != nil { if configFlag != "" { log.Fatalf("Failed to load TOML config %s: %v", configFlag, err) } // Provide helpful error message for data scientists log.Printf("=== Fetch ML TUI - Configuration Required ===") log.Printf("") log.Printf("Error: %v", err) log.Printf("") log.Printf("To get started with the TUI, you need to initialize your configuration:") log.Printf("") log.Printf("Option 1: Using the Zig CLI (Recommended)") log.Printf(" 1. Build the CLI: cd cli && make build") log.Printf(" 2. Initialize config: ./cli/zig-out/bin/ml init") log.Printf(" 3. Edit ~/.ml/config.toml with your settings") log.Printf(" 4. Run TUI: ./bin/tui") log.Printf("") log.Printf("Option 2: Manual Configuration") log.Printf(" 1. Create directory: mkdir -p ~/.ml") log.Printf(" 2. Create config: touch ~/.ml/config.toml") log.Printf(" 3. Add your settings to the file") log.Printf(" 4. Run TUI: ./bin/tui") log.Printf("") log.Printf("Example ~/.ml/config.toml:") log.Printf(" mode = \"dev\"") log.Printf(" # Paths auto-resolve based on mode:") log.Printf(" # dev mode: ./data/dev/experiments") log.Printf(" # prod mode: ./data/prod/experiments") log.Printf(" api_key = \"your_api_key_here\"") log.Printf("") log.Printf("For more help, see: https://github.com/jfraeys/fetch_ml/docs") os.Exit(1) } cfg, err = cliConfig.ToTUIConfig() if err != nil { log.Fatalf("Failed to convert CLI config to TUI config: %v", err) } log.Printf("Loaded TOML configuration from %s", cliConfPath) // Force local mode - TUI runs on server with direct filesystem access cfg.Host = "" // Clear BasePath to force mode-based path resolution cfg.BasePath = "" // Validate authentication configuration if err := cfg.Auth.ValidateAuthConfig(); err != nil { log.Fatalf("Invalid authentication configuration: %v", err) } if err := cfg.Validate(); err != nil { log.Fatalf("Invalid configuration: %v", err) } // Test authentication if enabled if cfg.Auth.Enabled { // Use API key from CLI config if available, otherwise use from flags var effectiveAPIKey string switch { case cliConfig != nil && cliConfig.APIKey != "": effectiveAPIKey = cliConfig.APIKey case apiKey != "": effectiveAPIKey = apiKey default: log.Fatal("Authentication required but no API key provided") } if _, err := cfg.Auth.ValidateAPIKey(effectiveAPIKey); err != nil { log.Fatalf("Authentication failed: %v", err) } } srv, err := services.NewMLServer(cfg) if err != nil { log.Fatalf("Failed to initialize local server: %v", err) } defer func() { if err := srv.Close(); err != nil { log.Printf("server close error: %v", err) } }() // TaskQueue is optional for local mode tq, err := services.NewTaskQueue(cfg) if err != nil { log.Printf("Warning: Failed to connect to Redis: %v", err) log.Printf("Continuing without task queue functionality") tq = nil } if tq != nil { defer func() { if err := tq.Close(); err != nil { log.Printf("task queue close error: %v", err) } }() } // Initialize logger with file output only (prevents TUI disruption) logFilePath := filepath.Join(logDir, "tui.log") logger := logging.NewFileLogger(-4, false, logFilePath) // -4 = slog.LevelError // Initialize State and Controller var effectiveAPIKey string if cliConfig != nil && cliConfig.APIKey != "" { effectiveAPIKey = cliConfig.APIKey } else { effectiveAPIKey = apiKey } initialState := model.InitialState(effectiveAPIKey) ctrl := controller.New(cfg, srv, tq, logger) appModel := AppModel{ state: initialState, controller: ctrl, } // Run TUI program with graceful shutdown on SIGINT/SIGTERM p := tea.NewProgram(appModel, tea.WithAltScreen(), tea.WithMouseAllMotion()) // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan logger.Info("Received shutdown signal, closing TUI...") p.Quit() }() if _, err := p.Run(); err != nil { _ = p.ReleaseTerminal() logger.Error("Error running TUI", "error", err) return } // Ensure terminal is released and resources are closed via defer statements _ = p.ReleaseTerminal() logger.Info("TUI shutdown complete") }