feat: initial project commit

commit to push first working snapshot to codeberg
This commit is contained in:
Patryk Hegenberg 2025-03-15 00:13:20 +01:00
commit 09b1054588
25 changed files with 1405 additions and 0 deletions

View file

@ -0,0 +1,315 @@
package dependency
import (
"fmt"
osinfo "jws/internal/os"
"jws/pkg/download"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"fyne.io/tools"
)
type Dependency struct {
Name string
Installed bool
Icon fyne.Resource
}
func CheckDependencies(dependencies []Dependency) {
// Check VSCode
dependencies[0].Installed = checkVSCode()
// Check Docker
if runtime.GOOS == "windows" {
dependencies[1].Installed = checkDockerDesktop()
} else {
dependencies[1].Installed = checkDocker()
}
}
func checkVSCode() bool {
switch runtime.GOOS {
case "windows":
_, err := os.Stat(filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Microsoft VS Code", "Code.exe"))
if err == nil {
return true
}
_, err = os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Microsoft VS Code", "Code.exe"))
return err == nil
case "darwin":
_, err := os.Stat("/Applications/Visual Studio Code.app")
if err != nil {
cmd := tools.CommandInShell("which", "code")
return cmd.Run() == nil
}
return err == nil
case "linux":
cmd := tools.CommandInShell("which", "code")
return cmd.Run() == nil
default:
return false
}
}
func checkDocker() bool {
cmd := tools.CommandInShell("which", "docker")
return cmd.Run() == nil
}
func checkDockerDesktop() bool {
switch runtime.GOOS {
case "windows":
_, err := os.Stat(filepath.Join(os.Getenv("ProgramFiles"), "Docker", "Docker", "Docker Desktop.exe"))
return err == nil
case "darwin":
_, err := os.Stat("/Applications/Docker.app")
return err == nil
case "linux":
cmd := tools.CommandInShell("systemctl", "is-active", "docker")
output, _ := cmd.Output()
return strings.TrimSpace(string(output)) == "active"
default:
return false
}
}
func InstallDependency(index int, sudoPassword string, dependencies []Dependency, mainWindow fyne.Window) {
depName := dependencies[index].Name
var cmd *exec.Cmd
var err error
switch runtime.GOOS {
case "windows":
switch index {
case 0: // VSCode
cmd = tools.CommandInShell("winget", "install", "-e", "--id",
"Microsoft.VisualStudioCode")
case 1: // Docker Desktop
wslCheckCmd := tools.CommandInShell("wsl", "--status")
err := wslCheckCmd.Run()
if err != nil {
wslInstallCmd := tools.CommandInShell("wsl", "--install")
err = wslInstallCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error: installing WSL: %v", err), mainWindow)
return
}
dialog.ShowInformation("WSL wird installiert", "WSL wird installiert. Bitte warten Sie, bis die Installation abgeschlossen ist und starten Sie die Anwendung neu.", mainWindow)
return
}
cmd = tools.CommandInShell("winget", "install", "-e", "--id",
"Docker.DockerDesktop")
}
case "darwin":
brewCheckCmd := tools.CommandInShell("which", "brew")
err := brewCheckCmd.Run()
if err != nil {
brewInstallCmd := tools.CommandInShell("bin/bash", "-c", "\"$(curl -fsSl https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
err := brewInstallCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error: installing homebrew falied: %v", err), mainWindow)
}
}
switch index {
case 0: // VSCode
cmd = tools.CommandInShell("brew", "install", "--cask", "visual-studio-code")
case 1: // Docker
cmd = tools.CommandInShell("brew", "install", "docker")
}
case "linux":
osInfo, err := osinfo.GetLinuxDistribution()
if err != nil {
log.Println(err)
dialog.ShowError(fmt.Errorf("error getting OS info: %v", err), mainWindow)
return
}
var downloadURL, fileName string
switch index {
case 0: // VSCode
switch osInfo.ID {
case "debian", "ubuntu", "linuxmint":
downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64"
fileName = "vscode.deb"
case "fedora":
downloadURL = "https://code.visualstudio.com/sha/download?build=stable&os=linux-rpm-x64"
fileName = "vscode.rpm"
default:
dialog.ShowInformation("Nicht unterstützt", fmt.Sprintf("Automatische Installation für dieses OS %v nicht verfügbar", osInfo), mainWindow)
return
}
go func() {
progressBar := widget.NewProgressBar()
progressDialog := dialog.NewCustomWithoutButtons("Download in progress", progressBar, mainWindow)
progressDialog.Show()
homeDir, _ := os.UserHomeDir()
downloadDir := filepath.Join(homeDir, "Downloads")
filePath := filepath.Join(downloadDir, fileName)
err := download.WithProgressBar(downloadURL, filePath, progressBar)
progressDialog.Hide()
if err != nil {
dialog.ShowError(err, mainWindow)
return
}
var installCmd string
switch osInfo.ID {
case "debian", "ubuntu", "linuxmint":
installCmd = fmt.Sprintf("sudo -S dpkg -i %s", filePath)
case "fedora":
installCmd = fmt.Sprintf("sudo -S dnf install -y %s", filePath)
}
cmd := exec.Command("sh", "-c", installCmd)
cmd.Stdin = strings.NewReader(sudoPassword + "\n")
dialog.ShowInformation("Installation gestartet",
"Die Installation von VSCode wurde gestartet.", mainWindow)
output, err := cmd.CombinedOutput()
if err != nil {
dialog.ShowError(fmt.Errorf("installation failed:\n%s", output), mainWindow)
return
} else {
dialog.ShowInformation("Erfolg", "VSCode erfolgreich installiert!", mainWindow)
CheckDependencies(dependencies)
}
}()
case 1: // Docker
go func() {
progressBar := widget.NewProgressBar()
progressDialog := dialog.NewCustomWithoutButtons("Docker Installation läuft...", progressBar, mainWindow)
progressDialog.Show()
osInfo, err := osinfo.GetLinuxDistribution()
if err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("error getting os infos: %v", err), mainWindow)
return
}
var commands []string
var cleanupCommands []string
var totalSteps int
switch osInfo.ID {
case "ubuntu", "linuxmint", "debian":
// Ubuntu/Debian Commands
distroPath := "ubuntu"
codeName := "$(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\")"
if osInfo.ID == "debian" {
distroPath = "debian"
codeName = "$(. /etc/os-release && echo \"$VERSION_CODENAME\")"
}
arch := "$(dpkg --print-architecture)"
commands = []string{
"apt-get update",
"apt-get install -y wget",
fmt.Sprintf("wget -qO- https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/ | grep -oP 'href=\"\\K[^\"]*(?=.*deb)' | xargs -I{} wget https://download.docker.com/linux/%s/dists/%s/pool/stable/%s/{}",
distroPath, codeName, arch, distroPath, codeName, arch),
"dpkg -i ./containerd.io*.deb docker-ce*.deb docker-ce-cli*.deb docker-buildx-plugin*.deb docker-compose-plugin*.deb",
"apt-get install -f -y",
"service docker start",
}
cleanupCommands = []string{
"rm -rf ./containerd.io*.deb ./docker-ce*.deb ./docker-ce-cli*.deb ./docker-buildx-plugin*.deb ./docker-compose-plugin*.deb",
"apt-get autoremove -y",
"apt-get clean",
}
totalSteps = len(commands) + len(cleanupCommands)
case "fedora":
// Fedora Commands
fedoraVer := "$(rpm -E %fedora)"
commands = []string{
"dnf install -y wget",
fmt.Sprintf("wget https://download.docker.com/linux/fedora/%s/x86_64/stable/Packages/containerd-*.rpm docker-*.rpm docker-ce-*.rpm", fedoraVer),
"dnf install -y ./*.rpm",
"systemctl enable --now docker",
}
cleanupCommands = []string{
"rm -rf ./containerd-*.rpm ./docker-*.rpm ./docker-ce-*.rpm",
"dnf autoremove -y",
"dnf clean all",
}
totalSteps = len(commands) + len(cleanupCommands)
default:
progressDialog.Hide()
dialog.ShowInformation("not supported", "Automatic Docker installation not supported for your OS.", mainWindow)
return
}
progressStep := 1.0 / float64(totalSteps)
currentProgress := 0.0
for _, cmd := range commands {
command := exec.Command("sudo", "-S", "sh", "-c", cmd)
command.Stdin = strings.NewReader(sudoPassword + "\n")
if output, err := command.CombinedOutput(); err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("error at %s:\n%s", cmd, output), mainWindow)
return
}
currentProgress += progressStep
progressBar.SetValue(currentProgress)
}
for _, cmd := range cleanupCommands {
command := exec.Command("sudo", "-S", "sh", "-c", cmd)
command.Stdin = strings.NewReader(sudoPassword + "\n")
if output, err := command.CombinedOutput(); err != nil {
progressDialog.Hide()
dialog.ShowError(fmt.Errorf("cleanup error at %s:\n%s", cmd, output), mainWindow)
return
}
currentProgress += progressStep
progressBar.SetValue(currentProgress)
}
progressDialog.Hide()
dialog.ShowInformation(
"Installation finished",
"Docker was succesfully installed! Please re-start your system to let the changes take effect.",
mainWindow,
)
CheckDependencies(dependencies)
}()
}
}
if cmd != nil {
err = cmd.Start()
if err != nil {
dialog.ShowError(fmt.Errorf("error starting installation process: %v", err), mainWindow)
} else {
dialog.ShowInformation("Installation started",
fmt.Sprintf("Installation of %s was started", depName), mainWindow)
}
}
}

