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:
commit
b083a8255c
9 changed files with 818 additions and 275 deletions
230
app.go
Normal file
230
app.go
Normal 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
76
cmd.go
Normal 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
55
config.go
Normal 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
58
forwarder.go
Normal 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
19
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
|
||||
)
|
||||
|
|
|
|||
59
go.sum
59
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=
|
||||
|
|
|
|||
257
main.go
257
main.go
|
|
@ -1,261 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"log"
|
||||
"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() {
|
||||
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()
|
||||
app, err := NewApp()
|
||||
if err != nil {
|
||||
slog.Error("Cannot run form", "error", err)
|
||||
return
|
||||
log.Fatalf("unable to setup application: %v", err)
|
||||
}
|
||||
|
||||
switch choice {
|
||||
case "Start":
|
||||
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
|
||||
if len(os.Args) > 1 {
|
||||
if err := app.setupCommands().Execute(); err != nil {
|
||||
log.Fatalf("error executing command: %v", err)
|
||||
}
|
||||
|
||||
remoteConn, err := sshConn.Dial("tcp", remoteHost+":"+remotePort)
|
||||
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)
|
||||
} else {
|
||||
app.makeChoice()
|
||||
}
|
||||
}
|
||||
|
||||
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
18
ssh.go
Normal 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
321
timewarrior.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue