From ebc11dd507cc257aa121aee36320f9fcc5423264 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 09:16:26 +0100 Subject: [PATCH 1/8] refactor(config): change config location and keyword names --- main.go | 172 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/main.go b/main.go index b810d11..dfa13ab 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "net" "os" "os/exec" + "path/filepath" "strings" "time" @@ -16,29 +17,36 @@ import ( ) type Config struct { - SSHUser string `mapstructure:"SSH_USER"` - SSHHost string `mapstructure:"SSH_HOST"` - VardaUser string `mapstructure:"VARDA_USER"` - VardaHost string `mapstructure:"VARDA_HOST"` - LouHost string `mapstructure:"LOU_HOST"` - LouUser string `mapstructure:"LOU_USER"` - LouMac string `mapstructure:"LOU_MAC"` - RDPUser string `mapstructure:"RDP_USER"` - RDPPassword string `mapstructure:"RDP_PASSWORD"` - LouIP string `mapstructure:"LOU_IP"` - SSHPort int `mapstructure:"SSH_PORT"` + SSHUser string `mapstructure:"SSH_USER"` + SSHHost string `mapstructure:"SSH_HOST"` + JumpUser string `mapstructure:"JUMP_USER"` + JumpHost string `mapstructure:"JUMP_HOST"` + WorkstationHost string `mapstructure:"WORKSTATION_HOST"` + WorkstationUser string `mapstructure:"WWORKSTATION_USER"` + WorkstationMac string `mapstructure:"WORKSTATION_MAC"` + RDPUser string `mapstructure:"RDP_USER"` + RDPPassword string `mapstructure:"RDP_PASSWORD"` + WorkstationIP string `mapstructure:"WORKSTATION_IP"` + SSHPort int `mapstructure:"SSH_PORT"` } var cfg Config func init() { log.Println("Loading Config File") - viper.SetConfigFile("work-config.toml") + configPath, err := os.UserConfigDir() + if err != nil { + return + } + + workConfigPath := filepath.Join(configPath, "work") + configFile := filepath.Join(workConfigPath, "config.toml") + viper.SetConfigFile(configFile) viper.SetConfigType("toml") viper.AddConfigPath(".") viper.AutomaticEnv() - err := viper.ReadInConfig() + err = viper.ReadInConfig() if err != nil { log.Printf("error loading config file: %v", err) } @@ -51,68 +59,6 @@ func init() { } } -func main() { - // err := LoadConfig() - // if err != nil { - // log.Printf("error: %v", err) - // } - var choice string - form := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("What would you like to do?"). - Options( - huh.NewOption("Start", "Start"), - huh.NewOption("Stop Work", "stop Work"), - huh.NewOption("Show Week Summary", "show week summary"), - huh.NewOption("Show Month Summary", "show month summary"), - huh.NewOption("Start Break", "start break"), - huh.NewOption("Stop Break", "stop break"), - huh.NewOption("Export Timetable", "export"), - huh.NewOption("Wake Lou", "wake lou"), - huh.NewOption("Connect to Varda", "connect to varda"), - huh.NewOption("Connect to Lou", "connect to lou"), - huh.NewOption("Start RDP Connection", "start rdp connection"), - ). - Value(&choice), - ), - ) - - err := form.Run() - if err != nil { - fmt.Println("Error:", err) - return - } - - switch choice { - case "Start": - connect() - // startSequence() - case "stop Work": - runCommand("timew", "stop", "work") - case "start break": - runCommand("timew", "stop", "work") - runCommand("timew", "start", "break") - case "stop break": - runCommand("timew", "stop", "break") - runCommand("timew", "start", "work") - case "show week summary": - runCommand("timew", "summary", ":week", "work") - case "show month summary": - runCommand("timew", "summary", ":month", "work") - case "wake lou": - wakeLou() - case "connect to varda": - connectToVarda() - case "connect to lou": - connectToLou() - case "start rdp connection": - startRDPConnection() - case "export": - fmt.Println("Exporting Work times") - } -} - func connect() { runCommand("timew", "start", "work") wakeLou() @@ -128,9 +74,9 @@ func connect() { } defer session.Close() - go forwardPort(sshClient, "2048", cfg.LouIP, "22") + go forwardPort(sshClient, "2048", cfg.WorkstationIP, "22") - go forwardPort(sshClient, "6000", cfg.LouIP, "3389") + go forwardPort(sshClient, "6000", cfg.WorkstationIP, "3389") connectToLou() } @@ -212,9 +158,9 @@ func wakeLou() { fmt.Sprintf("%v", cfg.SSHPort), cfg.SSHUser, cfg.SSHHost, - cfg.VardaUser, - cfg.VardaHost, - cfg.LouMac) + cfg.JumpUser, + cfg.JumpHost, + cfg.WorkstationMac) args := strings.Split(sshCommand, " ") log.Println(args) runCommand("ssh", args[1:]...) @@ -222,7 +168,7 @@ func wakeLou() { func connectToVarda() { sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", - cfg.LouHost, + cfg.WorkstationHost, cfg.SSHUser, cfg.SSHHost) args := strings.Split(sshCommand, " ") @@ -231,7 +177,7 @@ func connectToVarda() { func connectToLou() { sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", - cfg.LouHost, + cfg.WorkstationHost, cfg.SSHUser) args := strings.Split(sshCommand, " ") runCommand("ssh", args[1:]...) @@ -243,3 +189,65 @@ func startRDPConnection() { cfg.RDPPassword) runCommand("bash", "-c", rdpCommand) } + +func makeChoice() { + var choice string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("What would you like to do?"). + Options( + huh.NewOption("Start", "Start"), + huh.NewOption("Stop Work", "stop Work"), + huh.NewOption("Show Week Summary", "show week summary"), + huh.NewOption("Show Month Summary", "show month summary"), + huh.NewOption("Start Break", "start break"), + huh.NewOption("Stop Break", "stop break"), + huh.NewOption("Export Timetable", "export"), + huh.NewOption("Wake Lou", "wake lou"), + huh.NewOption("Connect to Varda", "connect to varda"), + huh.NewOption("Connect to Lou", "connect to lou"), + huh.NewOption("Start RDP Connection", "start rdp connection"), + ). + Value(&choice), + ), + ) + + err := form.Run() + if err != nil { + fmt.Println("Error:", err) + return + } + + switch choice { + case "Start": + connect() + // startSequence() + case "stop Work": + runCommand("timew", "stop", "work") + case "start break": + runCommand("timew", "stop", "work") + runCommand("timew", "start", "break") + case "stop break": + runCommand("timew", "stop", "break") + runCommand("timew", "start", "work") + case "show week summary": + runCommand("timew", "summary", ":week", "work") + case "show month summary": + runCommand("timew", "summary", ":month", "work") + case "wake lou": + wakeLou() + case "connect to varda": + connectToVarda() + case "connect to lou": + connectToLou() + case "start rdp connection": + startRDPConnection() + case "export": + fmt.Println("Exporting Work times") + } +} + +func main() { + makeChoice() +} From 35dd5d7885095d88626910244fef28bc77c80732 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 09:35:05 +0100 Subject: [PATCH 2/8] refactor: change FunctionNames to be more generic --- main.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index dfa13ab..e2273dc 100644 --- a/main.go +++ b/main.go @@ -61,7 +61,7 @@ func init() { func connect() { runCommand("timew", "start", "work") - wakeLou() + wakeWorkstation() sshClient, err := ssh.Dial("tcp", cfg.SSHHost+":"+fmt.Sprintf("%v", cfg.SSHPort), makeSSHClient()) if err != nil { log.Fatal("Failed to dial: ", err) @@ -78,7 +78,7 @@ func connect() { go forwardPort(sshClient, "6000", cfg.WorkstationIP, "3389") - connectToLou() + connectToWorkstation() } func forwardPort(sshConn *ssh.Client, localPort, remoteHost, remotePort string) { @@ -149,11 +149,11 @@ func runCommand(name string, args ...string) { func startSequence() { runCommand("timew", "start", "work") - wakeLou() - connectToVarda() + wakeWorkstation() + connectToJump() } -func wakeLou() { +func wakeWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -p %s %s@%s ssh -tt %s@%s \"wakeonlan %s && exit\"", fmt.Sprintf("%v", cfg.SSHPort), cfg.SSHUser, @@ -166,7 +166,7 @@ func wakeLou() { runCommand("ssh", args[1:]...) } -func connectToVarda() { +func connectToJump() { sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", cfg.WorkstationHost, cfg.SSHUser, @@ -175,7 +175,7 @@ func connectToVarda() { runCommand("ssh", args[1:]...) } -func connectToLou() { +func connectToWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", cfg.WorkstationHost, cfg.SSHUser) @@ -236,11 +236,11 @@ func makeChoice() { case "show month summary": runCommand("timew", "summary", ":month", "work") case "wake lou": - wakeLou() + wakeWorkstation() case "connect to varda": - connectToVarda() + connectToJump() case "connect to lou": - connectToLou() + connectToWorkstation() case "start rdp connection": startRDPConnection() case "export": From c052e6daf05df34d3613c63dc8d3f7e05f805063 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 09:58:27 +0100 Subject: [PATCH 3/8] feat: add cli commands for start, stop and show --- main.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index e2273dc..6613fba 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "time" "github.com/charmbracelet/huh" + "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) @@ -22,7 +23,7 @@ type Config struct { JumpUser string `mapstructure:"JUMP_USER"` JumpHost string `mapstructure:"JUMP_HOST"` WorkstationHost string `mapstructure:"WORKSTATION_HOST"` - WorkstationUser string `mapstructure:"WWORKSTATION_USER"` + WorkstationUser string `mapstructure:"WORKSTATION_USER"` WorkstationMac string `mapstructure:"WORKSTATION_MAC"` RDPUser string `mapstructure:"RDP_USER"` RDPPassword string `mapstructure:"RDP_PASSWORD"` @@ -30,9 +31,24 @@ type Config struct { SSHPort int `mapstructure:"SSH_PORT"` } -var cfg Config +type Flags struct { + ShowWeek bool + ShowMonth bool + ShowExport bool +} + +var ( + cfg Config + flags Flags +) func init() { + showCmd.Flags().BoolVarP(&flags.ShowWeek, "week", "w", false, "show timewarrior week summary") + showCmd.Flags().BoolVarP(&flags.ShowMonth, "month", "m", false, "show timewarrior month summary") + showCmd.Flags().BoolVarP(&flags.ShowExport, "export", "e", false, "export timewarrior timetable") + rootCmd.AddCommand(startCmd) + rootCmd.AddCommand(stopCmd) + rootCmd.AddCommand(showCmd) log.Println("Loading Config File") configPath, err := os.UserConfigDir() if err != nil { @@ -50,8 +66,8 @@ func init() { if err != nil { log.Printf("error loading config file: %v", err) } - allSettings := viper.AllSettings() - log.Printf("All loaded settings: %+v", allSettings) + // allSettings := viper.AllSettings() + // log.Printf("All loaded settings: %+v", allSettings) err = viper.UnmarshalKey("default", &cfg) if err != nil { @@ -248,6 +264,56 @@ func makeChoice() { } } -func main() { - makeChoice() +var rootCmd = &cobra.Command{ + Use: "work", + Short: "Fast work interactions", + Long: `A CLI tool to perform basic work cli tasks.`, +} + +var startCmd = &cobra.Command{ + Use: "start", + Short: "start work", + Long: "command to start the work day", + Run: func(cmd *cobra.Command, args []string) { + connect() + }, +} + +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "stop work", + Long: "command to stop the work day", + Run: func(cmd *cobra.Command, args []string) { + runCommand("timew", "stop", "work") + os.Exit(0) + }, +} + +var showCmd = &cobra.Command{ + Use: "show", + Short: "show timetracking", + Long: "show different timetracking", + Run: func(cmd *cobra.Command, args []string) { + if flags.ShowExport { + fmt.Println("Exporting timetable") + } + if flags.ShowWeek { + runCommand("timew", "summary", ":week", "work") + } + if flags.ShowMonth { + runCommand("timew", "summary", ":month", "work") + } + os.Exit(0) + }, +} + +func main() { + if len(os.Args) > 1 { + if err := rootCmd.Execute(); err != nil { + log.Fatalf("error executing command: %v", err) + } + // return + } else { + makeChoice() + } } From aa3000cce67392ae9b39df3fc9d13177e8be2b6e Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 11:21:28 +0100 Subject: [PATCH 4/8] refactor: introduce app struct --- main.go | 246 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 132 insertions(+), 114 deletions(-) diff --git a/main.go b/main.go index 6613fba..fc40ac7 100644 --- a/main.go +++ b/main.go @@ -37,48 +37,51 @@ type Flags struct { ShowExport bool } -var ( +type App struct { cfg Config flags Flags -) +} -func init() { - showCmd.Flags().BoolVarP(&flags.ShowWeek, "week", "w", false, "show timewarrior week summary") - showCmd.Flags().BoolVarP(&flags.ShowMonth, "month", "m", false, "show timewarrior month summary") - showCmd.Flags().BoolVarP(&flags.ShowExport, "export", "e", false, "export timewarrior timetable") - rootCmd.AddCommand(startCmd) - rootCmd.AddCommand(stopCmd) - rootCmd.AddCommand(showCmd) - log.Println("Loading Config File") +func NewApp() (*App, error) { + cfg, err := loadConfig() + if err != nil { + return nil, fmt.Errorf("error loading config: %w", err) + } + + return &App{ + cfg: cfg, + }, nil +} + +func loadConfig() (Config, error) { + var cfg Config configPath, err := os.UserConfigDir() if err != nil { - return + return cfg, err } workConfigPath := filepath.Join(configPath, "work") configFile := filepath.Join(workConfigPath, "config.toml") + viper.SetConfigFile(configFile) viper.SetConfigType("toml") viper.AddConfigPath(".") viper.AutomaticEnv() - - err = viper.ReadInConfig() - if err != nil { - log.Printf("error loading config file: %v", err) + if err := viper.ReadInConfig(); err != nil { + return cfg, fmt.Errorf("error reading config file: %w", err) } - // allSettings := viper.AllSettings() - // log.Printf("All loaded settings: %+v", allSettings) - err = viper.UnmarshalKey("default", &cfg) - if err != nil { - log.Printf("unable to decode into struct: %v", err) + if err := viper.UnmarshalKey("default", &cfg); err != nil { + return cfg, fmt.Errorf("error decoding config: %w", err) } + + return cfg, nil } -func connect() { - runCommand("timew", "start", "work") - wakeWorkstation() - sshClient, err := ssh.Dial("tcp", cfg.SSHHost+":"+fmt.Sprintf("%v", cfg.SSHPort), makeSSHClient()) +func (a *App) connect() { + a.runCommand("timew", "start", "work") + a.wakeWorkstation() + sshClient, err := ssh.Dial("tcp", a.cfg.SSHHost+":"+fmt.Sprintf("%v", a.cfg.SSHPort), a.makeSSHClient()) if err != nil { log.Fatal("Failed to dial: ", err) } @@ -90,11 +93,11 @@ func connect() { } defer session.Close() - go forwardPort(sshClient, "2048", cfg.WorkstationIP, "22") + go forwardPort(sshClient, "2048", a.cfg.WorkstationIP, "22") - go forwardPort(sshClient, "6000", cfg.WorkstationIP, "3389") + go forwardPort(sshClient, "6000", a.cfg.WorkstationIP, "3389") - connectToWorkstation() + a.connectToWorkstation() } func forwardPort(sshConn *ssh.Client, localPort, remoteHost, remotePort string) { @@ -130,20 +133,20 @@ func copyConn(dst, src net.Conn) { io.Copy(dst, src) } -func makeSSHClient() *ssh.ClientConfig { +func (a *App) makeSSHClient() *ssh.ClientConfig { keypath := os.ExpandEnv("$HOME/.ssh/hegenberg") keyBytes, err := os.ReadFile(keypath) if err != nil { log.Fatalf("Failed to read private key: %s", err) } - key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(cfg.RDPPassword)) + key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword)) if err != nil { log.Fatalf("Failed to parse private key: %s", err) } return &ssh.ClientConfig{ - User: cfg.SSHUser, + User: a.cfg.SSHUser, Auth: []ssh.AuthMethod{ ssh.PublicKeys(key), }, @@ -152,7 +155,7 @@ func makeSSHClient() *ssh.ClientConfig { } } -func runCommand(name string, args ...string) { +func (a *App) runCommand(name string, args ...string) { cmd := exec.Command(name, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -163,50 +166,44 @@ func runCommand(name string, args ...string) { } } -func startSequence() { - runCommand("timew", "start", "work") - wakeWorkstation() - connectToJump() -} - -func wakeWorkstation() { +func (a *App) wakeWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -p %s %s@%s ssh -tt %s@%s \"wakeonlan %s && exit\"", - fmt.Sprintf("%v", cfg.SSHPort), - cfg.SSHUser, - cfg.SSHHost, - cfg.JumpUser, - cfg.JumpHost, - cfg.WorkstationMac) + fmt.Sprintf("%v", a.cfg.SSHPort), + a.cfg.SSHUser, + a.cfg.SSHHost, + a.cfg.JumpUser, + a.cfg.JumpHost, + a.cfg.WorkstationMac) args := strings.Split(sshCommand, " ") log.Println(args) - runCommand("ssh", args[1:]...) + a.runCommand("ssh", args[1:]...) } -func connectToJump() { +func (a *App) connectToJump() { sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", - cfg.WorkstationHost, - cfg.SSHUser, - cfg.SSHHost) + a.cfg.WorkstationHost, + a.cfg.SSHUser, + a.cfg.SSHHost) args := strings.Split(sshCommand, " ") - runCommand("ssh", args[1:]...) + a.runCommand("ssh", args[1:]...) } -func connectToWorkstation() { +func (a *App) connectToWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", - cfg.WorkstationHost, - cfg.SSHUser) + a.cfg.WorkstationHost, + a.cfg.SSHUser) args := strings.Split(sshCommand, " ") - runCommand("ssh", args[1:]...) + a.runCommand("ssh", args[1:]...) } -func startRDPConnection() { +func (a *App) startRDPConnection() { rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350", - cfg.RDPUser, - cfg.RDPPassword) - runCommand("bash", "-c", rdpCommand) + a.cfg.RDPUser, + a.cfg.RDPPassword) + a.runCommand("bash", "-c", rdpCommand) } -func makeChoice() { +func (a *App) makeChoice() { var choice string form := huh.NewForm( huh.NewGroup( @@ -220,9 +217,9 @@ func makeChoice() { huh.NewOption("Start Break", "start break"), huh.NewOption("Stop Break", "stop break"), huh.NewOption("Export Timetable", "export"), - huh.NewOption("Wake Lou", "wake lou"), - huh.NewOption("Connect to Varda", "connect to varda"), - huh.NewOption("Connect to Lou", "connect to lou"), + huh.NewOption("Wake Workstation", "wake workstation"), + huh.NewOption("Connect to Jump", "connect to jump"), + huh.NewOption("Connect to Workstation", "connect to workstation"), huh.NewOption("Start RDP Connection", "start rdp connection"), ). Value(&choice), @@ -237,83 +234,104 @@ func makeChoice() { switch choice { case "Start": - connect() + a.connect() // startSequence() case "stop Work": - runCommand("timew", "stop", "work") + a.runCommand("timew", "stop", "work") case "start break": - runCommand("timew", "stop", "work") - runCommand("timew", "start", "break") + a.runCommand("timew", "track", "break") case "stop break": - runCommand("timew", "stop", "break") - runCommand("timew", "start", "work") + a.runCommand("timew", "track", "work") case "show week summary": - runCommand("timew", "summary", ":week", "work") + a.runCommand("timew", "summary", ":week", "work") case "show month summary": - runCommand("timew", "summary", ":month", "work") - case "wake lou": - wakeWorkstation() - case "connect to varda": - connectToJump() - case "connect to lou": - connectToWorkstation() + a.runCommand("timew", "summary", ":month", "work") + case "wake workstation": + a.wakeWorkstation() + case "connect to jump": + a.connectToJump() + case "connect to workstation": + a.connectToWorkstation() case "start rdp connection": - startRDPConnection() + a.startRDPConnection() case "export": fmt.Println("Exporting Work times") } } -var rootCmd = &cobra.Command{ - Use: "work", - Short: "Fast work interactions", - Long: `A CLI tool to perform basic work cli tasks.`, +func (a *App) setupCommands() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "work", + Short: "Fast work interactions", + Long: `A CLI tool to perform basic work cli tasks.`, + } + + rootCmd.AddCommand(a.startCommand()) + rootCmd.AddCommand(a.stopCommand()) + rootCmd.AddCommand(a.showCommand()) + return rootCmd } -var startCmd = &cobra.Command{ - Use: "start", - Short: "start work", - Long: "command to start the work day", - Run: func(cmd *cobra.Command, args []string) { - connect() - }, +func (a *App) startCommand() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "start work", + Long: "command to start the work day", + Run: func(cmd *cobra.Command, args []string) { + a.connect() + }, + } } -var stopCmd = &cobra.Command{ - Use: "stop", - Short: "stop work", - Long: "command to stop the work day", - Run: func(cmd *cobra.Command, args []string) { - runCommand("timew", "stop", "work") - os.Exit(0) - }, +func (a *App) stopCommand() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "stop work", + Long: "command to stop the work day", + Run: func(cmd *cobra.Command, args []string) { + a.runCommand("timew", "stop", "work") + os.Exit(0) + }, + } } -var showCmd = &cobra.Command{ - Use: "show", - Short: "show timetracking", - Long: "show different timetracking", - Run: func(cmd *cobra.Command, args []string) { - if flags.ShowExport { - fmt.Println("Exporting timetable") - } - if flags.ShowWeek { - runCommand("timew", "summary", ":week", "work") - } - if flags.ShowMonth { - runCommand("timew", "summary", ":month", "work") - } - os.Exit(0) - }, +func (a *App) showCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "show", + Short: "show timetracking", + Long: "show different timetracking", + Run: func(cmd *cobra.Command, args []string) { + if a.flags.ShowExport { + fmt.Println("Exporting timetable") + } + if a.flags.ShowWeek { + a.runCommand("timew", "summary", ":week", "work") + } + if a.flags.ShowMonth { + a.runCommand("timew", "summary", ":month", "work") + } + os.Exit(0) + }, + } + + cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary") + cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary") + cmd.Flags().BoolVarP(&a.flags.ShowExport, "export", "e", false, "export timewarrior timetable") + + return cmd } func main() { + app, err := NewApp() + if err != nil { + log.Fatalf("unable to setup application: %v", err) + } if len(os.Args) > 1 { - if err := rootCmd.Execute(); err != nil { + if err := app.setupCommands().Execute(); err != nil { log.Fatalf("error executing command: %v", err) } // return } else { - makeChoice() + app.makeChoice() } } From fec43ea77d538cd0fd9b6ac5100dedaef2bab469 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 11:40:22 +0100 Subject: [PATCH 5/8] refactor: introduce TimeWarrior struct --- main.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index fc40ac7..d1ac7b9 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,9 @@ func loadConfig() (Config, error) { } func (a *App) connect() { - a.runCommand("timew", "start", "work") + tw := NewTimeWarrior() + tw.StartWork() + // a.runCommand("timew", "start", "work") a.wakeWorkstation() sshClient, err := ssh.Dial("tcp", a.cfg.SSHHost+":"+fmt.Sprintf("%v", a.cfg.SSHPort), a.makeSSHClient()) if err != nil { @@ -205,6 +207,7 @@ func (a *App) startRDPConnection() { func (a *App) makeChoice() { var choice string + tw := NewTimeWarrior() form := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). @@ -235,17 +238,16 @@ func (a *App) makeChoice() { switch choice { case "Start": a.connect() - // startSequence() case "stop Work": - a.runCommand("timew", "stop", "work") + tw.StopWork() case "start break": - a.runCommand("timew", "track", "break") + tw.StartBreak() case "stop break": - a.runCommand("timew", "track", "work") + tw.StopBreak() case "show week summary": - a.runCommand("timew", "summary", ":week", "work") + tw.ShowSummary(":week") case "show month summary": - a.runCommand("timew", "summary", ":month", "work") + tw.ShowSummary(":month") case "wake workstation": a.wakeWorkstation() case "connect to jump": @@ -255,10 +257,49 @@ func (a *App) makeChoice() { case "start rdp connection": a.startRDPConnection() case "export": - fmt.Println("Exporting Work times") + tw.ExportSummary() } } +type TimeWarrior struct{} + +func NewTimeWarrior() *TimeWarrior { + return &TimeWarrior{} +} + +func (t *TimeWarrior) runCommand(args ...string) error { + cmd := exec.Command("timew", args...) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (t *TimeWarrior) StartWork() error { + return t.runCommand("start", "work") +} + +func (t *TimeWarrior) StopWork() error { + return t.runCommand("stop", "work") +} + +func (t *TimeWarrior) StartBreak() error { + return t.runCommand("track", "break") +} + +func (t *TimeWarrior) StopBreak() error { + return t.runCommand("track", "work") +} + +func (t *TimeWarrior) ShowSummary(period string) error { + return t.runCommand("summary", period, "work") +} + +func (t *TimeWarrior) ExportSummary() error { + fmt.Println("Export Timetable") + return nil +} + func (a *App) setupCommands() *cobra.Command { rootCmd := &cobra.Command{ Use: "work", @@ -330,7 +371,6 @@ func main() { if err := app.setupCommands().Execute(); err != nil { log.Fatalf("error executing command: %v", err) } - // return } else { app.makeChoice() } From a7ea6f41a7de3ab5537c720494ef17c19670eafb Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 12:50:04 +0100 Subject: [PATCH 6/8] refactor: seperate application into seperate files and add killForwarding --- app.go | 230 +++++++++++++++++++++++++++++++ cmd.go | 75 +++++++++++ config.go | 54 ++++++++ forwarder.go | 58 ++++++++ main.go | 357 ------------------------------------------------- ssh.go | 18 +++ timewarrior.go | 46 +++++++ 7 files changed, 481 insertions(+), 357 deletions(-) create mode 100644 app.go create mode 100644 cmd.go create mode 100644 config.go create mode 100644 forwarder.go create mode 100644 ssh.go create mode 100644 timewarrior.go diff --git a/app.go b/app.go new file mode 100644 index 0000000..c03ba19 --- /dev/null +++ b/app.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + "github.com/charmbracelet/huh" + "golang.org/x/crypto/ssh" +) + +type App struct { + cfg Config + flags Flags +} + +func NewApp() (*App, error) { + cfg, err := loadConfig() + if err != nil { + return nil, fmt.Errorf("error loading config: %w", err) + } + + return &App{ + cfg: cfg, + }, nil +} + +func (a *App) connect() { + tw := NewTimeWarrior() + tw.StartWork() + a.wakeWorkstation() + sshCon, err := a.newSSHConnection() + if err != nil { + log.Fatalf("failed to establish ssh-connection: %v", err) + } + defer sshCon.Close() + + sshFowarder := NewPortForwarder(sshCon.client, "2048", "22", a.cfg.WorkstationIP) + go sshFowarder.forward() + + rdpFowarder := NewPortForwarder(sshCon.client, "6000", "3389", a.cfg.WorkstationIP) + go rdpFowarder.forward() + + a.connectToWorkstation() +} + +func (a *App) makeSSHClient() *ssh.ClientConfig { + keypath := os.ExpandEnv("$HOME/.ssh/hegenberg") + keyBytes, err := os.ReadFile(keypath) + if err != nil { + log.Fatalf("Failed to read private key: %s", err) + } + + key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword)) + if err != nil { + log.Fatalf("Failed to parse private key: %s", err) + } + + return &ssh.ClientConfig{ + User: a.cfg.SSHUser, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(key), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 5 * time.Second, + } +} + +func (a *App) runCommand(name string, args ...string) { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + err := cmd.Run() + if err != nil { + fmt.Println("Error:", err) + } +} + +func (a *App) wakeWorkstation() { + sshCommand := fmt.Sprintf("ssh -tt -p %s %s@%s ssh -tt %s@%s \"wakeonlan %s && exit\"", + fmt.Sprintf("%v", a.cfg.SSHPort), + a.cfg.SSHUser, + a.cfg.SSHHost, + a.cfg.JumpUser, + a.cfg.JumpHost, + a.cfg.WorkstationMac) + args := strings.Split(sshCommand, " ") + log.Println(args) + a.runCommand("ssh", args[1:]...) +} + +func (a *App) connectToJump() { + sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", + a.cfg.WorkstationHost, + a.cfg.SSHUser, + a.cfg.SSHHost) + args := strings.Split(sshCommand, " ") + a.runCommand("ssh", args[1:]...) +} + +func (a *App) connectToWorkstation() { + sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", + a.cfg.WorkstationHost, + a.cfg.SSHUser) + args := strings.Split(sshCommand, " ") + a.runCommand("ssh", args[1:]...) +} + +func (a *App) startRDPConnection() { + rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350", + a.cfg.RDPUser, + a.cfg.RDPPassword) + a.runCommand("bash", "-c", rdpCommand) +} + +func (a *App) makeChoice() { + var choice string + tw := NewTimeWarrior() + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("What would you like to do?"). + Options( + huh.NewOption("Start Work", "start work"), + huh.NewOption("Stop Work", "stop work"), + huh.NewOption("Show Week Summary", "show week summary"), + huh.NewOption("Show Month Summary", "show month summary"), + huh.NewOption("Start Break", "start break"), + huh.NewOption("Stop Break", "stop break"), + huh.NewOption("Export Timetable", "export"), + huh.NewOption("Connect to Jump", "connect to jump"), + huh.NewOption("Connect to Workstation", "connect to workstation"), + huh.NewOption("Start RDP Connection", "start rdp connection"), + ). + Value(&choice), + ), + ) + + err := form.Run() + if err != nil { + fmt.Println("Error:", err) + return + } + + switch choice { + case "start work": + a.connect() + case "stop work": + tw.StopWork() + case "start break": + tw.StartBreak() + case "stop break": + tw.StopBreak() + case "show week summary": + tw.ShowSummary(":week") + case "show month summary": + tw.ShowSummary(":month") + case "connect to jump": + a.connectToJump() + case "connect to workstation": + a.connectToWorkstation() + case "start rdp connection": + a.startRDPConnection() + case "export": + tw.ExportSummary() + } +} + +func (a *App) getSSHAuth() ssh.AuthMethod { + keypath := os.ExpandEnv("$HOME/.ssh/hegenberg") + keyBytes, err := os.ReadFile(keypath) + if err != nil { + fmt.Printf("unable to read private key: %v", err) + return nil + } + + key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword)) + if err != nil { + fmt.Printf("unable to parse privat key: %v", err) + return nil + } + + return ssh.PublicKeys(key) +} + +func (a *App) newSSHConnection() (*SSHConnection, error) { + config := &ssh.ClientConfig{ + User: a.cfg.SSHUser, + Auth: []ssh.AuthMethod{a.getSSHAuth()}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 5 * time.Second, + } + + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", a.cfg.SSHHost, a.cfg.SSHPort), config) + if err != nil { + return nil, fmt.Errorf("ssh dial failed: %w", err) + } + + session, err := client.NewSession() + if err != nil { + client.Close() + return nil, fmt.Errorf("creating ssh session failed: %w", err) + } + + return &SSHConnection{ + client: client, + session: session, + }, nil +} + +func (a *App) killForwardings() error { + ports := []string{"2048", "6000"} + for _, port := range ports { + cmd := exec.Command("lsof", "-ti", "tcp:"+port) + output, err := cmd.Output() + if err != nil { + continue + } + + pids := strings.Split(string(output), "\n") + pid := strings.TrimSpace(pids[0]) + killCmd := exec.Command("kill", pid) + killCmd.Run() + } + return nil +} diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..0dfd35b --- /dev/null +++ b/cmd.go @@ -0,0 +1,75 @@ +package main + +import ( + "log" + "os" + + "github.com/spf13/cobra" +) + +func (a *App) setupCommands() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "work", + Short: "Fast work interactions", + Long: `A CLI tool to perform basic work cli tasks.`, + } + + rootCmd.AddCommand(a.startCommand()) + rootCmd.AddCommand(a.stopCommand()) + rootCmd.AddCommand(a.showCommand()) + return rootCmd +} + +func (a *App) startCommand() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "start work", + Long: "command to start the work day", + Run: func(cmd *cobra.Command, args []string) { + a.connect() + }, + } +} + +func (a *App) stopCommand() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "stop work", + Long: "command to stop the work day", + Run: func(cmd *cobra.Command, args []string) { + tw := NewTimeWarrior() + tw.StopWork() + if err := a.killForwardings(); err != nil { + log.Printf("error stoping port forwarding: %v", err) + } + os.Exit(0) + }, + } +} + +func (a *App) showCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "show", + Short: "show timetracking", + Long: "show different timetracking", + Run: func(cmd *cobra.Command, args []string) { + tw := NewTimeWarrior() + if a.flags.ShowExport { + tw.ExportSummary() + } + if a.flags.ShowWeek { + tw.ShowSummary(":week") + } + if a.flags.ShowMonth { + tw.ShowSummary(":month") + } + os.Exit(0) + }, + } + + cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary") + cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary") + cmd.Flags().BoolVarP(&a.flags.ShowExport, "export", "e", false, "export timewarrior timetable") + + return cmd +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..b5be710 --- /dev/null +++ b/config.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" +) + +type Config struct { + SSHUser string `mapstructure:"SSH_USER"` + SSHHost string `mapstructure:"SSH_HOST"` + JumpUser string `mapstructure:"JUMP_USER"` + JumpHost string `mapstructure:"JUMP_HOST"` + WorkstationHost string `mapstructure:"WORKSTATION_HOST"` + WorkstationUser string `mapstructure:"WORKSTATION_USER"` + WorkstationMac string `mapstructure:"WORKSTATION_MAC"` + RDPUser string `mapstructure:"RDP_USER"` + RDPPassword string `mapstructure:"RDP_PASSWORD"` + WorkstationIP string `mapstructure:"WORKSTATION_IP"` + SSHPort int `mapstructure:"SSH_PORT"` +} + +type Flags struct { + ShowWeek bool + ShowMonth bool + ShowExport bool +} + +func loadConfig() (Config, error) { + var cfg Config + configPath, err := os.UserConfigDir() + if err != nil { + return cfg, err + } + + workConfigPath := filepath.Join(configPath, "work") + configFile := filepath.Join(workConfigPath, "config.toml") + + viper.SetConfigFile(configFile) + viper.SetConfigType("toml") + viper.AddConfigPath(".") + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { + return cfg, fmt.Errorf("error reading config file: %w", err) + } + + if err := viper.UnmarshalKey("default", &cfg); err != nil { + return cfg, fmt.Errorf("error decoding config: %w", err) + } + + return cfg, nil +} diff --git a/forwarder.go b/forwarder.go new file mode 100644 index 0000000..6d56d2d --- /dev/null +++ b/forwarder.go @@ -0,0 +1,58 @@ +package main + +import ( + "io" + "log" + "net" + + "golang.org/x/crypto/ssh" +) + +type PortFowarder struct { + sshCon *ssh.Client + localPort string + remotePort string + remoteHost string +} + +func NewPortForwarder(sshCon *ssh.Client, localPort, remotePort, remoteHost string) *PortFowarder { + return &PortFowarder{ + sshCon: sshCon, + localPort: localPort, + remotePort: remotePort, + remoteHost: remoteHost, + } +} + +func (pw *PortFowarder) forward() error { + listener, err := net.Listen("tcp", "127.0.0.1:"+pw.localPort) + if err != nil { + log.Printf("Fehler beim Öffnen des lokalen Ports %s: %v", pw.localPort, err) + return err + } + defer listener.Close() + + for { + localConn, err := listener.Accept() + if err != nil { + log.Printf("Fehler beim Akzeptieren der Verbindung: %v", err) + continue + } + + remoteConn, err := pw.sshCon.Dial("tcp", pw.remoteHost+":"+pw.remotePort) + if err != nil { + log.Printf("Fehler beim Verbinden zum Remote-Host %s:%s: %v", pw.remoteHost, pw.remotePort, err) + localConn.Close() + continue + } + + go pw.copyConn(localConn, remoteConn) + go pw.copyConn(remoteConn, localConn) + } +} + +func (pw *PortFowarder) copyConn(dst, src net.Conn) { + defer dst.Close() + defer src.Close() + io.Copy(dst, src) +} diff --git a/main.go b/main.go index d1ac7b9..5a18829 100644 --- a/main.go +++ b/main.go @@ -1,367 +1,10 @@ package main import ( - "fmt" - "io" "log" - "net" "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/charmbracelet/huh" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "golang.org/x/crypto/ssh" ) -type Config struct { - SSHUser string `mapstructure:"SSH_USER"` - SSHHost string `mapstructure:"SSH_HOST"` - JumpUser string `mapstructure:"JUMP_USER"` - JumpHost string `mapstructure:"JUMP_HOST"` - WorkstationHost string `mapstructure:"WORKSTATION_HOST"` - WorkstationUser string `mapstructure:"WORKSTATION_USER"` - WorkstationMac string `mapstructure:"WORKSTATION_MAC"` - RDPUser string `mapstructure:"RDP_USER"` - RDPPassword string `mapstructure:"RDP_PASSWORD"` - WorkstationIP string `mapstructure:"WORKSTATION_IP"` - SSHPort int `mapstructure:"SSH_PORT"` -} - -type Flags struct { - ShowWeek bool - ShowMonth bool - ShowExport bool -} - -type App struct { - cfg Config - flags Flags -} - -func NewApp() (*App, error) { - cfg, err := loadConfig() - if err != nil { - return nil, fmt.Errorf("error loading config: %w", err) - } - - return &App{ - cfg: cfg, - }, nil -} - -func loadConfig() (Config, error) { - var cfg Config - configPath, err := os.UserConfigDir() - if err != nil { - return cfg, err - } - - workConfigPath := filepath.Join(configPath, "work") - configFile := filepath.Join(workConfigPath, "config.toml") - - viper.SetConfigFile(configFile) - viper.SetConfigType("toml") - viper.AddConfigPath(".") - viper.AutomaticEnv() - if err := viper.ReadInConfig(); err != nil { - return cfg, fmt.Errorf("error reading config file: %w", err) - } - - if err := viper.UnmarshalKey("default", &cfg); err != nil { - return cfg, fmt.Errorf("error decoding config: %w", err) - } - - return cfg, nil -} - -func (a *App) connect() { - tw := NewTimeWarrior() - tw.StartWork() - // a.runCommand("timew", "start", "work") - a.wakeWorkstation() - sshClient, err := ssh.Dial("tcp", a.cfg.SSHHost+":"+fmt.Sprintf("%v", a.cfg.SSHPort), a.makeSSHClient()) - if err != nil { - log.Fatal("Failed to dial: ", err) - } - defer sshClient.Close() - - session, err := sshClient.NewSession() - if err != nil { - log.Fatal("Failed to create session: ", err) - } - defer session.Close() - - go forwardPort(sshClient, "2048", a.cfg.WorkstationIP, "22") - - go forwardPort(sshClient, "6000", a.cfg.WorkstationIP, "3389") - - a.connectToWorkstation() -} - -func forwardPort(sshConn *ssh.Client, localPort, remoteHost, remotePort string) { - listener, err := net.Listen("tcp", "127.0.0.1:"+localPort) - if err != nil { - log.Printf("Fehler beim Öffnen des lokalen Ports %s: %v", localPort, err) - return - } - defer listener.Close() - - for { - localConn, err := listener.Accept() - if err != nil { - log.Printf("Fehler beim Akzeptieren der Verbindung: %v", err) - continue - } - - remoteConn, err := sshConn.Dial("tcp", remoteHost+":"+remotePort) - if err != nil { - log.Printf("Fehler beim Verbinden zum Remote-Host %s:%s: %v", remoteHost, remotePort, err) - localConn.Close() - continue - } - - go copyConn(localConn, remoteConn) - go copyConn(remoteConn, localConn) - } -} - -func copyConn(dst, src net.Conn) { - defer dst.Close() - defer src.Close() - io.Copy(dst, src) -} - -func (a *App) makeSSHClient() *ssh.ClientConfig { - keypath := os.ExpandEnv("$HOME/.ssh/hegenberg") - keyBytes, err := os.ReadFile(keypath) - if err != nil { - log.Fatalf("Failed to read private key: %s", err) - } - - key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword)) - if err != nil { - log.Fatalf("Failed to parse private key: %s", err) - } - - return &ssh.ClientConfig{ - User: a.cfg.SSHUser, - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(key), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: 5 * time.Second, - } -} - -func (a *App) runCommand(name string, args ...string) { - cmd := exec.Command(name, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - err := cmd.Run() - if err != nil { - fmt.Println("Error:", err) - } -} - -func (a *App) wakeWorkstation() { - sshCommand := fmt.Sprintf("ssh -tt -p %s %s@%s ssh -tt %s@%s \"wakeonlan %s && exit\"", - fmt.Sprintf("%v", a.cfg.SSHPort), - a.cfg.SSHUser, - a.cfg.SSHHost, - a.cfg.JumpUser, - a.cfg.JumpHost, - a.cfg.WorkstationMac) - args := strings.Split(sshCommand, " ") - log.Println(args) - a.runCommand("ssh", args[1:]...) -} - -func (a *App) connectToJump() { - sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", - a.cfg.WorkstationHost, - a.cfg.SSHUser, - a.cfg.SSHHost) - args := strings.Split(sshCommand, " ") - a.runCommand("ssh", args[1:]...) -} - -func (a *App) connectToWorkstation() { - sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", - a.cfg.WorkstationHost, - a.cfg.SSHUser) - args := strings.Split(sshCommand, " ") - a.runCommand("ssh", args[1:]...) -} - -func (a *App) startRDPConnection() { - rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350", - a.cfg.RDPUser, - a.cfg.RDPPassword) - a.runCommand("bash", "-c", rdpCommand) -} - -func (a *App) makeChoice() { - var choice string - tw := NewTimeWarrior() - form := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("What would you like to do?"). - Options( - huh.NewOption("Start", "Start"), - huh.NewOption("Stop Work", "stop Work"), - huh.NewOption("Show Week Summary", "show week summary"), - huh.NewOption("Show Month Summary", "show month summary"), - huh.NewOption("Start Break", "start break"), - huh.NewOption("Stop Break", "stop break"), - huh.NewOption("Export Timetable", "export"), - huh.NewOption("Wake Workstation", "wake workstation"), - huh.NewOption("Connect to Jump", "connect to jump"), - huh.NewOption("Connect to Workstation", "connect to workstation"), - huh.NewOption("Start RDP Connection", "start rdp connection"), - ). - Value(&choice), - ), - ) - - err := form.Run() - if err != nil { - fmt.Println("Error:", err) - return - } - - switch choice { - case "Start": - a.connect() - case "stop Work": - tw.StopWork() - case "start break": - tw.StartBreak() - case "stop break": - tw.StopBreak() - case "show week summary": - tw.ShowSummary(":week") - case "show month summary": - tw.ShowSummary(":month") - case "wake workstation": - a.wakeWorkstation() - case "connect to jump": - a.connectToJump() - case "connect to workstation": - a.connectToWorkstation() - case "start rdp connection": - a.startRDPConnection() - case "export": - tw.ExportSummary() - } -} - -type TimeWarrior struct{} - -func NewTimeWarrior() *TimeWarrior { - return &TimeWarrior{} -} - -func (t *TimeWarrior) runCommand(args ...string) error { - cmd := exec.Command("timew", args...) - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func (t *TimeWarrior) StartWork() error { - return t.runCommand("start", "work") -} - -func (t *TimeWarrior) StopWork() error { - return t.runCommand("stop", "work") -} - -func (t *TimeWarrior) StartBreak() error { - return t.runCommand("track", "break") -} - -func (t *TimeWarrior) StopBreak() error { - return t.runCommand("track", "work") -} - -func (t *TimeWarrior) ShowSummary(period string) error { - return t.runCommand("summary", period, "work") -} - -func (t *TimeWarrior) ExportSummary() error { - fmt.Println("Export Timetable") - return nil -} - -func (a *App) setupCommands() *cobra.Command { - rootCmd := &cobra.Command{ - Use: "work", - Short: "Fast work interactions", - Long: `A CLI tool to perform basic work cli tasks.`, - } - - rootCmd.AddCommand(a.startCommand()) - rootCmd.AddCommand(a.stopCommand()) - rootCmd.AddCommand(a.showCommand()) - return rootCmd -} - -func (a *App) startCommand() *cobra.Command { - return &cobra.Command{ - Use: "start", - Short: "start work", - Long: "command to start the work day", - Run: func(cmd *cobra.Command, args []string) { - a.connect() - }, - } -} - -func (a *App) stopCommand() *cobra.Command { - return &cobra.Command{ - Use: "stop", - Short: "stop work", - Long: "command to stop the work day", - Run: func(cmd *cobra.Command, args []string) { - a.runCommand("timew", "stop", "work") - os.Exit(0) - }, - } -} - -func (a *App) showCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "show", - Short: "show timetracking", - Long: "show different timetracking", - Run: func(cmd *cobra.Command, args []string) { - if a.flags.ShowExport { - fmt.Println("Exporting timetable") - } - if a.flags.ShowWeek { - a.runCommand("timew", "summary", ":week", "work") - } - if a.flags.ShowMonth { - a.runCommand("timew", "summary", ":month", "work") - } - os.Exit(0) - }, - } - - cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary") - cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary") - cmd.Flags().BoolVarP(&a.flags.ShowExport, "export", "e", false, "export timewarrior timetable") - - return cmd -} - func main() { app, err := NewApp() if err != nil { diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..ee43275 --- /dev/null +++ b/ssh.go @@ -0,0 +1,18 @@ +package main + +import "golang.org/x/crypto/ssh" + +type SSHConnection struct { + client *ssh.Client + session *ssh.Session +} + +func (s *SSHConnection) Close() { + if s.session != nil { + s.session.Close() + } + + if s.client != nil { + s.client.Close() + } +} diff --git a/timewarrior.go b/timewarrior.go new file mode 100644 index 0000000..d3d3dc6 --- /dev/null +++ b/timewarrior.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +type TimeWarrior struct{} + +func NewTimeWarrior() *TimeWarrior { + return &TimeWarrior{} +} + +func (t *TimeWarrior) runCommand(args ...string) error { + cmd := exec.Command("timew", args...) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (t *TimeWarrior) StartWork() error { + return t.runCommand("start", "work") +} + +func (t *TimeWarrior) StopWork() error { + return t.runCommand("stop", "work") +} + +func (t *TimeWarrior) StartBreak() error { + return t.runCommand("track", "break") +} + +func (t *TimeWarrior) StopBreak() error { + return t.runCommand("track", "work") +} + +func (t *TimeWarrior) ShowSummary(period string) error { + return t.runCommand("summary", period, "work") +} + +func (t *TimeWarrior) ExportSummary() error { + fmt.Println("Export Timetable") + return nil +} From a6e298614334d6506ec5deaa98d2503058ce3427 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 16:56:03 +0100 Subject: [PATCH 7/8] feat: add basic excel export --- go.mod | 19 ++-- go.sum | 59 +++++++---- timewarrior.go | 274 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 156526d..0d287fc 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,10 @@ go 1.23.0 require ( github.com/charmbracelet/huh v0.5.3 - github.com/charmbracelet/log v0.4.0 - github.com/joho/godotenv v1.5.1 - github.com/magefile/mage v1.15.0 github.com/spf13/cobra v1.8.1 - golang.org/x/crypto v0.26.0 + github.com/spf13/viper v1.19.0 + github.com/xuri/excelize/v2 v2.9.0 + golang.org/x/crypto v0.28.0 ) require ( @@ -24,7 +23,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -34,10 +32,13 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -45,14 +46,16 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8711252..31108ec 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/charmbracelet/huh v0.5.3 h1:3KLP4a/K1/S4dq4xFMTNMt3XWhgMl/yx8NYtygQ0b github.com/charmbracelet/huh v0.5.3/go.mod h1:OZC3lshuF+/y8laj//DoZdFSHxC51OrtXLJI8xWVouQ= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= -github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= -github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.2.2 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0= github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= @@ -24,26 +22,29 @@ github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4h github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= -github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -56,6 +57,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4 github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -64,11 +67,19 @@ github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -98,25 +109,37 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/timewarrior.go b/timewarrior.go index d3d3dc6..c87654d 100644 --- a/timewarrior.go +++ b/timewarrior.go @@ -4,6 +4,12 @@ import ( "fmt" "os" "os/exec" + "sort" + "strconv" + "strings" + "time" + + "github.com/xuri/excelize/v2" ) type TimeWarrior struct{} @@ -40,7 +46,275 @@ func (t *TimeWarrior) ShowSummary(period string) error { return t.runCommand("summary", period, "work") } +type TimeEntry struct { + Week string + Date string + Day string + Start string + End string + Duration string + Tag string +} + func (t *TimeWarrior) ExportSummary() error { fmt.Println("Export Timetable") + exportCommand := exec.Command("timew", "summary", ":year") + output, err := exportCommand.Output() + if err != nil { + return err + } + lines := strings.Split(string(output), "\n") + if len(lines) > 2 { + var entries []TimeEntry + for _, line := range lines[3 : len(lines)-4] { + words := strings.Fields(line) + newLine := strings.Join(words, " ") + parts := strings.Split(strings.TrimSpace(newLine), " ") + entry := TimeEntry{} + + switch len(parts) { + case 4, 5: + entry.Tag = parts[0] + entry.Start = parts[1] + entry.End = parts[2] + entry.Duration = parts[3] + case 7, 8: + entry.Week = parts[0] + entry.Date = parts[1] + entry.Day = parts[2] + entry.Tag = parts[3] + entry.Start = parts[4] + entry.End = parts[5] + entry.Duration = parts[6] + default: + fmt.Println("Unknown length") + } + entries = append(entries, entry) + } + + dailySummary := make(map[string]*DailySummary) + var currentDate string + + for _, entry := range entries { + if entry.Date != "" { + currentDate = entry.Date + } + + if currentDate == "" { + continue + } + + if _, exists := dailySummary[currentDate]; !exists { + dailySummary[currentDate] = &DailySummary{ + Date: currentDate, + Day: entry.Day, + } + } + + summary := dailySummary[currentDate] + + switch strings.ToLower(entry.Tag) { + case "work": + if summary.WorkStart == "" { + summary.WorkStart = entry.Start + } + summary.WorkEnd = entry.End + case "break": + duration, _ := parseDuration(entry.Duration) + summary.BreakDuration += duration + case "uni", "free", "krank", "urlaub": + summary.Tag = entry.Tag + } + } + + var excelEntries []ExcelEntry + for _, summary := range dailySummary { + entry := ExcelEntry{ + Date: summary.Date, + Day: summary.Day, + WorkStart: summary.WorkStart, + WorkEnd: summary.WorkEnd, + BreakDuration: formatDuration(summary.BreakDuration), + Tag: summary.Tag, + } + excelEntries = append(excelEntries, entry) + } + + for _, entry := range excelEntries { + fmt.Printf("%+v\n", entry) + } + + err = writeExcelSheet(excelEntries) + if err != nil { + return err + } + } else { + fmt.Println("No Data") + } + + return nil +} + +type DailySummary struct { + Date string + Day string + WorkStart string + WorkEnd string + BreakDuration time.Duration + Tag string +} + +type ExcelEntry struct { + Date string + Day string + WorkStart string + WorkEnd string + BreakDuration string + Tag string +} + +func parseDuration(s string) (time.Duration, error) { + parts := strings.Split(s, ":") + if len(parts) != 3 { + return 0, fmt.Errorf("invalid duration format: %s", s) + } + + hours, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, err + } + + minutes, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, err + } + + seconds, err := strconv.Atoi(parts[2]) + if err != nil { + return 0, err + } + duration, err := time.ParseDuration(fmt.Sprintf("%vh%vm%vs", hours, minutes, seconds)) + if err != nil { + return 0, err + } + + return duration, nil +} + +func formatDuration(d time.Duration) string { + h := d / time.Hour + d -= h * time.Hour + m := d / time.Minute + d -= m * time.Minute + s := d / time.Second + return fmt.Sprintf("%d:%02d:%02d", h, m, s) +} + +func writeExcelSheet(entries []ExcelEntry) error { + sort.Slice(entries, func(i, j int) bool { + dateI, _ := time.Parse("2006-01-02", entries[i].Date) + dateJ, _ := time.Parse("2006-01-02", entries[j].Date) + return dateI.Before(dateJ) + }) + + sheetName := fmt.Sprint(time.Now().Year()) + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + + index, err := f.NewSheet(sheetName) + if err != nil { + return err + } + + // Überschriften setzen + f.SetCellValue(sheetName, "B1", "Arbeitszeiten "+sheetName) + f.SetCellValue(sheetName, "B3", "Datum") + f.SetCellValue(sheetName, "D3", "Arbeitszeit") + f.SetCellValue(sheetName, "G3", "Summe") + f.SetCellValue(sheetName, "I3", "Pause") + f.SetCellValue(sheetName, "J3", "Summe") + f.SetCellValue(sheetName, "K3", "Soll") + f.SetCellValue(sheetName, "L3", "Saldo") + f.SetCellValue(sheetName, "N3", "Saldo") + f.SetCellValue(sheetName, "D4", "von") + f.SetCellValue(sheetName, "E4", "bis") + f.SetCellValue(sheetName, "G4", "brutto") + f.SetCellValue(sheetName, "J4", "netto") + f.SetCellValue(sheetName, "L4", "Tag") + f.SetCellValue(sheetName, "N4", "total") + + strStyle := "hh:mm" + timeStyle, err := f.NewStyle(&excelize.Style{ + CustomNumFmt: &strStyle, + }) + if err != nil { + return err + } + + for num, entry := range entries { + var soll string + switch entry.Day { + case "Mon", "Tue": + soll = "08:00" + case "Wed": + soll = "04:00" + default: + soll = "" + } + + row := fmt.Sprint(num + 6) + + dateValue, _ := time.Parse("2006-01-02", entry.Date) + f.SetCellValue(sheetName, "B"+row, dateValue) + + if entry.Tag == "" { + startTime, _ := time.Parse("15:04:05", entry.WorkStart) + endTime, _ := time.Parse("15:04:05", entry.WorkEnd) + + startExcel := float64(startTime.Hour())/24.0 + float64(startTime.Minute())/(24.0*60.0) + endExcel := float64(endTime.Hour())/24.0 + float64(endTime.Minute())/(24.0*60.0) + + f.SetCellValue(sheetName, "D"+row, startExcel) + f.SetCellStyle(sheetName, "D"+row, "D"+row, timeStyle) + f.SetCellValue(sheetName, "E"+row, endExcel) + f.SetCellStyle(sheetName, "E"+row, "E"+row, timeStyle) + + // Formeln setzen + f.SetCellFormula(sheetName, "G"+row, fmt.Sprintf("E%d-D%d", num+6, num+6)) + f.SetCellStyle(sheetName, "G"+row, "G"+row, timeStyle) + + f.SetCellValue(sheetName, "I"+row, entry.BreakDuration) + f.SetCellFormula(sheetName, "J"+row, fmt.Sprintf("G%d-I%d", num+6, num+6)) + f.SetCellStyle(sheetName, "J"+row, "J"+row, timeStyle) + + f.SetCellValue(sheetName, "K"+row, soll) + f.SetCellStyle(sheetName, "K"+row, "K"+row, timeStyle) + f.SetCellFormula(sheetName, "L"+row, fmt.Sprintf("J%d-K%d", num+6, num+6)) + f.SetCellStyle(sheetName, "L"+row, "L"+row, timeStyle) + + } else { + text := "" + switch entry.Tag { + case "uni": + text = "Hochschule" + case "urlaub": + text = "Urlaub" + default: + text = "" + } + f.SetCellValue(sheetName, "D"+row, text) + } + f.SetCellFormula(sheetName, "N"+row, fmt.Sprintf("N%d+L%d", num+5, num+6)) + f.SetCellStyle(sheetName, "N"+row, "N"+row, timeStyle) + } + + f.SetActiveSheet(index) + if err := f.SaveAs("Test.xlsx"); err != nil { + fmt.Println(err) + } return nil } From b1d345b152c18f03e18dbef381cfd641c7130228 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Fri, 3 Jan 2025 17:14:19 +0100 Subject: [PATCH 8/8] feat: add filename to exported exceltable --- app.go | 2 +- cmd.go | 3 ++- config.go | 1 + timewarrior.go | 13 +++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index c03ba19..417772c 100644 --- a/app.go +++ b/app.go @@ -166,7 +166,7 @@ func (a *App) makeChoice() { case "start rdp connection": a.startRDPConnection() case "export": - tw.ExportSummary() + tw.ExportSummary(a.flags.ExportName) } } diff --git a/cmd.go b/cmd.go index 0dfd35b..3de9559 100644 --- a/cmd.go +++ b/cmd.go @@ -55,7 +55,7 @@ func (a *App) showCommand() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { tw := NewTimeWarrior() if a.flags.ShowExport { - tw.ExportSummary() + tw.ExportSummary(a.flags.ExportName) } if a.flags.ShowWeek { tw.ShowSummary(":week") @@ -70,6 +70,7 @@ func (a *App) showCommand() *cobra.Command { cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary") cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary") cmd.Flags().BoolVarP(&a.flags.ShowExport, "export", "e", false, "export timewarrior timetable") + cmd.Flags().StringVarP(&a.flags.ExportName, "name", "n", "Arbeitszeiten.xlsx", "name of exported excel table") return cmd } diff --git a/config.go b/config.go index b5be710..ef79ab0 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,7 @@ type Flags struct { ShowWeek bool ShowMonth bool ShowExport bool + ExportName string } func loadConfig() (Config, error) { diff --git a/timewarrior.go b/timewarrior.go index c87654d..044d197 100644 --- a/timewarrior.go +++ b/timewarrior.go @@ -56,7 +56,7 @@ type TimeEntry struct { Tag string } -func (t *TimeWarrior) ExportSummary() error { +func (t *TimeWarrior) ExportSummary(name string) error { fmt.Println("Export Timetable") exportCommand := exec.Command("timew", "summary", ":year") output, err := exportCommand.Output() @@ -122,7 +122,7 @@ func (t *TimeWarrior) ExportSummary() error { case "break": duration, _ := parseDuration(entry.Duration) summary.BreakDuration += duration - case "uni", "free", "krank", "urlaub": + case "uni", "free", "krank", "urlaub", "feiertag": summary.Tag = entry.Tag } } @@ -144,7 +144,7 @@ func (t *TimeWarrior) ExportSummary() error { fmt.Printf("%+v\n", entry) } - err = writeExcelSheet(excelEntries) + err = writeExcelSheet(excelEntries, name) if err != nil { return err } @@ -210,7 +210,7 @@ func formatDuration(d time.Duration) string { return fmt.Sprintf("%d:%02d:%02d", h, m, s) } -func writeExcelSheet(entries []ExcelEntry) error { +func writeExcelSheet(entries []ExcelEntry, name string) error { sort.Slice(entries, func(i, j int) bool { dateI, _ := time.Parse("2006-01-02", entries[i].Date) dateJ, _ := time.Parse("2006-01-02", entries[j].Date) @@ -230,7 +230,6 @@ func writeExcelSheet(entries []ExcelEntry) error { return err } - // Überschriften setzen f.SetCellValue(sheetName, "B1", "Arbeitszeiten "+sheetName) f.SetCellValue(sheetName, "B3", "Datum") f.SetCellValue(sheetName, "D3", "Arbeitszeit") @@ -303,6 +302,8 @@ func writeExcelSheet(entries []ExcelEntry) error { text = "Hochschule" case "urlaub": text = "Urlaub" + case "feiertag": + text = "Feiertag" default: text = "" } @@ -313,7 +314,7 @@ func writeExcelSheet(entries []ExcelEntry) error { } f.SetActiveSheet(index) - if err := f.SaveAs("Test.xlsx"); err != nil { + if err := f.SaveAs(name); err != nil { fmt.Println(err) } return nil