diff --git a/internal/app/cli/cli.go b/internal/app/cli/cli.go index 205a7eb..26c5033 100644 --- a/internal/app/cli/cli.go +++ b/internal/app/cli/cli.go @@ -11,6 +11,7 @@ import ( "fuku/internal/config/logger" ) +// Help text constants const ( Usage = `Usage: fuku Run services with default profile (with TUI) diff --git a/internal/app/cli/commands.go b/internal/app/cli/commands.go index 33f13f7..c077ac8 100644 --- a/internal/app/cli/commands.go +++ b/internal/app/cli/commands.go @@ -9,6 +9,7 @@ import ( // CommandType represents the type of CLI command type CommandType int +// Command type values const ( CommandRun CommandType = iota CommandLogs diff --git a/internal/app/logs/client.go b/internal/app/logs/client.go index cafdef9..ede0013 100644 --- a/internal/app/logs/client.go +++ b/internal/app/logs/client.go @@ -23,6 +23,7 @@ type Client interface { Close() error } +// client implements the Client interface type client struct { conn net.Conn formatter *LogFormatter diff --git a/internal/app/logs/hub.go b/internal/app/logs/hub.go index c94c34d..9f455be 100644 --- a/internal/app/logs/hub.go +++ b/internal/app/logs/hub.go @@ -48,6 +48,7 @@ func (c *ClientConn) ShouldReceive(service string) bool { return c.Services[service] } +// hub implements the Hub interface type hub struct { clients map[*ClientConn]bool register chan *ClientConn diff --git a/internal/app/logs/protocol.go b/internal/app/logs/protocol.go index f6c597e..cae7318 100644 --- a/internal/app/logs/protocol.go +++ b/internal/app/logs/protocol.go @@ -8,6 +8,7 @@ type Broadcaster interface { // MessageType represents the type of message in the wire protocol type MessageType string +// Message types for the wire protocol const ( // MessageSubscribe is sent from client to server to subscribe to services MessageSubscribe MessageType = "subscribe" diff --git a/internal/app/logs/runner.go b/internal/app/logs/runner.go index 43c691e..977942a 100644 --- a/internal/app/logs/runner.go +++ b/internal/app/logs/runner.go @@ -16,6 +16,7 @@ type Runner interface { Run(profile string, services []string) int } +// runner implements the Runner interface type runner struct { client Client log logger.Logger diff --git a/internal/app/logs/server.go b/internal/app/logs/server.go index 30bd972..4e0daad 100644 --- a/internal/app/logs/server.go +++ b/internal/app/logs/server.go @@ -24,6 +24,7 @@ type Server interface { SocketPath() string } +// server implements the Server interface type server struct { profile string socketPath string @@ -124,6 +125,7 @@ func (s *server) Broadcast(service, message string) { } } +// cleanupStaleSocket removes stale socket file if not in use func (s *server) cleanupStaleSocket() error { if _, err := os.Stat(s.socketPath); os.IsNotExist(err) { return nil @@ -141,6 +143,7 @@ func (s *server) cleanupStaleSocket() error { return os.Remove(s.socketPath) } +// acceptConnections handles incoming client connections func (s *server) acceptConnections(ctx context.Context) { for s.running.Load() { conn, err := s.listener.Accept() @@ -162,6 +165,7 @@ func (s *server) acceptConnections(ctx context.Context) { } } +// handleConnection processes a single client connection func (s *server) handleConnection(ctx context.Context, conn net.Conn) { defer conn.Close() diff --git a/internal/app/monitor/monitor.go b/internal/app/monitor/monitor.go index 7f33d51..add2e0f 100644 --- a/internal/app/monitor/monitor.go +++ b/internal/app/monitor/monitor.go @@ -18,6 +18,7 @@ type Monitor interface { GetStats(ctx context.Context, pid int) (Stats, error) } +// monitor implements the Monitor interface type monitor struct{} // NewMonitor creates a new Monitor instance @@ -25,6 +26,7 @@ func NewMonitor() Monitor { return &monitor{} } +// GetStats retrieves CPU and memory statistics for a process func (m *monitor) GetStats(ctx context.Context, pid int) (Stats, error) { if pid <= 0 || pid > math.MaxInt32 { return Stats{}, nil diff --git a/internal/app/runner/discovery.go b/internal/app/runner/discovery.go index a72b1c7..aa552f9 100644 --- a/internal/app/runner/discovery.go +++ b/internal/app/runner/discovery.go @@ -19,6 +19,7 @@ type Discovery interface { Resolve(profile string) ([]Tier, error) } +// discovery implements the Discovery interface type discovery struct { cfg *config.Config topology *config.Topology diff --git a/internal/app/runner/lifecycle.go b/internal/app/runner/lifecycle.go index 6f01514..5be25e0 100644 --- a/internal/app/runner/lifecycle.go +++ b/internal/app/runner/lifecycle.go @@ -16,6 +16,7 @@ type Lifecycle interface { Terminate(proc Process, timeout time.Duration) error } +// lifecycle implements the Lifecycle interface type lifecycle struct { log logger.Logger } @@ -59,10 +60,12 @@ func (l *lifecycle) Terminate(proc Process, timeout time.Duration) error { } } +// signalGroup sends a signal to the process group func (l *lifecycle) signalGroup(pid int, sig syscall.Signal) error { return syscall.Kill(-pid, sig) } +// forceKill sends SIGKILL to the process group func (l *lifecycle) forceKill(proc Process, pid int) error { if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil { l.log.Warn().Err(err).Msgf("Failed to SIGKILL process group, trying direct kill") diff --git a/internal/app/runner/process.go b/internal/app/runner/process.go index 3b5039f..6739e98 100644 --- a/internal/app/runner/process.go +++ b/internal/app/runner/process.go @@ -16,6 +16,7 @@ type Process interface { StderrReader() *io.PipeReader } +// process implements the Process interface type process struct { name string cmd *exec.Cmd @@ -25,22 +26,27 @@ type process struct { stderrReader *io.PipeReader } +// Name returns the service name func (p *process) Name() string { return p.name } +// Cmd returns the underlying exec command func (p *process) Cmd() *exec.Cmd { return p.cmd } +// Done returns a channel that closes when the process exits func (p *process) Done() <-chan struct{} { return p.done } +// Ready returns a channel that receives when the process is ready func (p *process) Ready() <-chan error { return p.ready } +// SignalReady signals the ready channel with optional error func (p *process) SignalReady(err error) { if err != nil { select { @@ -52,10 +58,12 @@ func (p *process) SignalReady(err error) { close(p.ready) } +// StdoutReader returns the stdout pipe reader func (p *process) StdoutReader() *io.PipeReader { return p.stdoutReader } +// StderrReader returns the stderr pipe reader func (p *process) StderrReader() *io.PipeReader { return p.stderrReader } diff --git a/internal/app/runner/readiness.go b/internal/app/runner/readiness.go index cafe094..5a95423 100644 --- a/internal/app/runner/readiness.go +++ b/internal/app/runner/readiness.go @@ -21,6 +21,7 @@ type Readiness interface { Check(ctx context.Context, name string, service *config.Service, process Process) } +// readiness implements the Readiness interface type readiness struct { log logger.Logger } diff --git a/internal/app/runner/runner.go b/internal/app/runner/runner.go index 523a061..670f48d 100644 --- a/internal/app/runner/runner.go +++ b/internal/app/runner/runner.go @@ -21,6 +21,7 @@ type Runner interface { Run(ctx context.Context, profile string) error } +// runner implements the Runner interface type runner struct { cfg *config.Config discovery Discovery @@ -158,6 +159,7 @@ func (r *runner) Run(ctx context.Context, profile string) error { return nil } +// runStartupPhase handles the service startup phase and waits for completion or interruption func (r *runner) runStartupPhase(ctx context.Context, cancel context.CancelFunc, tiers []Tier, registry Registry, sigChan chan os.Signal, commandChan <-chan runtime.Command) error { startupDone := make(chan struct{}, 1) @@ -217,6 +219,7 @@ func (r *runner) runStartupPhase(ctx context.Context, cancel context.CancelFunc, } } +// runServicePhase runs the main event loop handling signals and commands func (r *runner) runServicePhase(ctx context.Context, cancel context.CancelFunc, sigChan chan os.Signal, registry Registry, commandChan <-chan runtime.Command) { for { select { @@ -246,6 +249,7 @@ func (r *runner) runServicePhase(ctx context.Context, cancel context.CancelFunc, } } +// handleCommand processes a command and returns true if shutdown is requested func (r *runner) handleCommand(ctx context.Context, cmd runtime.Command, registry Registry) bool { switch cmd.Type { case runtime.CommandStopService: @@ -274,6 +278,7 @@ func (r *runner) handleCommand(ctx context.Context, cmd runtime.Command, registr return false } +// stopService stops a single service by name func (r *runner) stopService(serviceName string, registry Registry) { lookup := registry.Get(serviceName) if !lookup.Exists { @@ -293,6 +298,7 @@ func (r *runner) stopService(serviceName string, registry Registry) { r.service.Stop(lookup.Proc) } +// restartService stops and starts a service, or just starts if not running func (r *runner) restartService(ctx context.Context, serviceName string, registry Registry) { lookup := registry.Get(serviceName) @@ -346,6 +352,7 @@ func (r *runner) restartService(ctx context.Context, serviceName string, registr }() } +// startTier starts all services in a tier concurrently and returns failed service names func (r *runner) startTier(ctx context.Context, tierName string, tierServices []string, registry Registry) []string { failedChan := make(chan string, len(tierServices)) procChan := make(chan Process, len(tierServices)) @@ -418,6 +425,7 @@ func (r *runner) startTier(ctx context.Context, tierName string, tierServices [] return failedServices } +// startServiceWithRetry attempts to start a service with configurable retries func (r *runner) startServiceWithRetry(ctx context.Context, name string, tierName string, service *config.Service) (Process, error) { var lastErr error @@ -493,6 +501,7 @@ func (r *runner) startServiceWithRetry(ctx context.Context, name string, tierNam return nil, fmt.Errorf("%w after %d attempts: %w", errors.ErrMaxRetriesExceeded, config.RetryAttempt, lastErr) } +// startAllTiers starts services tier by tier in order func (r *runner) startAllTiers(ctx context.Context, tiers []Tier, registry Registry) { for tierIdx, tier := range tiers { if len(tier.Services) > 0 { @@ -524,6 +533,7 @@ func (r *runner) startAllTiers(ctx context.Context, tiers []Tier, registry Regis } } +// shutdown stops all services in reverse order and waits for completion func (r *runner) shutdown(registry Registry) { processes := registry.SnapshotReverse() diff --git a/internal/app/runner/service.go b/internal/app/runner/service.go index 4765638..e153782 100644 --- a/internal/app/runner/service.go +++ b/internal/app/runner/service.go @@ -16,6 +16,7 @@ import ( "fuku/internal/config/logger" ) +// Scanner buffer size constants const ( // scannerBufferSize is the initial buffer size for reading service output (64KB) scannerBufferSize = 64 * 1024 @@ -30,6 +31,7 @@ type Service interface { SetBroadcaster(broadcaster logs.Broadcaster) } +// service implements the Service interface type service struct { lifecycle Lifecycle readiness Readiness @@ -134,6 +136,7 @@ func (s *service) SetBroadcaster(broadcaster logs.Broadcaster) { s.broadcaster = broadcaster } +// teeStream reads from source and writes to destination while logging output func (s *service) teeStream(src io.Reader, dst *io.PipeWriter, serviceName, streamType string) { scanner := bufio.NewScanner(src) scanner.Buffer(make([]byte, scannerBufferSize), scannerMaxBufferSize) @@ -156,11 +159,13 @@ func (s *service) teeStream(src io.Reader, dst *io.PipeWriter, serviceName, stre } } +// startDraining begins consuming both stdout and stderr pipes func (s *service) startDraining(stdout, stderr *io.PipeReader) { go s.drainPipe(stdout) go s.drainPipe(stderr) } +// drainPipe consumes all data from a pipe until EOF func (s *service) drainPipe(reader *io.PipeReader) { scanner := bufio.NewScanner(reader) scanner.Buffer(make([]byte, scannerBufferSize), scannerMaxBufferSize) @@ -173,6 +178,7 @@ func (s *service) drainPipe(reader *io.PipeReader) { } } +// handleReadinessCheck sets up the appropriate readiness check based on service config func (s *service) handleReadinessCheck(ctx context.Context, name string, svc *config.Service, proc *process, stdout, stderr *io.PipeReader) { if svc.Readiness == nil { proc.SignalReady(nil) diff --git a/internal/app/runner/workerpool.go b/internal/app/runner/workerpool.go index 5116f5f..4be3975 100644 --- a/internal/app/runner/workerpool.go +++ b/internal/app/runner/workerpool.go @@ -12,6 +12,7 @@ type WorkerPool interface { Release() } +// workerPool implements the WorkerPool interface type workerPool struct { sem chan struct{} } diff --git a/internal/app/runtime/commands.go b/internal/app/runtime/commands.go index 253aa3f..df60cb4 100644 --- a/internal/app/runtime/commands.go +++ b/internal/app/runtime/commands.go @@ -8,6 +8,7 @@ import ( // CommandType represents the type of command type CommandType string +// Command types for service control const ( CommandStopService CommandType = "stop_service" CommandRestartService CommandType = "restart_service" @@ -37,6 +38,7 @@ type CommandBus interface { Close() } +// commandBus implements the CommandBus interface type commandBus struct { subscribers []chan Command mu sync.RWMutex @@ -103,6 +105,7 @@ func (cb *commandBus) Close() { cb.subscribers = nil } +// unsubscribe removes a channel from subscribers and closes it func (cb *commandBus) unsubscribe(ch chan Command) { cb.mu.Lock() defer cb.mu.Unlock() @@ -126,6 +129,7 @@ func NewNoOpCommandBus() CommandBus { return &noOpCommandBus{} } +// Subscribe returns a channel that closes when context is cancelled func (ncb *noOpCommandBus) Subscribe(ctx context.Context) <-chan Command { ch := make(chan Command) @@ -137,6 +141,8 @@ func (ncb *noOpCommandBus) Subscribe(ctx context.Context) <-chan Command { return ch } +// Publish is a no-op func (ncb *noOpCommandBus) Publish(cmd Command) {} +// Close is a no-op func (ncb *noOpCommandBus) Close() {} diff --git a/internal/app/runtime/events.go b/internal/app/runtime/events.go index 40582b9..28f9972 100644 --- a/internal/app/runtime/events.go +++ b/internal/app/runtime/events.go @@ -9,6 +9,7 @@ import ( // EventType represents the type of event type EventType string +// Event types for runtime notifications const ( EventProfileResolved EventType = "profile_resolved" EventPhaseChanged EventType = "phase_changed" @@ -26,6 +27,7 @@ const ( // Phase represents the application phase type Phase string +// Phase values for application lifecycle const ( PhaseStartup Phase = "startup" PhaseRunning Phase = "running" @@ -124,6 +126,7 @@ type EventBus interface { Close() } +// eventBus implements the EventBus interface type eventBus struct { subscribers []chan Event mu sync.RWMutex @@ -201,6 +204,7 @@ func (eb *eventBus) Close() { eb.subscribers = nil } +// unsubscribe removes a channel from subscribers and closes it func (eb *eventBus) unsubscribe(ch chan Event) { eb.mu.Lock() defer eb.mu.Unlock() @@ -224,6 +228,7 @@ func NewNoOpEventBus() EventBus { return &noOpEventBus{} } +// Subscribe returns an immediately closed channel func (neb *noOpEventBus) Subscribe(ctx context.Context) <-chan Event { ch := make(chan Event) close(ch) @@ -231,6 +236,8 @@ func (neb *noOpEventBus) Subscribe(ctx context.Context) <-chan Event { return ch } +// Publish is a no-op func (neb *noOpEventBus) Publish(event Event) {} +// Close is a no-op func (neb *noOpEventBus) Close() {} diff --git a/internal/app/ui/components/blink.go b/internal/app/ui/components/blink.go index 2323234..4e13944 100644 --- a/internal/app/ui/components/blink.go +++ b/internal/app/ui/components/blink.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/lipgloss" ) +// Blink animation constants const ( empty = "◯" full = "◉" @@ -46,8 +47,10 @@ type Blink struct { state state } +// state represents the current phase of the blink animation type state int +// Animation state phases const ( settle state = iota // Settle phase (empty) beat1 // First beat (full) diff --git a/internal/app/ui/components/constants.go b/internal/app/ui/components/constants.go index 05195da..bf57d67 100644 --- a/internal/app/ui/components/constants.go +++ b/internal/app/ui/components/constants.go @@ -57,6 +57,7 @@ const ( ColWidthUptime = 8 ) +// Unit conversion constants const ( MBToGB = 1024 ) diff --git a/internal/app/ui/components/layout.go b/internal/app/ui/components/layout.go index 4190af8..d65ad0a 100644 --- a/internal/app/ui/components/layout.go +++ b/internal/app/ui/components/layout.go @@ -123,6 +123,7 @@ func Truncate(s string, maxWidth int) string { return ellipsis } +// buildTopBorder builds the top border with title and status func buildTopBorder(border func(string) string, titleText, topRightText string, middleWidth int) string { hLine := func(n int) string { return strings.Repeat(BorderHorizontal, n) } spacer := PanelTitleSpacer.Render("") @@ -138,6 +139,7 @@ func buildTopBorder(border func(string) string, titleText, topRightText string, return result } +// buildBottomBorder builds the bottom border with version func buildBottomBorder(border func(string) string, bottomRightText string, innerWidth int) string { hLine := func(n int) string { return strings.Repeat(BorderHorizontal, n) } @@ -160,6 +162,7 @@ func buildBottomBorder(border func(string) string, bottomRightText string, inner return result } +// splitAndPadContent splits content into lines and pads to fill height func splitAndPadContent(content string, height int) []string { lines := strings.Split(content, "\n") @@ -174,6 +177,7 @@ func splitAndPadContent(content string, height int) []string { return lines } +// appendContentLines adds content lines with borders and padding func appendContentLines(result, contentLines []string, innerWidth int, border func(string) string) []string { for _, line := range contentLines { lineWidth := lipgloss.Width(line) @@ -190,6 +194,7 @@ func appendContentLines(result, contentLines []string, innerWidth int, border fu return result } +// splitAtDisplayWidth splits a string at half its display width func splitAtDisplayWidth(s string) (left, right string) { runes := []rune(s) totalWidth := lipgloss.Width(s) @@ -212,6 +217,7 @@ func splitAtDisplayWidth(s string) (left, right string) { return string(runes[:splitIdx]), string(runes[splitIdx:]) } +// renderFooter renders the footer with help and tips func renderFooter(help, tips string, width int) string { content := FooterStyle.Render(help) diff --git a/internal/app/ui/services/controller.go b/internal/app/ui/services/controller.go index 725727a..2032014 100644 --- a/internal/app/ui/services/controller.go +++ b/internal/app/ui/services/controller.go @@ -18,17 +18,19 @@ type Controller interface { HandleStopped(ctx context.Context, service *ServiceState) bool } +// controller implements the Controller interface type controller struct { command runtime.CommandBus } -// NewController creates a new service controller +// NewController creates a new controller with the given command bus func NewController(command runtime.CommandBus) Controller { return &controller{ command: command, } } +// Start requests a service start if it's currently stopped func (c *controller) Start(ctx context.Context, service *ServiceState) { if service == nil || service.FSM == nil { return @@ -45,6 +47,7 @@ func (c *controller) Start(ctx context.Context, service *ServiceState) { _ = service.FSM.Event(ctx, Start) } +// Stop requests a service stop if it's currently running func (c *controller) Stop(ctx context.Context, service *ServiceState) { if service == nil || service.FSM == nil { return @@ -62,6 +65,7 @@ func (c *controller) Stop(ctx context.Context, service *ServiceState) { _ = service.FSM.Event(ctx, Stop) } +// Restart requests a service restart if it's running, failed, or stopped func (c *controller) Restart(ctx context.Context, service *ServiceState) { if service == nil || service.FSM == nil { return @@ -80,10 +84,12 @@ func (c *controller) Restart(ctx context.Context, service *ServiceState) { _ = service.FSM.Event(ctx, Restart) } +// StopAll sends a command to stop all services func (c *controller) StopAll() { c.command.Publish(runtime.Command{Type: runtime.CommandStopAll}) } +// HandleStarting updates service state when a process starts func (c *controller) HandleStarting(ctx context.Context, service *ServiceState, pid int) { if service == nil { return @@ -95,6 +101,7 @@ func (c *controller) HandleStarting(ctx context.Context, service *ServiceState, } } +// HandleReady updates service state when it becomes ready func (c *controller) HandleReady(ctx context.Context, service *ServiceState) { if service == nil { return @@ -105,6 +112,7 @@ func (c *controller) HandleReady(ctx context.Context, service *ServiceState) { } } +// HandleFailed updates service state when it fails func (c *controller) HandleFailed(ctx context.Context, service *ServiceState) { if service == nil { return @@ -115,6 +123,7 @@ func (c *controller) HandleFailed(ctx context.Context, service *ServiceState) { } } +// HandleStopped updates service state when it stops, returns true if it was restarting func (c *controller) HandleStopped(ctx context.Context, service *ServiceState) bool { if service == nil { return false diff --git a/internal/app/ui/services/model.go b/internal/app/ui/services/model.go index 793774a..64ab75a 100644 --- a/internal/app/ui/services/model.go +++ b/internal/app/ui/services/model.go @@ -20,6 +20,7 @@ import ( // Status represents the status of a service type Status string +// Status values for service lifecycle const ( StatusStarting Status = "Starting" StatusRunning Status = "Running" diff --git a/internal/app/ui/services/monitor.go b/internal/app/ui/services/monitor.go index 4a21942..dcb7d7c 100644 --- a/internal/app/ui/services/monitor.go +++ b/internal/app/ui/services/monitor.go @@ -31,6 +31,7 @@ func (m *Model) applyStatsUpdate(msg statsUpdateMsg) { } } +// updateBlinkAnimations updates blink state for services in transition states func (m *Model) updateBlinkAnimations() bool { hasActiveBlinking := false @@ -58,6 +59,7 @@ func (m *Model) updateBlinkAnimations() bool { return hasActiveBlinking } +// getUptime returns formatted uptime string for a service func (m *Model) getUptime(service *ServiceState) string { if service.Status == StatusStopped || service.Status == StatusFailed || service.Monitor.StartTime.IsZero() { return "" @@ -75,6 +77,7 @@ func (m *Model) getUptime(service *ServiceState) string { return pad(minutes) + ":" + pad(seconds) } +// getCPU returns formatted CPU usage for a service func (m *Model) getCPU(service *ServiceState) string { if !m.isServiceMonitored(service) { return "" @@ -83,6 +86,7 @@ func (m *Model) getCPU(service *ServiceState) string { return fmt.Sprintf("%.1f%%", service.Monitor.CPU) } +// getMem returns formatted memory usage for a service func (m *Model) getMem(service *ServiceState) string { if !m.isServiceMonitored(service) { return "" @@ -95,6 +99,7 @@ func (m *Model) getMem(service *ServiceState) string { return fmt.Sprintf("%.1fGB", service.Monitor.MEM/components.MBToGB) } +// getPID returns the process ID string for a running service func (m *Model) getPID(service *ServiceState) string { if service.Status == StatusRunning && service.Monitor.PID != 0 { return fmt.Sprintf("%d", service.Monitor.PID) @@ -103,10 +108,12 @@ func (m *Model) getPID(service *ServiceState) string { return "" } +// isServiceMonitored returns true if service has valid monitoring data func (m *Model) isServiceMonitored(service *ServiceState) bool { return service.Status != StatusStopped && service.Status != StatusFailed && service.Monitor.PID != 0 } +// pad formats a number with leading zero func pad(n int) string { return fmt.Sprintf("%02d", n) } @@ -123,11 +130,13 @@ func statsWorkerCmd(ctx context.Context, m *Model) tea.Cmd { }) } +// job represents a stats collection task type job struct { name string pid int } +// result holds stats collection output type result struct { name string stats ServiceStats @@ -169,6 +178,7 @@ func (m *Model) collectStats(ctx context.Context) map[string]ServiceStats { return stats } +// launchStatsWorkers spawns goroutines to collect stats concurrently func (m *Model) launchStatsWorkers(ctx context.Context, jobs []job, sem chan struct{}, results chan result) int { launched := 0 diff --git a/internal/app/ui/services/state.go b/internal/app/ui/services/state.go index 648df8e..7392ed3 100644 --- a/internal/app/ui/services/state.go +++ b/internal/app/ui/services/state.go @@ -35,6 +35,7 @@ const ( OnFailed = "enter_failed" ) +// newServiceFSM creates a state machine for service lifecycle management func newServiceFSM(service *ServiceState, loader *Loader) *fsm.FSM { serviceName := service.Name diff --git a/internal/app/ui/services/update.go b/internal/app/ui/services/update.go index 28675f2..409e67a 100644 --- a/internal/app/ui/services/update.go +++ b/internal/app/ui/services/update.go @@ -11,15 +11,19 @@ import ( "fuku/internal/app/ui/components" ) +// Tick timing constants const ( tickInterval = components.UITickInterval tickCounterMaximum = 1000000 ) +// eventMsg wraps a runtime event for tea messaging type eventMsg runtime.Event +// tickMsg signals a UI tick for animations type tickMsg time.Time +// channelClosedMsg signals the event channel has closed type channelClosedMsg struct{} // Update handles messages and updates the model @@ -89,6 +93,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +// handleKeyPress processes keyboard input func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if key.Matches(msg, m.ui.servicesKeys.ForceQuit) { m.log.Warn().Msg("TUI: Force quit requested, exiting immediately") @@ -138,6 +143,7 @@ func (m Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } +// handleUpKey moves selection up one service func (m Model) handleUpKey() (tea.Model, tea.Cmd) { if m.state.selected > 0 { m.state.selected-- @@ -148,6 +154,7 @@ func (m Model) handleUpKey() (tea.Model, tea.Cmd) { return m, nil } +// handleDownKey moves selection down one service func (m Model) handleDownKey() (tea.Model, tea.Cmd) { total := m.getTotalServices() if m.state.selected < total-1 { @@ -159,6 +166,7 @@ func (m Model) handleDownKey() (tea.Model, tea.Cmd) { return m, nil } +// handleStopKey toggles the selected service between running and stopped func (m Model) handleStopKey() (tea.Model, tea.Cmd) { service := m.getSelectedService() if service == nil || service.FSM == nil { @@ -178,6 +186,7 @@ func (m Model) handleStopKey() (tea.Model, tea.Cmd) { return m, nil } +// handleRestartKey restarts the selected service func (m Model) handleRestartKey() (tea.Model, tea.Cmd) { service := m.getSelectedService() if service == nil || service.FSM == nil { @@ -189,6 +198,7 @@ func (m Model) handleRestartKey() (tea.Model, tea.Cmd) { return m, m.loader.Model.Tick } +// handleEvent dispatches runtime events to specific handlers func (m Model) handleEvent(event runtime.Event) (tea.Model, tea.Cmd) { switch event.Type { case runtime.EventProfileResolved: @@ -215,6 +225,7 @@ func (m Model) handleEvent(event runtime.Event) (tea.Model, tea.Cmd) { return m, waitForEventCmd(m.eventChan) } +// handleProfileResolved initializes services from profile data func (m Model) handleProfileResolved(event runtime.Event) Model { data, ok := event.Data.(runtime.ProfileResolvedData) if !ok { @@ -250,6 +261,7 @@ func (m Model) handleProfileResolved(event runtime.Event) Model { return m } +// handlePhaseChanged updates the application phase state func (m Model) handlePhaseChanged(event runtime.Event) (Model, tea.Cmd) { data, ok := event.Data.(runtime.PhaseChangedData) if !ok { @@ -266,6 +278,7 @@ func (m Model) handlePhaseChanged(event runtime.Event) (Model, tea.Cmd) { return m, waitForEventCmd(m.eventChan) } +// handleTierStarting marks a tier as not ready when starting func (m Model) handleTierStarting(event runtime.Event) Model { data, ok := event.Data.(runtime.TierStartingData) if !ok { @@ -282,6 +295,7 @@ func (m Model) handleTierStarting(event runtime.Event) Model { return m } +// handleTierReady marks a tier as ready func (m Model) handleTierReady(event runtime.Event) Model { data, ok := event.Data.(runtime.TierReadyData) if !ok { @@ -298,6 +312,7 @@ func (m Model) handleTierReady(event runtime.Event) Model { return m } +// handleServiceStarting updates a service when it begins starting func (m Model) handleServiceStarting(event runtime.Event) Model { data, ok := event.Data.(runtime.ServiceStartingData) if !ok { @@ -313,6 +328,7 @@ func (m Model) handleServiceStarting(event runtime.Event) Model { return m } +// handleServiceReady updates a service when it becomes ready func (m Model) handleServiceReady(event runtime.Event) Model { data, ok := event.Data.(runtime.ServiceReadyData) if !ok { @@ -329,6 +345,7 @@ func (m Model) handleServiceReady(event runtime.Event) Model { return m } +// handleServiceFailed updates a service when it fails func (m Model) handleServiceFailed(event runtime.Event) Model { data, ok := event.Data.(runtime.ServiceFailedData) if !ok { @@ -344,6 +361,7 @@ func (m Model) handleServiceFailed(event runtime.Event) Model { return m } +// handleServiceStopped updates a service when it stops func (m Model) handleServiceStopped(event runtime.Event) Model { data, ok := event.Data.(runtime.ServiceStoppedData) if !ok { @@ -360,6 +378,7 @@ func (m Model) handleServiceStopped(event runtime.Event) Model { return m } +// waitForEventCmd returns a command that waits for the next event func waitForEventCmd(eventChan <-chan runtime.Event) tea.Cmd { return func() tea.Msg { event, ok := <-eventChan @@ -371,6 +390,7 @@ func waitForEventCmd(eventChan <-chan runtime.Event) tea.Cmd { } } +// tickCmd returns a command that sends a tick after the interval func tickCmd() tea.Cmd { return tea.Tick(tickInterval, func(t time.Time) tea.Msg { return tickMsg(t) diff --git a/internal/app/ui/services/view.go b/internal/app/ui/services/view.go index 6f8ae1f..d8b3e58 100644 --- a/internal/app/ui/services/view.go +++ b/internal/app/ui/services/view.go @@ -34,6 +34,7 @@ func (m Model) View() string { return components.AppContainerStyle.Render(panel) } +// renderStatus renders the status bar with phase and service counts func (m Model) renderStatus() string { ready := m.getReadyServices() total := m.getTotalServices() @@ -60,14 +61,17 @@ func (m Model) renderStatus() string { ) } +// renderVersion renders the version string func (m Model) renderVersion() string { return fmt.Sprintf("v%s", config.Version) } +// renderHelp renders the help text with keybindings func (m Model) renderHelp() string { return components.HelpStyle.Render(m.ui.help.View(m.ui.servicesKeys)) } +// renderTip returns the current rotating tip or empty string if tips disabled func (m Model) renderTip() string { if !m.ui.showTips { return "" @@ -79,6 +83,7 @@ func (m Model) renderTip() string { return components.Tips[tipIndex] } +// renderTitle renders the title with optional loading spinner func (m Model) renderTitle() string { if m.loader.Active { var b strings.Builder @@ -91,6 +96,7 @@ func (m Model) renderTitle() string { return "services" } +// renderServices renders the services list or empty state func (m Model) renderServices() string { if len(m.state.tiers) == 0 { return components.EmptyStateStyle.Render("No services configured") @@ -99,6 +105,7 @@ func (m Model) renderServices() string { return m.ui.servicesViewport.View() } +// getRowWidth returns the available width for service rows func (m Model) getRowWidth() int { rowWidth := m.ui.servicesViewport.Width if rowWidth < 1 { @@ -108,6 +115,7 @@ func (m Model) getRowWidth() int { return rowWidth } +// clampNameWidth constrains service name width to available space func (m Model) clampNameWidth(maxNameLen int) int { availableWidth := m.getRowWidth() - components.FixedColumnsWidth if availableWidth < components.ServiceNameMinWidth { @@ -121,6 +129,7 @@ func (m Model) clampNameWidth(maxNameLen int) int { return maxNameLen } +// renderColumnHeaders renders the column headers row func (m Model) renderColumnHeaders(maxNameLen int) string { rowWidth := m.getRowWidth() maxNameLen = m.clampNameWidth(maxNameLen) @@ -139,6 +148,7 @@ func (m Model) renderColumnHeaders(maxNameLen int) string { return components.ServiceHeaderStyle.Width(rowWidth).Render(header) } +// renderTier renders a tier header and its service rows func (m Model) renderTier(tier Tier, currentIdx *int, maxNameLen int) string { rowWidth := m.getRowWidth() rows := make([]string, 0, len(tier.Services)+1) @@ -162,6 +172,7 @@ func (m Model) renderTier(tier Tier, currentIdx *int, maxNameLen int) string { return components.TierContainerStyle.Render(content) } +// getServiceIndicator returns the selection or status indicator for a service func (m Model) getServiceIndicator(service *ServiceState, isSelected bool) string { defaultIndicator := components.IndicatorEmpty if isSelected { @@ -188,6 +199,7 @@ func (m Model) getServiceIndicator(service *ServiceState, isSelected bool) strin return service.Blink.Render(components.IndicatorActiveStyle) } +// renderServiceRow renders a single service row with all columns func (m Model) renderServiceRow(service *ServiceState, isSelected bool, maxNameLen int) string { rowWidth := m.getRowWidth() maxNameLen = m.clampNameWidth(maxNameLen) @@ -227,6 +239,7 @@ func (m Model) renderServiceRow(service *ServiceState, isSelected bool, maxNameL return components.ServiceRowStyle.Width(rowWidth).Render(row) } +// getStyledAndPaddedStatus returns the styled status string with padding func (m Model) getStyledAndPaddedStatus(service *ServiceState, isSelected bool) string { statusStr := string(service.Status) padding := strings.Repeat(components.IndicatorEmpty, components.ColWidthStatus-len(statusStr)) diff --git a/internal/config/logger/logger.go b/internal/config/logger/logger.go index f6fe71d..207fb3c 100644 --- a/internal/config/logger/logger.go +++ b/internal/config/logger/logger.go @@ -12,6 +12,7 @@ import ( "fuku/internal/config" ) +// Logger configuration constants const ( DebugLevel = "debug" InfoLevel = "info"