diff --git a/cmd/life/main.go b/cmd/life/main.go new file mode 100644 index 0000000..f23df8f --- /dev/null +++ b/cmd/life/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "code.chimeric.al/dwrz/life/pkg/life" + "code.chimeric.al/dwrz/life/pkg/terminal" +) + +var ( + height = flag.Int("h", 0, "height") + width = flag.Int("w", 0, "width") + tick = flag.Int("t", 100, "ms between turns") +) + +func main() { + var log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + })) + + if err := run(log); err != nil { + log.Error("failed", slog.Any("error", err)) + os.Exit(1) + } +} + +func run(log *slog.Logger) error { + // Parse flags. + flag.Parse() + if *height < 0 { + return fmt.Errorf("invalid height: %d", *height) + } + if *width < 0 { + return fmt.Errorf("invalid width: %d", *width) + } + if *tick <= 0 { + return fmt.Errorf("invalid tick: %d", *tick) + } + + // Setup the main context. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Setup workspace and log file. + cdir, err := os.UserCacheDir() + if err != nil { + return fmt.Errorf( + "failed to determine user cache directory: %w", err, + ) + } + wdir := filepath.Join(cdir, "life") + + if err := os.MkdirAll(wdir, os.ModeDir|0700); err != nil { + return fmt.Errorf("failed to create tmp dir: %w", err) + } + + f, err := os.Create(wdir + "/log") + if err != nil { + return fmt.Errorf("failed to create log file: %w", err) + } + defer f.Close() + + var gameLog = slog.New(slog.NewTextHandler(f, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + })) + + // Retrieve terminal info. + t, err := terminal.New(os.Stdin.Fd()) + if err != nil { + return fmt.Errorf("failed to get terminal attributes: %w", err) + } + size, err := t.Size() + if err != nil { + return fmt.Errorf("failed to get terminal size: %w", err) + } + if *height == 0 { + rows := int(size.Rows) + height = &rows + } + if *width == 0 { + cols := int(size.Columns) + width = &cols + } + + // TODO: refactor; handle sigwinch. + go func() { + var signals = make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) + + // Block until we receive a signal. + s := <-signals + log.Debug("received signal", slog.Any("signal", s)) + + cancel() + }() + + // Setup the game. + var game = life.New(life.Parameters{ + Height: *height, + In: os.Stdin, + Log: gameLog, + Out: os.Stdout, + Terminal: t, + Tick: time.Duration(*tick) * time.Millisecond, + Width: *width, + }) + + // Run the game. + return game.Run(ctx) +}