Compare commits

...

2 Commits

Author SHA1 Message Date
Preston Baxter d97cf32d72 add make file. Move exe 2024-02-10 22:10:29 -06:00
Preston Baxter a42de52c60 add tui
add tui
2024-02-10 18:42:39 -06:00
6 changed files with 328 additions and 144 deletions

6
Makefile Normal file
View File

@ -0,0 +1,6 @@
build:
GOOS=windows GOARCH=amd64 go build -o hln.exe
dev:
GOOS=windows GOARCH=amd64 go build -o hln-dev.exe

20
go.mod
View File

@ -1,3 +1,23 @@
module hnl
go 1.21.5
require github.com/charmbracelet/bubbletea v0.25.0
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

36
go.sum Normal file
View File

@ -0,0 +1,36 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
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/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=

Binary file not shown.

269
main.go
View File

@ -13,82 +13,68 @@ import (
"strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
)
const (
battleNetInstaller = "https://downloader.battle.net/download/getInstallerForGame?os=win&gameProgram=BATTLENET_APP&version=Live"
hdtInstaller = "https://github.com/HearthSim/Hearthstone-Deck-Tracker/releases/download/v1.23.15/Hearthstone.Deck.Tracker-v1.23.15.zip"
hdtInstaller = "https://github.com/HearthSim/Hearthstone-Deck-Tracker/releases/download/v1.23.15/Hearthstone.Deck.Tracker-v1.23.15.zip"
programDir = "C:\\Program Files (x86)"
)
func main(){
//check app data
init, err := checkInit()
if err != nil {
fmt.Printf("Error: %s", err)
time.Sleep(15 * time.Second)
var (
hdtBin = filepath.Join("C:", "Program Files (x86)", "hln", "Hearthstone Deck Tracker", "Hearthstone Deck Tracker.exe")
battleNetBin = filepath.Join("C:", "Program Files (x86)", "Battle.net", "Battle.net.exe")
)
func main() {
p := tea.NewProgram(initHlnModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Failed to run program. Error Below:\n\n%v\n", err)
time.Sleep(10 * time.Second)
os.Exit(1)
}
//If not installed - installed
if !init {
fmt.Printf("Games not isntalled. Installing...\n")
err := installBins()
if err != nil {
fmt.Printf("Error: %s", err)
time.Sleep(15 * time.Second)
os.Exit(1)
}
} else {
err := launchBins()
if err != nil {
fmt.Printf("Error: %s", err)
time.Sleep(15 * time.Second)
os.Exit(1)
}
}
//if installed. Launch battlenet and hdt
}
//Check if installed file is in the user config directory
func checkInit() (bool, error) {
// Check if installed file is in the user config directory
func checkInstalled() tea.Msg {
configDir, err := os.UserConfigDir()
if err != nil {
return false, err
return errMsg{err: err}
}
path := fmt.Sprintf("%s%c%s%cinstalled", configDir, os.PathSeparator, "HearthstoneNerdLinux", os.PathSeparator)
path := filepath.Join(configDir, "HearthstoneLinuxNerd", "installed")
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist){
return false, nil
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return NOT_INSTALLED
} else if err != nil {
return false, err
return errMsg{err: err}
}
return true, nil
return INSTALLED
}
//download and loaunch battlenet installer and install HDT
func installBins() error {
// download and loaunch battlenet installer and install HDT
func installBins() tea.Msg {
wg := new(sync.WaitGroup)
wg.Add(2)
errs := make([]error, 2)
cacheDir, err := os.UserCacheDir()
if err != nil {
return err
return errMsg{err: err}
}
configDir, err := os.UserConfigDir()
if err != nil {
return err
return errMsg{err: err}
}
go func(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Installing batlle.net\n")
file := fmt.Sprintf("%s%cbattlenet.exe", cacheDir, os.PathSeparator)
// fmt.Printf("Installing batlle.net\n")
file := filepath.Join(cacheDir, "battlenet.exe")
errs[0] = downloadBin(battleNetInstaller, file)
if errs[0] != nil {
return
@ -100,15 +86,15 @@ func installBins() error {
go func(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Installing hdt\n")
file := fmt.Sprintf("%s%chdt.zip", cacheDir, os.PathSeparator)
// fmt.Printf("Installing hdt\n")
file := filepath.Join(cacheDir, "hdt.zip")
errs[1] = downloadBin(hdtInstaller, file)
if errs[1] != nil {
return
}
programDir := "C:\\Program Files (x86)\\hnl"
errs[1] = unzip(file, programDir)
dir := filepath.Join(programDir, "hln")
errs[1] = unzip(file, dir)
if errs[1] != nil {
return
}
@ -117,148 +103,143 @@ func installBins() error {
wg.Wait()
if err := errors.Join(errs...); err != nil {
return err
return errMsg{err: err}
}
err = os.MkdirAll(filepath.Join(configDir, "HearthstoneLinuxNerd"), 0755)
if err != nil {
return err
return errMsg{err: err}
}
_, err = os.Create(filepath.Join(configDir, "HearthstoneLinuxNerd", "installed"))
if err != nil {
return err
return errMsg{err: err}
}
return nil
return INSTALLED
}
func launchBins() error {
battleNetBin := filepath.Join("C:", "Program Files (x86)", "Battle.net", "Battle.net.exe")
hdtBin := filepath.Join("C:", "Program Files (x86)", "hnl", "Hearthstone Deck Tracker", "Hearthstone Deck Tracker.exe")
wg := new(sync.WaitGroup)
wg.Add(2)
errs := make([]error, 2)
go func(wg *sync.WaitGroup) {
defer wg.Done()
func (m *HlnModel) launchBattleNet() tea.Cmd {
return func() tea.Msg {
cmd := exec.Command(battleNetBin)
errs[0] = cmd.Run()
}(wg)
err := cmd.Run()
if err != nil {
return ApplicationEvent{app: BATTLE_NET_ID, state: STATE_STOPPED, err: err}
}
go func(wg *sync.WaitGroup) {
defer wg.Done()
return ApplicationEvent{app: BATTLE_NET_ID, state: STATE_STOPPED, err: nil}
}
}
func (m *HlnModel) launchHdt() tea.Cmd {
return func() tea.Msg {
cmd := exec.Command(hdtBin)
errs[1] = cmd.Run()
}(wg)
err := cmd.Run()
if err != nil {
return ApplicationEvent{app: HDT_ID, state: STATE_STOPPED, err: err}
}
wg.Wait()
return ApplicationEvent{app: HDT_ID, state: STATE_STOPPED, err: nil}
}
}
if err := errors.Join(errs...); err != nil {
func downloadBin(uri, filepath string) error { // Build fileName from fullPath
fileURL, err := url.Parse(uri)
if err != nil {
return err
}
return nil
}
func downloadBin(uri, filepath string) error {// Build fileName from fullPath
fileURL, err := url.Parse(uri)
if err != nil {
return err
}
// Create blank file
// Create blank file
file, err := os.Create(filepath)
if err != nil {
if err != nil {
return err
}
client := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
// Put content on file
resp, err := client.Get(fileURL.String())
if err != nil {
}
client := http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
r.URL.Opaque = r.URL.Path
return nil
},
}
// Put content on file
resp, err := client.Get(fileURL.String())
if err != nil {
return err
}
defer resp.Body.Close()
}
defer resp.Body.Close()
_, err = io.Copy(file, resp.Body)
_, err = io.Copy(file, resp.Body)
defer file.Close()
defer file.Close()
return err
}
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
fmt.Printf("stuff...\n")
time.Sleep(10)
panic(err)
}
}()
panic(err)
}
}()
os.MkdirAll(dest, 0755)
os.MkdirAll(dest, 0755)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
fmt.Printf("stuff...\n")
time.Sleep(10)
panic(err)
}
}()
}
}()
path := filepath.Join(dest, f.Name)
path := filepath.Join(dest, f.Name)
// Check for ZipSlip (Directory traversal)
if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
// Check for ZipSlip (Directory traversal)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("stuff...\n")
time.Sleep(10)
panic(err)
}
}()
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
return nil
}

