summaryrefslogtreecommitdiff
path: root/internal/cli/root.go
blob: 8b5ad09a1c3f4ec4c02033250af7fedfa131a03c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package cli

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	timesamurai "codeberg.org/snonux/timesamurai/internal"
	"codeberg.org/snonux/timesamurai/internal/config"
	"codeberg.org/snonux/timesamurai/internal/worktime"
	"github.com/spf13/cobra"
)

type configContextKey struct{}

// 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
	var checkDBIntegrity bool

	cmd := &cobra.Command{
		Use:          "timesamurai",
		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)
			}

			setConfig(cmd, cfg)
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if showVersion {
				_, err := fmt.Fprintln(cmd.OutOrStdout(), timesamurai.Version)
				return err
			}
			if checkDBIntegrity {
				return runDBIntegrityCheck(cmd)
			}

			return cmd.Help()
		},
	}

	cmd.Flags().BoolVar(&showVersion, "version", false, "Print version and exit")
	cmd.Flags().BoolVar(&checkDBIntegrity, "check-db-integrity", false, "Validate worktime database integrity and exit")
	cmd.PersistentFlags().StringVar(&configPath, "config", "", "Path to config file")
	cmd.AddCommand(newTimerCmd())
	cmd.AddCommand(newWorkCmd())
	cmd.AddCommand(newTUICmd())

	return cmd
}

func setConfig(cmd *cobra.Command, cfg config.Config) {
	baseContext := cmd.Context()
	if baseContext == nil {
		baseContext = context.Background()
	}
	cmd.SetContext(context.WithValue(baseContext, configContextKey{}, cfg))
}

func currentConfig(cmd *cobra.Command) config.Config {
	if cmd == nil {
		return config.Default()
	}

	commandContext := cmd.Context()
	if commandContext == nil {
		return config.Default()
	}

	cfg, ok := commandContext.Value(configContextKey{}).(config.Config)
	if !ok {
		return config.Default()
	}
	return cfg
}

func runDBIntegrityCheck(cmd *cobra.Command) error {
	cfg := currentConfig(cmd)
	entries, err := worktime.LoadAll(cfg.WorktimeDBDir)
	if err != nil {
		return err
	}

	issues := worktime.CheckEntriesIntegrity(entries, worktime.DefaultMaxSessionSpan)
	openSessions := worktime.OpenSessions(entries)

	lines := make([]string, 0, len(issues)+len(openSessions)+2)
	if len(issues) == 0 {
		lines = append(lines, "Database integrity check passed.")
	} else {
		lines = append(lines, fmt.Sprintf("Database integrity check found %d issue(s):", len(issues)))
		for idx, issue := range issues {
			lines = append(lines, fmt.Sprintf("%d. %s", idx+1, issue.String()))
		}
	}

	if len(openSessions) > 0 {
		lines = append(lines, fmt.Sprintf("Warning: currently logged in (%d open session(s)):", len(openSessions)))
		for _, session := range openSessions {
			lines = append(lines, fmt.Sprintf(
				"- category=%s source=%s since=%s",
				session.Category,
				session.Login.Source,
				time.Unix(session.Login.Epoch, 0).Format("2006-01-02 15:04:05"),
			))
		}
	}

	_, printErr := fmt.Fprintln(cmd.OutOrStdout(), strings.Join(lines, "\n"))
	if printErr != nil {
		return printErr
	}
	if len(issues) > 0 {
		return errors.New("database integrity check failed")
	}
	return nil
}