summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/cli/root.go59
-rw-r--r--internal/cli/root_test.go128
2 files changed, 187 insertions, 0 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go
new file mode 100644
index 0000000..6029e81
--- /dev/null
+++ b/internal/cli/root.go
@@ -0,0 +1,59 @@
+package cli
+
+import (
+ "fmt"
+
+ timr "codeberg.org/snonux/timr/internal"
+ "codeberg.org/snonux/timr/internal/config"
+ "github.com/spf13/cobra"
+)
+
+var loadedConfig = config.Default()
+
+// Execute runs the root command.
+func Execute() error {
+ return NewRootCmd().Execute()
+}
+
+// NewRootCmd creates the Cobra root command.
+func NewRootCmd() *cobra.Command {
+ var configPath string
+ var showVersion bool
+
+ cmd := &cobra.Command{
+ Use: "timr",
+ Short: "Track time from your terminal",
+ SilenceUsage: true,
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ if showVersion {
+ return nil
+ }
+
+ cfg, err := config.Load(configPath)
+ if err != nil {
+ return fmt.Errorf("load config: %w", err)
+ }
+
+ loadedConfig = cfg
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if showVersion {
+ _, err := fmt.Fprintln(cmd.OutOrStdout(), timr.Version)
+ return err
+ }
+
+ return cmd.Help()
+ },
+ }
+
+ cmd.Flags().BoolVar(&showVersion, "version", false, "Print version and exit")
+ cmd.PersistentFlags().StringVar(&configPath, "config", "", "Path to config file")
+
+ return cmd
+}
+
+// CurrentConfig returns the config loaded in PersistentPreRunE.
+func CurrentConfig() config.Config {
+ return loadedConfig
+}
diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go
new file mode 100644
index 0000000..c900027
--- /dev/null
+++ b/internal/cli/root_test.go
@@ -0,0 +1,128 @@
+package cli
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ timr "codeberg.org/snonux/timr/internal"
+)
+
+func TestRootVersionFlag(t *testing.T) {
+ loadedConfig = CurrentConfig()
+
+ var out bytes.Buffer
+ cmd := NewRootCmd()
+ cmd.SetOut(&out)
+ cmd.SetErr(io.Discard)
+ cmd.SetArgs([]string{"--version"})
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("Execute() error = %v", err)
+ }
+
+ if strings.TrimSpace(out.String()) != timr.Version {
+ t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timr.Version)
+ }
+}
+
+func TestRootLoadsConfigInPersistentPreRun(t *testing.T) {
+ tempHome := t.TempDir()
+ t.Setenv("HOME", tempHome)
+
+ cfgPath := filepath.Join(t.TempDir(), "config.json")
+ content := `{
+ "hostname": "from-config",
+ "worktime_db_dir": "~/custom-db"
+}`
+ if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
+ t.Fatalf("WriteFile() error = %v", err)
+ }
+
+ cmd := NewRootCmd()
+ cmd.SetOut(io.Discard)
+ cmd.SetErr(io.Discard)
+ cmd.SetArgs([]string{"--config", cfgPath})
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("Execute() error = %v", err)
+ }
+
+ cfg := CurrentConfig()
+ if cfg.Hostname != "from-config" {
+ t.Fatalf("Hostname = %q, want %q", cfg.Hostname, "from-config")
+ }
+
+ wantDir := filepath.Join(tempHome, "custom-db")
+ if cfg.WorktimeDBDir != wantDir {
+ t.Fatalf("WorktimeDBDir = %q, want %q", cfg.WorktimeDBDir, wantDir)
+ }
+}
+
+func TestRootInvalidConfigFileReturnsError(t *testing.T) {
+ cfgPath := filepath.Join(t.TempDir(), "config.json")
+ if err := os.WriteFile(cfgPath, []byte(`{"hostname":`), 0o644); err != nil {
+ t.Fatalf("WriteFile() error = %v", err)
+ }
+
+ cmd := NewRootCmd()
+ cmd.SetOut(io.Discard)
+ cmd.SetErr(io.Discard)
+ cmd.SetArgs([]string{"--config", cfgPath})
+
+ err := cmd.Execute()
+ if err == nil {
+ t.Fatal("Execute() error = nil, want config parse error")
+ }
+ if !strings.Contains(err.Error(), "load config") {
+ t.Fatalf("Execute() error = %v, want load config context", err)
+ }
+}
+
+func TestVersionSkipsConfigLoading(t *testing.T) {
+ cfgPath := filepath.Join(t.TempDir(), "config.json")
+ if err := os.WriteFile(cfgPath, []byte(`{"hostname":`), 0o644); err != nil {
+ t.Fatalf("WriteFile() error = %v", err)
+ }
+
+ var out bytes.Buffer
+ cmd := NewRootCmd()
+ cmd.SetOut(&out)
+ cmd.SetErr(io.Discard)
+ cmd.SetArgs([]string{"--config", cfgPath, "--version"})
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("Execute() error = %v", err)
+ }
+
+ if strings.TrimSpace(out.String()) != timr.Version {
+ t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timr.Version)
+ }
+}
+
+func TestRootUsesDefaultConfigWhenNoFileExists(t *testing.T) {
+ tempHome := t.TempDir()
+ t.Setenv("HOME", tempHome)
+ t.Setenv("XDG_CONFIG_HOME", filepath.Join(tempHome, ".config"))
+
+ cmd := NewRootCmd()
+ cmd.SetOut(io.Discard)
+ cmd.SetErr(io.Discard)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("Execute() error = %v", err)
+ }
+
+ cfg := CurrentConfig()
+ if cfg.WeekWorkHours != 40 {
+ t.Fatalf("WeekWorkHours = %v, want 40", cfg.WeekWorkHours)
+ }
+
+ wantDir := filepath.Join(tempHome, "git", "worktime")
+ if cfg.WorktimeDBDir != wantDir {
+ t.Fatalf("WorktimeDBDir = %q, want %q", cfg.WorktimeDBDir, wantDir)
+ }
+}