refactor: refactor project structure to use golang best practices

This commit is contained in:
Patryk Hegenberg 2026-01-11 11:33:26 +01:00
parent 5b16cef525
commit 4ed6a61b1d
10 changed files with 617 additions and 702 deletions

105
cmd.go
View file

@ -4,17 +4,16 @@ import (
"fmt"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
"time"
"workctl/internal/config"
"workctl/internal/store"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
)
var withoutTimew bool
func (a *App) setupCommands() *cobra.Command {
rootCmd := &cobra.Command{
Use: "workctl",
@ -71,14 +70,14 @@ func (a *App) configCommand() *cobra.Command {
}
if sshPw != "" {
if err := setSecret(keySSHPassword, sshPw); err != nil {
if err := config.SetSecret(config.KeySSHPassword(), sshPw); err != nil {
return fmt.Errorf("failed to save SSH password: %w", err)
}
fmt.Println("✓ SSH password saved to keyring.")
}
if rdpPw != "" {
if err := setSecret(keyRDPPassword, rdpPw); err != nil {
if err := config.SetSecret(config.KeyRDPPassword(), rdpPw); err != nil {
return fmt.Errorf("failed to save RDP password: %w", err)
}
fmt.Println("✓ RDP password saved to keyring.")
@ -100,64 +99,15 @@ func (a *App) startCommand() *cobra.Command {
Use: "start",
Short: "Start work: Track time, WOL, setup tunnels, optionally connect or run in background",
Long: `Starts time tracking, attempts WOL, sets up SSH tunnels.
Default behavior: Immediately starts an interactive SSH session to the workstation via the tunnel. The command blocks until this session ends.
Use --background (-b) to keep tunnels running in the background without auto-connecting. Press Ctrl+C to stop background tunnels.`,
Default behavior: Immediately starts an interactive SSH session to the workstation via the tunnel.
Use --background (-b) to keep tunnels running in the background without auto-connecting.`,
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Starting workday procedures...")
sshCon, err := a.connect(withoutTimew)
if err != nil {
fmt.Printf("ERROR: Failed to start connections: %v\n", err)
return fmt.Errorf("connection setup failed: %w", err)
}
defer func() {
slog.Info("Closing SSH connection to jump host (defer)...")
if err := sshCon.Close(); err != nil {
slog.Warn(fmt.Sprintf("Error closing SSH connection in defer: %v", err))
} else {
slog.Info("SSH connection closed via defer.")
}
}()
if a.flags.StartInBackground {
fmt.Println("\nINFO: Tunnels are active in background.")
fmt.Println(" Connect manually via SSH: ssh -p 2048 <user>@127.0.0.1")
fmt.Println(" Connect manually via RDP: xfreerdp /v:127.0.0.1:6000 ...")
fmt.Println("INFO: Press Ctrl+C to stop tunnels.")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
fmt.Println("\nINFO: Received interrupt signal. Shutting down background process...")
slog.Info("Received signal, cleanup via defer sshCon.Close() will run.")
if err := a.StopTracking(withoutTimew); err != nil {
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
slog.Info("Time tracking stopped.")
}
fmt.Println("INFO: Background shutdown complete.")
} else {
fmt.Println("Automatically connecting to workstation via SSH tunnel...")
a.connectToWorkstation()
fmt.Println("Workstation SSH session finished.")
slog.Info("Foreground session ended, cleanup via defer sshCon.Close() will run.")
if err := a.StopTracking(withoutTimew); err != nil {
slog.Warn(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
slog.Info("Time tracking stopped.")
}
}
return nil
return a.connect(cmd.Context())
},
}
cmd.Flags().BoolVarP(&a.flags.StartInBackground, "background", "b", false, "Run tunnels in the background instead of connecting immediately")
cmd.Flags().BoolVarP(&withoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
cmd.Flags().BoolVarP(&a.flags.WithoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
return cmd
}
@ -169,7 +119,7 @@ func (a *App) stopCommand() *cobra.Command {
Long: "Stops the current time tracking entry and attempts to kill active SSH tunnels.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Stopping workday procedures...")
if err := a.StopTracking(withoutTimew); err != nil {
if err := a.StopTracking(cmd.Context()); err != nil {
slog.Error(fmt.Sprintf("Failed to stop time tracking: %v", err))
} else {
fmt.Println("Time tracking stopped.")
@ -184,7 +134,7 @@ func (a *App) stopCommand() *cobra.Command {
fmt.Println("Workday stop procedures finished.")
},
}
cmd.Flags().BoolVarP(&withoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
cmd.Flags().BoolVarP(&a.flags.WithoutTimew, "timew", "t", false, "Set this flag if you dont want to use Timewarrior Timestorage as well")
return cmd
}
@ -201,7 +151,7 @@ it will mark the *current day* with that tag instead of starting an interval tim
This also stops any currently running timer.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
tag := TagWork
tag := store.TagWork
if len(args) > 0 {
tag = args[0]
}
@ -216,14 +166,14 @@ This also stops any currently running timer.`,
case "uni", "urlaub", "feiertag", "krank", "free":
today := time.Now()
fmt.Printf("Logging '%s' for today (%s)...\n", tagLower, today.Format("2006-01-02"))
if err := a.timeStore.LogFullDay(tagLower, today); err != nil {
if err := a.store.LogFullDay(cmd.Context(), tagLower, today); err != nil {
return fmt.Errorf("could not log '%s' for today: %w", tagLower, err)
}
return nil
default:
fmt.Printf("Attempting to start tracking interval '%s'...\n", tag)
if err := a.StartTracking(tag, withoutTimew); err != nil {
if err := a.StartTracking(cmd.Context(), tag); err != nil {
slog.Error(fmt.Sprintf("Failed to start tracking '%s': %v", tag, err))
return fmt.Errorf("could not start tracking '%s': %w", tag, err)
}
@ -237,7 +187,7 @@ This also stops any currently running timer.`,
Short: "Start tracking 'break'",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Starting break...")
if err := a.StartTracking(TagBreak, withoutTimew); err != nil {
if err := a.StartTracking(cmd.Context(), store.TagBreak); err != nil {
slog.Error(fmt.Sprintf("Failed to start break tracking: %v", err))
return fmt.Errorf("could not start break: %w", err)
}
@ -271,23 +221,23 @@ Export: Use the --export flag or the 'export' subcommand.`,
slog.Info(fmt.Sprintf("No export name specified, using default: %s", filename))
}
fmt.Printf("Exporting yearly timetable to '%s'...\n", filename)
if err := a.timeStore.ExportSummary(filename); err != nil {
if err := a.store.ExportSummary(cmd.Context(), filename); err != nil {
slog.Error(fmt.Sprintf("Failed to export summary to '%s': %v", filename, err))
fmt.Printf("Error: Could not export to '%s'.\n", filename)
}
} else if a.flags.ShowWeek {
fmt.Println("Showing weekly summary...")
if err := a.timeStore.ShowSummary("week"); err != nil {
if err := a.store.ShowSummary(cmd.Context(), "week"); err != nil {
slog.Error(fmt.Sprintf("Failed to show week summary: %v", err))
}
} else if a.flags.ShowMonth {
fmt.Println("Showing monthly summary...")
if err := a.timeStore.ShowSummary("month"); err != nil {
if err := a.store.ShowSummary(cmd.Context(), "month"); err != nil {
slog.Error(fmt.Sprintf("Failed to show month summary: %v", err))
}
} else {
fmt.Printf("Showing summary for period: %s...\n", period)
if err := a.timeStore.ShowSummary(period); err != nil {
if err := a.store.ShowSummary(cmd.Context(), period); err != nil {
slog.Error(fmt.Sprintf("Failed to show summary for '%s': %v", period, err))
}
}
@ -311,7 +261,7 @@ Export: Use the --export flag or the 'export' subcommand.`,
filename = args[0]
}
fmt.Printf("Exporting yearly timetable to '%s'...\n", filename)
if err := a.timeStore.ExportSummary(filename); err != nil {
if err := a.store.ExportSummary(cmd.Context(), filename); err != nil {
slog.Error(fmt.Sprintf("Failed to export summary to '%s': %v", filename, err))
fmt.Printf("Error: Could not export to '%s'.\n", filename)
}
@ -343,7 +293,6 @@ func (a *App) connectCommands() *cobra.Command {
cmd.AddCommand(&cobra.Command{
Use: "jump",
Short: "Connect to Jump Host (with tunnel to workstation)",
Long: "Establishes an SSH connection to the Jump Host and forwards local port 2048 to the workstation's SSH port (22). This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.connectToJump()
},
@ -352,7 +301,6 @@ func (a *App) connectCommands() *cobra.Command {
cmd.AddCommand(&cobra.Command{
Use: "workstation",
Short: "Connect to Workstation via SSH tunnel",
Long: "Establishes an SSH connection to the Workstation via the local tunnel on port 2048. Also sets up RDP tunnel on local port 6000. Requires the 'jump' tunnel to be active. This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.connectToWorkstation()
},
@ -361,7 +309,6 @@ func (a *App) connectCommands() *cobra.Command {
cmd.AddCommand(&cobra.Command{
Use: "rdp",
Short: "Start RDP session via tunnel",
Long: "Starts an RDP client (xfreerdp) connecting to localhost:6000. Requires an active tunnel forwarding this port to the workstation's RDP port (3389). This command blocks.",
Run: func(cmd *cobra.Command, args []string) {
a.startRDPConnection()
},
@ -374,11 +321,7 @@ func (a *App) importTimewarriorCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "import-timew [filepath]",
Short: "Import time entries from 'timewarrior summary' output file",
Long: `Parses the output of 'timewarrior summary :year' (or similar) stored in a text file
and inserts the individual time intervals into the workctl SQLite database.
It expects the standard timewarrior summary format.
Example: workctl import-timew /path/to/timew-summary.txt`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
filepath := args[0]
fmt.Printf("Attempting to import timewarrior data from: %s\n", filepath)
@ -405,7 +348,7 @@ func (a *App) runImport(filepath string) (int, error) {
content := string(contentBytes)
lines := strings.Split(content, "\n")
tx, err := a.timeStore.db.Begin()
tx, err := a.store.DB().Begin()
if err != nil {
return 0, fmt.Errorf("could not begin database transaction: %w", err)
}
@ -500,9 +443,9 @@ func (a *App) runImport(filepath string) (int, error) {
dbTag := strings.ToLower(tag)
switch dbTag {
case "work":
dbTag = TagWork
dbTag = store.TagWork
case "break":
dbTag = TagBreak
dbTag = store.TagBreak
}
_, err = stmt.Exec(dbTag, startTime, endTime)