141
tui.go Normal file
View File

@ -0,0 +1,141 @@
package main
import (
"errors"
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
type (
InstallState int
appState int
appId int
ApplicationEvent struct {
app appId
state appState
err error
}
errMsg struct{ err error }
)
const (
INSTALLED InstallState = 0
NOT_INSTALLED InstallState = 1
UNKOWN_INSTALL_STATE InstallState = 2
BATTLE_NET_ID appId = 1
HDT_ID appId = 2
STATE_RUNNING appState = 0
STATE_STOPPED appState = 1
)
func (m errMsg) Error() string {
return m.err.Error()
}
type HlnModel struct {
battlenetRunning bool
hdtRunning bool
installed InstallState
err error
}
func initHlnModel() *HlnModel {
return &HlnModel{
battlenetRunning: false,
hdtRunning: false,
installed: UNKOWN_INSTALL_STATE,
err: nil,
}
}
func (m *HlnModel) Init() tea.Cmd {
return checkInstalled
}
func (m *HlnModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
//Check install state
case InstallState:
switch msg {
case NOT_INSTALLED:
m.installed = NOT_INSTALLED
return m, installBins
case INSTALLED:
m.battlenetRunning = true
m.hdtRunning = true
m.installed = INSTALLED
return m, tea.Batch(
m.launchHdt(),
m.launchBattleNet(),
)
}
//Hanlde appliaction events
case ApplicationEvent:
//Check app error
if msg.err != nil {
m.err = errors.Join(msg.err, fmt.Errorf("Failed to do something on app_id: %d", msg.app))
return m, tea.Quit
}
//Set state of app
switch msg.app {
case BATTLE_NET_ID:
m.battlenetRunning = false
return m, nil
case HDT_ID:
m.hdtRunning = false
return m, nil
}
//Check error mssg
case errMsg:
m.err = msg
return m, tea.Quit
//Check key press
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
}
}
return m, nil
}
func (m *HlnModel) View() string {
if m.err != nil {
return fmt.Sprintf("Error occured: %v\n", m.err)
}
s := "----- Welcome to Hearthston Linux Nerd Launcher -----\n\n"
switch m.installed {
case INSTALLED:
s += fmt.Sprintf("\tInstalled: Yes\n")
case NOT_INSTALLED:
s += fmt.Sprintf("\tInstalled: No. Installing...\n")
case UNKOWN_INSTALL_STATE:
s += fmt.Sprintf("\tInstalled: checking\n")
}
s += "\n"
//Battle net display
s += fmt.Sprintf("\tBattlenet Running: %t\n", m.battlenetRunning)
//hdt display
s += fmt.Sprintf("\tHdt Running: %t\n", m.hdtRunning)
s += "\n"
s += "-----------------------------------------------------\n"
return s
}