diff options
Diffstat (limited to 'internal')
36 files changed, 1789 insertions, 187 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go index faba592..8b5ad09 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -2,10 +2,14 @@ package cli import ( "context" + "errors" "fmt" + "strings" + "time" - timr "codeberg.org/snonux/timr/internal" - "codeberg.org/snonux/timr/internal/config" + timesamurai "codeberg.org/snonux/timesamurai/internal" + "codeberg.org/snonux/timesamurai/internal/config" + "codeberg.org/snonux/timesamurai/internal/worktime" "github.com/spf13/cobra" ) @@ -20,9 +24,10 @@ func Execute() error { func NewRootCmd() *cobra.Command { var configPath string var showVersion bool + var checkDBIntegrity bool cmd := &cobra.Command{ - Use: "timr", + Use: "timesamurai", Short: "Track time from your terminal", SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { @@ -40,15 +45,19 @@ func NewRootCmd() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { if showVersion { - _, err := fmt.Fprintln(cmd.OutOrStdout(), timr.Version) + _, 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()) @@ -81,3 +90,45 @@ func currentConfig(cmd *cobra.Command) config.Config { } 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 +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 2b12514..e1f74b1 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -7,8 +7,10 @@ import ( "path/filepath" "strings" "testing" + "time" - timr "codeberg.org/snonux/timr/internal" + timesamurai "codeberg.org/snonux/timesamurai/internal" + "codeberg.org/snonux/timesamurai/internal/worktime" ) func TestRootVersionFlag(t *testing.T) { @@ -22,8 +24,8 @@ func TestRootVersionFlag(t *testing.T) { 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) + if strings.TrimSpace(out.String()) != timesamurai.Version { + t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timesamurai.Version) } } @@ -96,8 +98,8 @@ func TestVersionSkipsConfigLoading(t *testing.T) { 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) + if strings.TrimSpace(out.String()) != timesamurai.Version { + t.Fatalf("output = %q, want %q", strings.TrimSpace(out.String()), timesamurai.Version) } } @@ -124,3 +126,113 @@ func TestRootUsesDefaultConfigWhenNoFileExists(t *testing.T) { t.Fatalf("WorktimeDBDir = %q, want %q", cfg.WorktimeDBDir, wantDir) } } + +func TestRootCheckDBIntegrityPasses(t *testing.T) { + dbDir := t.TempDir() + host := "host-a" + + if _, err := worktime.Login(dbDir, host, "work", time.Unix(100, 0), ""); err != nil { + t.Fatalf("Login() error = %v", err) + } + if _, err := worktime.Logout(dbDir, host, "work", time.Unix(200, 0), ""); err != nil { + t.Fatalf("Logout() error = %v", err) + } + + cfgPath := writeRootConfig(t, dbDir, host) + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"--config", cfgPath, "--check-db-integrity"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v (output: %q)", err, out.String()) + } + if !strings.Contains(out.String(), "Database integrity check passed.") { + t.Fatalf("unexpected output: %q", out.String()) + } +} + +func TestRootCheckDBIntegrityFailsOnIssues(t *testing.T) { + dbDir := t.TempDir() + host := "host-a" + + db := worktime.Database{ + Entries: map[string][]worktime.Entry{ + host: { + { + Action: "logout", + What: "work", + Epoch: 100, + Source: host, + Human: time.Unix(100, 0).Format("Mon 02.01.2006 15:04:05"), + }, + }, + }, + } + if err := worktime.SaveHost(dbDir, host, db); err != nil { + t.Fatalf("SaveHost() error = %v", err) + } + + cfgPath := writeRootConfig(t, dbDir, host) + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"--config", cfgPath, "--check-db-integrity"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("Execute() error = nil, want integrity failure") + } + if !strings.Contains(err.Error(), "database integrity check failed") { + t.Fatalf("Execute() error = %v, want integrity failure", err) + } + if !strings.Contains(out.String(), "Database integrity check found") { + t.Fatalf("unexpected output: %q", out.String()) + } +} + +func TestRootCheckDBIntegrityWarnsForOpenSession(t *testing.T) { + dbDir := t.TempDir() + host := "host-a" + + if _, err := worktime.Login(dbDir, host, "work", time.Unix(100, 0), ""); err != nil { + t.Fatalf("Login() error = %v", err) + } + + cfgPath := writeRootConfig(t, dbDir, host) + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"--config", cfgPath, "--check-db-integrity"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v (output: %q)", err, out.String()) + } + if !strings.Contains(out.String(), "Database integrity check passed.") { + t.Fatalf("unexpected output: %q", out.String()) + } + if !strings.Contains(out.String(), "Warning: currently logged in") { + t.Fatalf("expected open-session warning in output: %q", out.String()) + } +} + +func writeRootConfig(t *testing.T, dbDir, host string) string { + t.Helper() + + content := `{ + "worktime_db_dir": "` + dbDir + `", + "hostname": "` + host + `" +} +` + path := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write config file: %v", err) + } + return path +} diff --git a/internal/cli/timer.go b/internal/cli/timer.go index 37e50d0..e9889c4 100644 --- a/internal/cli/timer.go +++ b/internal/cli/timer.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "codeberg.org/snonux/timr/internal/ascii" - timrTimer "codeberg.org/snonux/timr/internal/timer" - tuiapp "codeberg.org/snonux/timr/internal/tui" - "codeberg.org/snonux/timr/internal/worktime" + "codeberg.org/snonux/timesamurai/internal/ascii" + timesamuraiTimer "codeberg.org/snonux/timesamurai/internal/timer" + tuiapp "codeberg.org/snonux/timesamurai/internal/tui" + "codeberg.org/snonux/timesamurai/internal/worktime" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) @@ -44,7 +44,7 @@ func newTimerStartCmd() *cobra.Command { return err } - output, err := timrTimer.StartTimer(hasElapsed) + output, err := timesamuraiTimer.StartTimer(hasElapsed) if err != nil { return err } @@ -61,7 +61,7 @@ func newTimerStopCmd() *cobra.Command { Use: "stop", Short: "Stop the timer", RunE: func(cmd *cobra.Command, args []string) error { - output, err := timrTimer.StopTimer() + output, err := timesamuraiTimer.StopTimer() if err != nil { return fmt.Errorf("stop timer: %w", err) } @@ -85,7 +85,7 @@ func newTimerContinueCmd() *cobra.Command { output := "Timer is at 0, cannot continue." if hasElapsed { - output, err = timrTimer.StartTimer(true) + output, err = timesamuraiTimer.StartTimer(true) if err != nil { return err } @@ -101,7 +101,7 @@ func newTimerResetCmd() *cobra.Command { Use: "reset", Short: "Reset the timer", RunE: func(cmd *cobra.Command, args []string) error { - output, err := timrTimer.ResetTimer() + output, err := timesamuraiTimer.ResetTimer() if err != nil { return fmt.Errorf("reset timer: %w", err) } @@ -129,11 +129,11 @@ func newTimerStatusCmd() *cobra.Command { switch { case raw: - output, err = timrTimer.GetRawStatus() + output, err = timesamuraiTimer.GetRawStatus() case rawMinutes: - output, err = timrTimer.GetRawMinutesStatus() + output, err = timesamuraiTimer.GetRawMinutesStatus() default: - output, err = timrTimer.GetStatus() + output, err = timesamuraiTimer.GetStatus() } if err != nil { return err @@ -153,7 +153,7 @@ func newTimerPromptCmd() *cobra.Command { Use: "prompt", Short: "Show prompt-friendly timer status", RunE: func(cmd *cobra.Command, args []string) error { - output, err := timrTimer.GetPromptStatus() + output, err := timesamuraiTimer.GetPromptStatus() if err != nil { return fmt.Errorf("get prompt timer status: %w", err) } @@ -169,7 +169,7 @@ func newTimerTrackCmd() *cobra.Command { Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { description := strings.Join(args, " ") - output, err := timrTimer.TrackTime(description) + output, err := timesamuraiTimer.TrackTime(description) if err != nil { return fmt.Errorf("track timer entry %q: %w", description, err) } @@ -242,7 +242,7 @@ func syncWorktimeWithTimer(cmd *cobra.Command, start bool) error { } func timerHasElapsed() (bool, error) { - rawStatus, err := timrTimer.GetRawStatus() + rawStatus, err := timesamuraiTimer.GetRawStatus() if err != nil { return false, fmt.Errorf("get raw timer status: %w", err) } diff --git a/internal/cli/timer_test.go b/internal/cli/timer_test.go index 572d5a0..be4391e 100644 --- a/internal/cli/timer_test.go +++ b/internal/cli/timer_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "codeberg.org/snonux/timr/internal/worktime" + "codeberg.org/snonux/timesamurai/internal/worktime" ) func TestTimerStartAndStopCommands(t *testing.T) { diff --git a/internal/cli/tui.go b/internal/cli/tui.go index ff4f2aa..474ffc2 100644 --- a/internal/cli/tui.go +++ b/internal/cli/tui.go @@ -1,17 +1,19 @@ package cli import ( - tuiapp "codeberg.org/snonux/timr/internal/tui" + tuiapp "codeberg.org/snonux/timesamurai/internal/tui" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" ) func newTUICmd() *cobra.Command { - return &cobra.Command{ + var disco bool + + cmd := &cobra.Command{ Use: "tui", Short: "Launch full-screen TUI", RunE: func(cmd *cobra.Command, args []string) error { - model, err := tuiapp.NewModelWithConfig(currentConfig(cmd)) + model, err := tuiapp.NewModelWithConfigAndDisco(currentConfig(cmd), disco) if err != nil { return err } @@ -19,4 +21,7 @@ func newTUICmd() *cobra.Command { return program.Start() }, } + + cmd.Flags().BoolVar(&disco, "disco", false, "Enable disco mode (random theme changes)") + return cmd } diff --git a/internal/cli/tui_test.go b/internal/cli/tui_test.go index 9d8f481..8ec4afd 100644 --- a/internal/cli/tui_test.go +++ b/internal/cli/tui_test.go @@ -25,4 +25,9 @@ func TestNewTUICmdMetadata(t *testing.T) { if cmd.Short == "" { t.Fatal("Short description should not be empty") } + + flag := cmd.Flags().Lookup("disco") + if flag == nil { + t.Fatal("expected --disco flag to be defined") + } } diff --git a/internal/cli/work.go b/internal/cli/work.go index 22f0406..d6b989e 100644 --- a/internal/cli/work.go +++ b/internal/cli/work.go @@ -10,10 +10,10 @@ import ( "strings" "time" - "codeberg.org/snonux/timr/internal/duration" - "codeberg.org/snonux/timr/internal/timefmt" - timrTimer "codeberg.org/snonux/timr/internal/timer" - "codeberg.org/snonux/timr/internal/worktime" + "codeberg.org/snonux/timesamurai/internal/duration" + "codeberg.org/snonux/timesamurai/internal/timefmt" + timesamuraiTimer "codeberg.org/snonux/timesamurai/internal/timer" + "codeberg.org/snonux/timesamurai/internal/worktime" "github.com/spf13/cobra" ) @@ -32,6 +32,7 @@ func newWorkCmd() *cobra.Command { cmd.AddCommand(newWorkLogoutCmd()) cmd.AddCommand(newWorkAddCmd()) cmd.AddCommand(newWorkSubCmd()) + cmd.AddCommand(newWorkDayOffCmd()) cmd.AddCommand(newWorkUseBufferCmd()) cmd.AddCommand(newWorkReportCmd()) cmd.AddCommand(newWorkStatusCmd()) @@ -211,6 +212,39 @@ func newWorkSubCmd() *cobra.Command { return cmd } +func newWorkDayOffCmd() *cobra.Command { + var at string + var descr string + + cmd := &cobra.Command{ + Use: "day-off", + Short: "Add an 8-hour day-off entry", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, err := resolveWorkContext(cmd) + if err != nil { + return err + } + + day, err := parseAtOrNow(at) + if err != nil { + return err + } + + entry, err := worktime.AddDayOff(ctx.dbDir, ctx.host, day, descr) + if err != nil { + return err + } + + dayLabel := time.Unix(entry.Epoch, 0).Format("2006-01-02") + return printOutput(cmd, fmt.Sprintf("Added day off: 8h on %s", dayLabel)) + }, + } + + cmd.Flags().StringVar(&at, "at", "", "Date/time override (unix, ISO, today, yesterday)") + cmd.Flags().StringVarP(&descr, "descr", "d", "", "Description") + return cmd +} + func newWorkUseBufferCmd() *cobra.Command { var at string var descr string @@ -407,9 +441,9 @@ func startTimerFromWorkCommand() (string, error) { if err != nil { return "", err } - return timrTimer.StartTimer(hasElapsed) + return timesamuraiTimer.StartTimer(hasElapsed) } func stopTimerFromWorkCommand() (string, error) { - return timrTimer.StopTimer() + return timesamuraiTimer.StopTimer() } diff --git a/internal/cli/work_test.go b/internal/cli/work_test.go index 9621bfb..04a1643 100644 --- a/internal/cli/work_test.go +++ b/internal/cli/work_test.go @@ -7,8 +7,10 @@ import ( "strconv" "strings" "testing" + "time" - timrTimer "codeberg.org/snonux/timr/internal/timer" + timesamuraiTimer "codeberg.org/snonux/timesamurai/internal/timer" + "codeberg.org/snonux/timesamurai/internal/worktime" ) func TestWorkLoginStatusLogoutFlow(t *testing.T) { @@ -87,7 +89,7 @@ func TestWorkLoginLogoutWithTimerFlags(t *testing.T) { if err != nil { t.Fatalf("work login --start-timer error = %v (output: %q)", err, out) } - state, err := timrTimer.LoadState() + state, err := timesamuraiTimer.LoadState() if err != nil { t.Fatalf("LoadState() error = %v", err) } @@ -99,7 +101,7 @@ func TestWorkLoginLogoutWithTimerFlags(t *testing.T) { if err != nil { t.Fatalf("work logout --stop-timer error = %v (output: %q)", err, out) } - state, err = timrTimer.LoadState() + state, err = timesamuraiTimer.LoadState() if err != nil { t.Fatalf("LoadState() error = %v", err) } @@ -108,6 +110,42 @@ func TestWorkLoginLogoutWithTimerFlags(t *testing.T) { } } +func TestWorkDayOffCommand(t *testing.T) { + dbDir := t.TempDir() + host := "host-day-off" + cfgPath := writeWorkConfig(t, dbDir, host) + + out, err := runRootCommand("--config", cfgPath, "work", "day-off", "--at", "2026-02-17", "--descr", "vacation") + if err != nil { + t.Fatalf("work day-off error = %v (output: %q)", err, out) + } + if !strings.Contains(out, "Added day off: 8h on 2026-02-17") { + t.Fatalf("unexpected day-off output: %q", out) + } + + db, err := worktime.LoadHost(dbDir, host) + if err != nil { + t.Fatalf("LoadHost() error = %v", err) + } + + entries := db.Entries[host] + if len(entries) != 1 { + t.Fatalf("entries len = %d, want 1", len(entries)) + } + + entry := entries[0] + wantEpoch := time.Date(2026, 2, 17, 0, 0, 0, 0, time.Local).Unix() + if entry.Action != "add" || entry.What != "off" { + t.Fatalf("unexpected day-off entry: %+v", entry) + } + if entry.Value != 8*3600 { + t.Fatalf("day-off value = %d, want 28800", entry.Value) + } + if entry.Epoch != wantEpoch { + t.Fatalf("day-off epoch = %d, want %d", entry.Epoch, wantEpoch) + } +} + func writeWorkConfig(t *testing.T, dbDir, host string) string { return writeWorkConfigWithAuto(t, dbDir, host, false) } diff --git a/internal/config/blackbox_test.go b/internal/config/blackbox_test.go index 0d97660..2303a74 100644 --- a/internal/config/blackbox_test.go +++ b/internal/config/blackbox_test.go @@ -3,7 +3,7 @@ package config_test import ( "testing" - "codeberg.org/snonux/timr/internal/config" + "codeberg.org/snonux/timesamurai/internal/config" ) func TestDefaultPublicAPI(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 812cca2..5dae114 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,7 @@ import ( const ( defaultWeekWorkHours = 40.0 defaultWorktimeDBDir = "~/git/worktime" - configDirName = "timr" + configDirName = "timesamurai" configFileName = "config.json" ) diff --git a/internal/config/doc.go b/internal/config/doc.go index 5d58f69..4e34d6b 100644 --- a/internal/config/doc.go +++ b/internal/config/doc.go @@ -1,2 +1,2 @@ -// Package config loads, saves, and defaults timr configuration values. +// Package config loads, saves, and defaults timesamurai configuration values. package config diff --git a/internal/duration/blackbox_test.go b/internal/duration/blackbox_test.go index 87fa9dc..3553cdc 100644 --- a/internal/duration/blackbox_test.go +++ b/internal/duration/blackbox_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "codeberg.org/snonux/timr/internal/duration" + "codeberg.org/snonux/timesamurai/internal/duration" ) func TestParsePublicAPI(t *testing.T) { diff --git a/internal/duration/doc.go b/internal/duration/doc.go index 9a99242..ad267c5 100644 --- a/internal/duration/doc.go +++ b/internal/duration/doc.go @@ -1,2 +1,2 @@ -// Package duration parses CLI duration values used by timr commands. +// Package duration parses CLI duration values used by timesamurai commands. package duration diff --git a/internal/timefmt/doc.go b/internal/timefmt/doc.go index 63afde5..747c1cc 100644 --- a/internal/timefmt/doc.go +++ b/internal/timefmt/doc.go @@ -1,2 +1,2 @@ -// Package timefmt parses timestamp inputs accepted by timr commands. +// Package timefmt parses timestamp inputs accepted by timesamurai commands. package timefmt diff --git a/internal/timer/timer.go b/internal/timer/timer.go index 2a5dbde..dec9421 100644 --- a/internal/timer/timer.go +++ b/internal/timer/timer.go @@ -9,7 +9,7 @@ import ( ) const ( - stateFile = ".timr_state" + stateFile = ".timesamurai_state" ) // State stores persisted timer progress. @@ -29,7 +29,7 @@ func resolveStateFilePath(path string) (string, error) { return "", err } - return filepath.Join(configDir, "timr", stateFile), nil + return filepath.Join(configDir, "timesamurai", stateFile), nil } // GetStateFile returns the default state file path. diff --git a/internal/tui/doc.go b/internal/tui/doc.go index 624b708..0621595 100644 --- a/internal/tui/doc.go +++ b/internal/tui/doc.go @@ -1,2 +1,2 @@ -// Package tui provides Bubble Tea models used by timr terminal interfaces. +// Package tui provides Bubble Tea models used by timesamurai terminal interfaces. package tui diff --git a/internal/tui/entries.go b/internal/tui/entries.go index e189846..2d35708 100644 --- a/internal/tui/entries.go +++ b/internal/tui/entries.go @@ -4,13 +4,15 @@ import ( "errors" "fmt" "slices" + "sort" "strconv" "strings" "time" "unicode/utf8" - "codeberg.org/snonux/timr/internal/duration" - "codeberg.org/snonux/timr/internal/worktime" + "codeberg.org/snonux/timesamurai/internal/duration" + "codeberg.org/snonux/timesamurai/internal/timefmt" + "codeberg.org/snonux/timesamurai/internal/worktime" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -20,26 +22,35 @@ type entryEditField int const ( entryEditFieldDescription entryEditField = iota entryEditFieldValue + entryEditFieldDate + entryEditFieldTime + entryEditFieldAction + entryEditFieldCategory ) -var categoryColors = map[string]string{ - "work": "#8BD3DD", - "lunch": "#F6BD60", - "off": "#CDB4DB", - "bank": "#A8DADC", - "sick": "#FFB4A2", - "selfdevelopment": "#B8E1A9", -} +type entriesColumn int + +const ( + entriesColumnDate entriesColumn = iota + entriesColumnTime + entriesColumnAction + entriesColumnCategory + entriesColumnValue + entriesColumnSource + entriesColumnDescription + entriesColumnCount +) // EntriesModel is a chronological worktime entry browser. type EntriesModel struct { allEntries []worktime.Entry visible []worktime.Entry - cursor int - offset int - width int - height int + cursor int + offset int + selectedColumn entriesColumn + width int + height int pendingG bool pendingD bool @@ -49,6 +60,8 @@ type EntriesModel struct { filterMode bool filterQuery string + dayOffMode bool + dayOffDate time.Time editMode bool confirmDelete bool @@ -57,22 +70,31 @@ type EntriesModel struct { editField entryEditField dbDir string + dbHost string statusMessage string statusError bool + mutationCount int + dirty bool + changedHosts map[string]struct{} } // NewEntriesModel creates an entry browser model. func NewEntriesModel(entries []worktime.Entry) EntriesModel { model := EntriesModel{ - height: 16, + height: 16, + selectedColumn: entriesColumnDescription, } model.SetEntries(entries) return model } -// SetPersistence enables edit/delete persistence against dbDir. -func (m *EntriesModel) SetPersistence(dbDir string) { +// SetPersistence enables edit/delete/create persistence against dbDir and host. +func (m *EntriesModel) SetPersistence(dbDir, host string) { m.dbDir = strings.TrimSpace(dbDir) + m.dbHost = strings.TrimSpace(host) + if m.changedHosts == nil { + m.changedHosts = map[string]struct{}{} + } } // SetSize updates viewport size used for scrolling. @@ -96,6 +118,8 @@ func (m *EntriesModel) SetEntries(entries []worktime.Entry) { } return 1 }) + m.dirty = false + m.changedHosts = map[string]struct{}{} m.applyFilters() } @@ -110,6 +134,10 @@ func (m *EntriesModel) Update(msg tea.Msg) (EntriesModel, tea.Cmd) { return *m, nil } + if m.updateDayOffMode(keyMsg) { + return *m, nil + } + if m.updateEditMode(keyMsg) { return *m, nil } @@ -170,6 +198,45 @@ func (m *EntriesModel) updateEditMode(keyMsg tea.KeyMsg) bool { return true } +func (m *EntriesModel) updateDayOffMode(keyMsg tea.KeyMsg) bool { + if !m.dayOffMode { + return false + } + + switch keyMsg.String() { + case "enter": + if addErr := m.addDayOff(m.dayOffDate); addErr != nil { + m.setStatusError("Add day off failed: " + addErr.Error()) + } else { + m.setStatusInfo("Day off added.") + } + + m.dayOffMode = false + case "esc": + m.dayOffMode = false + case "left", "h": + m.dayOffDate = m.dayOffDate.AddDate(0, 0, -1) + case "right", "l": + m.dayOffDate = m.dayOffDate.AddDate(0, 0, 1) + case "up", "k": + m.dayOffDate = m.dayOffDate.AddDate(0, 0, -7) + case "down", "j": + m.dayOffDate = m.dayOffDate.AddDate(0, 0, 7) + case "pgup": + m.dayOffDate = m.dayOffDate.AddDate(0, -1, 0) + case "pgdown": + m.dayOffDate = m.dayOffDate.AddDate(0, 1, 0) + case "home": + m.dayOffDate = time.Date(m.dayOffDate.Year(), m.dayOffDate.Month(), 1, 0, |