View file

@ -0,0 +1,92 @@
package gui
import (
"jws/internal/dependency"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// ShowDependencyScreen displays the dependency check screen
func ShowDependencyScreen() {
title := widget.NewLabel("check dependencies")
title.TextStyle = fyne.TextStyle{Bold: true}
title.Alignment = fyne.TextAlignCenter
description := widget.NewLabel("dependencies needed for dev-environment:")
description.Wrapping = fyne.TextWrapWord
dependencyContainer := container.NewVBox()
allInstalled := true
for i, dep := range dependencies {
statusIcon := theme.CancelIcon()
statusText := "not installed"
if dep.Installed {
statusIcon = theme.ConfirmIcon()
statusText = "installed"
} else {
allInstalled = false
}
depBox := container.NewHBox(
widget.NewIcon(dep.Icon),
widget.NewLabel(dep.Name),
layout.NewSpacer(),
widget.NewIcon(statusIcon),
widget.NewLabel(statusText),
)
dependencyContainer.Add(depBox)
if !dep.Installed {
installBtn := widget.NewButton("install", func(i int) func() {
return func() {
ShowPasswordDialog(i)
}
}(i))
depBox = container.NewStack(
layout.NewSpacer(),
installBtn,
)
dependencyContainer.Add(depBox)
}
}
nextBtn := widget.NewButton("go to projects", func() {
if allInstalled {
ShowProjectScreen()
} else {
dialog.ShowInformation("dependencies missing",
"Bitte installieren Sie alle erforderlichen Abhängigkeiten, bevor Sie fortfahren.", mainWindow)
}
})
nextBtn.Importance = widget.HighImportance
recheckBtn := widget.NewButton("check again", func() {
dependency.CheckDependencies(dependencies)
ShowDependencyScreen()
})
buttonContainer := container.New(layout.NewGridLayout(2), recheckBtn, nextBtn)
paddedButtonContainer := container.NewPadded(buttonContainer)
description.Alignment = fyne.TextAlignCenter
header := container.NewVBox(title, description)
content := container.NewBorder(header,
paddedButtonContainer,
nil,
nil,
dependencyContainer,
)
mainWindow.SetContent(content)
}

47
internal/gui/gui.go Normal file
View file

@ -0,0 +1,47 @@
package gui
import (
"embed"
"jws/internal/dependency"
"jws/internal/project"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
var (
// Global variables available to the package
mainWindow fyne.Window
dependencies []dependency.Dependency
projects []project.Project
projectsFS embed.FS
)
// Init initializes the GUI package with required dependencies
func Init(window fyne.Window, deps []dependency.Dependency, projs []project.Project, fs embed.FS) {
mainWindow = window
dependencies = deps
projects = projs
projectsFS = fs
}
// ShowPasswordDialog shows a dialog to enter sudo password for installations
func ShowPasswordDialog(index int) {
password := binding.NewString()
entry := widget.NewEntryWithData(password)
entry.Password = true
dialog.NewForm("Sudo Password", "ok", "cancel",
[]*widget.FormItem{widget.NewFormItem("password", entry)},
func(b bool) {
pass, err := password.Get()
if err == nil {
if b {
dependency.InstallDependency(index, pass, dependencies, mainWindow)
}
}
},
mainWindow).Show()
}

View file

@ -0,0 +1,91 @@
package gui
import (
"jws/internal/project"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// ShowProjectScreen displays the project selection screen
func ShowProjectScreen() {
title := widget.NewLabel("Project-Selection")
title.TextStyle = fyne.TextStyle{Bold: true}
title.Alignment = fyne.TextAlignCenter
description := widget.NewLabel("Choose a Starter-Project to deploy:")
description.Wrapping = fyne.TextWrapWord
description.Alignment = fyne.TextAlignCenter
projectsList := container.NewVBox()
for i, proj := range projects {
projTitle := widget.NewLabel(proj.Name)
projTitle.TextStyle = fyne.TextStyle{Bold: true}
projDesc := widget.NewLabel(proj.Description)
projDesc.Wrapping = fyne.TextWrapWord
deployBtn := widget.NewButton("deploy", func(i int) func() {
return func() {
project.DeployProject(i, projects, projectsFS, mainWindow)
}
}(i))
deployBtn.Importance = widget.HighImportance
projectContent := container.NewBorder(
nil,
nil,
nil,
container.NewCenter(deployBtn),
container.NewVBox(projTitle, projDesc),
)
projectCard := container.NewBorder(
nil,
nil,
nil,
nil,
container.NewPadded(projectContent),
)
bg := canvas.NewRectangle(theme.Color(theme.ColorNameBackground))
borderedContainer := container.NewStack(
bg,
projectCard,
)
spacedContainer := container.NewVBox(
borderedContainer,
widget.NewSeparator(),
)
projectsList.Add(spacedContainer)
}
backBtn := widget.NewButton("Check Dependecies", func() {
ShowDependencyScreen()
})
scrollContainer := container.NewVScroll(projectsList)
header := container.NewVBox(
title,
description,
widget.NewSeparator(),
)
content := container.NewBorder(
header,
backBtn, // footer,
nil,
nil,
scrollContainer,
)
mainWindow.SetContent(content)
}

83
internal/os/osinfo.go Normal file
View file

@ -0,0 +1,83 @@
package os
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
type OS struct {
ID string
Name string
Version string
PackageManager string
}
func GetLinuxDistribution() (*OS, error) {
_, err := os.Stat("/etc/os-release")
if os.IsNotExist(err) {
return nil, fmt.Errorf("unable to read system information")
}
osRelease, _ := os.ReadFile("/etc/os-release")
return parseOsRelease(string(osRelease)), nil
}
func parseOsRelease(osRelease string) *OS {
var result OS
result.ID = "Unknown"
result.Name = "Unknown"
result.Version = "Unknown"
lines := strings.Split(osRelease, "\n")
for _, line := range lines {
splitLine := strings.SplitN(line, "=", 2)
if len(splitLine) != 2 {
continue
}
switch splitLine[0] {
case "ID":
result.ID = strings.ToLower(strings.Trim(splitLine[1], "\""))
case "NAME":
result.Name = strings.Trim(splitLine[1], "\"")
case "VERSION_ID":
result.Version = strings.Trim(splitLine[1], "\"")
}
}
err := result.getPackageManager()
if err != nil {
log.Fatal(err)
}
return &result
}
func (os *OS) getPackageManager() error {
switch os.ID {
case "debian", "ubuntu", "linuxmint":
os.PackageManager = "apt"
return nil
case "arch":
os.PackageManager = "pacman"
return nil
case "fedora":
os.PackageManager = "dnf"
return nil
default:
pmcommands := []string{
"apt",
"dnf",
"pacman",
}
for _, pmname := range pmcommands {
_, err := exec.LookPath(pmname)
if err == nil {
os.PackageManager = pmname
return nil
}
}
return fmt.Errorf("no packagemanager found for os: %s", os.Name)
}
}

127
internal/project/project.go Normal file
View file

@ -0,0 +1,127 @@
package project
import (
"embed"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
type Project struct {
Name string `json:"name"`
Description string `json:"description"`
FolderName string `json:"folderName"`
}
func DeployProject(index int, projects []Project, projectsFS embed.FS, mainWindow fyne.Window) {
project := projects[index]
homeDir, err := os.UserHomeDir()
if err != nil {
dialog.ShowError(fmt.Errorf("error getting home directory: %v", err), mainWindow)
return
}
projectPath := filepath.Join(homeDir, "Projects", project.FolderName)
confirmDialog := dialog.NewConfirm(
"deploying project",
fmt.Sprintf("the project '%s' will be deployed to '%s' and opend in VS Code. Continue?", project.Name, projectPath),
func(ok bool) {
if ok {
progress := widget.NewProgressBar()
progressDiag := dialog.NewCustomWithoutButtons("project will be deployed", progress, mainWindow)
progressDiag.Show()
go func() {
defer progressDiag.Hide()
progress.SetValue(0.1)
err := os.MkdirAll(projectPath, 0755)
if err != nil {
dialog.ShowError(fmt.Errorf("error creating directory: %v", err), mainWindow)
return
}
progress.SetValue(0.3)
err = CopyEmbeddedProject(projectsFS, project.FolderName, projectPath)
if err != nil {
dialog.ShowError(fmt.Errorf("error copying project files: %v", err), mainWindow)
return
}
progress.SetValue(0.7)
var openCmd *exec.Cmd
switch runtime.GOOS {
case "windows":
openCmd = exec.Command("code", projectPath)
case "darwin":
openCmd = exec.Command("open", "-a", "Visual Studio Code", projectPath)
case "linux":
openCmd = exec.Command("code", projectPath)
}
if openCmd != nil {
progress.SetValue(0.9)
err = openCmd.Run()
if err != nil {
dialog.ShowError(fmt.Errorf("error opening VS Code: %v", err), mainWindow)
return
}
}
progress.SetValue(1.0)
dialog.ShowInformation("project deployed",
fmt.Sprintf("the project '%s' was successfully deployed and opend in VS Code.\n\n"+
"Path: %s\n\n"+
"Application can be started with Docker Compose.",
project.Name, projectPath), mainWindow)
}()
}
},
mainWindow,
)
confirmDialog.Show()
}
func CopyEmbeddedProject(projectsFS embed.FS, projectFolder string, targetPath string) error {
sourcePath := fmt.Sprintf("projects/%s", projectFolder)
return fs.WalkDir(projectsFS, sourcePath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(sourcePath, path)
if err != nil {
return err
}
destPath := filepath.Join(targetPath, relPath)
if d.IsDir() {
return os.MkdirAll(destPath, 0755)
}
data, err := projectsFS.ReadFile(path)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return err
}
return os.WriteFile(destPath, data, 0644)
})
}