Merge branch 'dev/refactor-from-functioning-state'

* dev/refactor-from-functioning-state:
  feat: add filename to exported exceltable
  feat: add basic excel export
  refactor: seperate application into seperate files and add killForwarding
  refactor: introduce TimeWarrior struct
  refactor: introduce app struct
  feat: add cli commands for start, stop and show
  refactor: change FunctionNames to be more generic
  refactor(config): change config location and keyword names
This commit is contained in:
Patryk Hegenberg 2025-01-03 18:13:47 +01:00
commit b083a8255c
9 changed files with 818 additions and 275 deletions

230
app.go Normal file
View file

@ -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(a.flags.ExportName)
}
}
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
}

76
cmd.go Normal file
View file

@ -0,0 +1,76 @@
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(a.flags.ExportName)
}
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")
cmd.Flags().StringVarP(&a.flags.ExportName, "name", "n", "Arbeitszeiten.xlsx", "name of exported excel table")
return cmd
}

55
config.go Normal file
View file

@ -0,0 +1,55 @@
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
ExportName string
}
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
}

58
forwarder.go Normal file
View file

@ -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)
}

19
go.mod
View file

@ -4,11 +4,10 @@ go 1.23.0
require ( require (
github.com/charmbracelet/huh v0.5.3 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 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 ( require (
@ -24,7 +23,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.7.0 // 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/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.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/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // 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/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.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/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/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/atomic v1.9.0 // indirect
go.uber.org/multierr 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/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/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.17.0 // indirect golang.org/x/text v0.19.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

59
go.sum
View file

@ -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/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 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= 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 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0=
github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 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= 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/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/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.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.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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 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= 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/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 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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.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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/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 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 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/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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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 h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.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.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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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 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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

257
main.go
View file

@ -1,261 +1,20 @@
package main package main
import ( import (
"fmt" "log"
"io"
"log/slog"
"net"
"os" "os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/spf13/viper"
"golang.org/x/crypto/ssh"
) )
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"`
}
var cfg Config
func init() {
configPath, err := os.UserConfigDir()
if err != nil {
slog.Error("Unable to find config path", "error", err)
return
}
workConfigPath := filepath.Join(configPath, "work")
configFile := filepath.Join(workConfigPath, "config.toml")
slog.Info("Looking for config file", "path", configFile)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
slog.Error("Config file does not exist", "path", configFile)
return
}
viper.SetConfigFile(configFile)
viper.SetConfigType("toml")
err = viper.ReadInConfig()
if err != nil {
slog.Error("Error loading config file", "error", err)
return
}
slog.Info("Config file successfully loaded")
allSettings := viper.AllSettings()
slog.Debug("Loaded settings", "config", allSettings)
err = viper.UnmarshalKey("default", &cfg)
if err != nil {
slog.Error("Unable to decode into struct", "error", err)
}
}
func main() { func main() {
var choice string app, err := NewApp()
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 { if err != nil {
slog.Error("Cannot run form", "error", err) log.Fatalf("unable to setup application: %v", err)
return
} }
if len(os.Args) > 1 {
switch choice { if err := app.setupCommands().Execute(); err != nil {
case "Start": log.Fatalf("error executing command: %v", err)
connect()
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()
sshClient, err := ssh.Dial("tcp", cfg.SSHHost+":"+fmt.Sprintf("%v", cfg.SSHPort), makeSSHClient())
if err != nil {
slog.Error("Failed to dial", "error", err)
return
}
defer sshClient.Close()
session, err := sshClient.NewSession()
if err != nil {
slog.Error("Failed to create session", "error", err)
}
defer session.Close()
go forwardPort(sshClient, "2048", cfg.LouIP, "22")
go forwardPort(sshClient, "6000", cfg.LouIP, "3389")
connectToLou()
}
func forwardPort(sshConn *ssh.Client, localPort, remoteHost, remotePort string) {
listener, err := net.Listen("tcp", "127.0.0.1:"+localPort)
if err != nil {
slog.Error("Error opening local port", "port", localPort, "error", err)
return
}
defer listener.Close()
for {
localConn, err := listener.Accept()
if err != nil {
slog.Error("Error accepting connection", "error", err)
continue
} }
} else {
remoteConn, err := sshConn.Dial("tcp", remoteHost+":"+remotePort) app.makeChoice()
if err != nil {
slog.Error("Error connecting to remote host", "host", remoteHost, "port", remotePort, "error", 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 makeSSHClient() *ssh.ClientConfig {
keypath := os.ExpandEnv("$HOME/.ssh/hegenberg")
keyBytes, err := os.ReadFile(keypath)
if err != nil {
slog.Error("Failed to read private key", "error", err)
os.Exit(1)
}
key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(cfg.RDPPassword))
if err != nil {
slog.Error("Failed to parse private key", "error", err)
os.Exit(1)
}
return &ssh.ClientConfig{
User: cfg.SSHUser,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
}
func 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 {
slog.Error("Command execution error", "command", name, "args", args, "error", err)
}
}
func startSequence() {
runCommand("timew", "start", "work")
wakeLou()
connectToVarda()
}
func wakeLou() {
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.VardaUser,
cfg.VardaHost,
cfg.LouMac)
args := strings.Split(sshCommand, " ")
slog.Info("Executing wake command", "args", args)
runCommand("ssh", args[1:]...)
}
func connectToVarda() {
sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s",
cfg.LouHost,
cfg.SSHUser,
cfg.SSHHost)
args := strings.Split(sshCommand, " ")
runCommand("ssh", args[1:]...)
}
func connectToLou() {
sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1",
cfg.LouHost,
cfg.SSHUser)
args := strings.Split(sshCommand, " ")
runCommand("ssh", args[1:]...)
}
func 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)
}

18
ssh.go Normal file
View file

@ -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()
}
}

321
timewarrior.go Normal file
View file

@ -0,0 +1,321 @@
package main
import (
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"time"
"github.com/xuri/excelize/v2"
)
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")
}
type TimeEntry struct {
Week string
Date string
Day string
Start string
End string
Duration string
Tag string
}
func (t *TimeWarrior) ExportSummary(name string) 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", "feiertag":
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, name)
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, 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)
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
}
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"
case "feiertag":
text = "Feiertag"
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(name); err != nil {
fmt.Println(err)
}
return nil
}