f4a88f4b2c
golangci-lint / lint (push) Failing after 4s
- Complete GUI overhaul with Fyne framework * Modern dark theme with indigo/pink accent colors * 4-step setup wizard (Welcome → Server → Auth → Finish) * Dashboard with proxy list, real-time logs, and config viewer * Windows Service manager (install/start/stop/uninstall) * System tray integration with quick controls - WiX v7 MSI installer * Auto-registers as Windows Service (manual start) * Feature tree UI for optional components * Desktop shortcut and PATH options * Build script: packaging/windows/build-msi.ps1 - Build system updates * Added kanholec-windows-gui target (CGO required) * Added kanholec-windows-msi target * Separate main_gui.go entry point for GUI builds GUI binary: bin/kanholec-windows-amd64.exe (31.4 MB) MSI installer: bin/kanholec-0.69.0-amd64.msi (12.1 MB)
411 lines
9.6 KiB
Go
411 lines
9.6 KiB
Go
//go:build kanholec_gui
|
|
|
|
package gui
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type wizardStep int
|
|
|
|
const (
|
|
stepWelcome wizardStep = iota
|
|
stepServer
|
|
stepAuth
|
|
stepFinish
|
|
)
|
|
|
|
type Wizard struct {
|
|
app *App
|
|
currentStep wizardStep
|
|
container *fyne.Container
|
|
stepIndicator *fyne.Container
|
|
|
|
serverAddr *widget.Entry
|
|
serverPort *widget.Entry
|
|
authMethod *widget.Select
|
|
authToken *widget.Entry
|
|
authUser *widget.Entry
|
|
authPass *widget.Entry
|
|
clientName *widget.Entry
|
|
|
|
onComplete func(cfg WizardResult)
|
|
}
|
|
|
|
type WizardResult struct {
|
|
ServerAddr string
|
|
ServerPort string
|
|
AuthMethod string
|
|
Token string
|
|
Username string
|
|
Password string
|
|
ClientName string
|
|
}
|
|
|
|
func NewWizard(app *App, onComplete func(WizardResult)) *Wizard {
|
|
w := &Wizard{
|
|
app: app,
|
|
onComplete: onComplete,
|
|
}
|
|
w.buildUI()
|
|
return w
|
|
}
|
|
|
|
func (w *Wizard) buildUI() {
|
|
w.serverAddr = widget.NewEntry()
|
|
w.serverAddr.PlaceHolder = "127.0.0.1"
|
|
w.serverAddr.SetText("127.0.0.1")
|
|
|
|
w.serverPort = widget.NewEntry()
|
|
w.serverPort.PlaceHolder = "7000"
|
|
w.serverPort.SetText("7000")
|
|
|
|
w.authMethod = widget.NewSelect([]string{"Token", "Username/Password"}, nil)
|
|
w.authMethod.SetSelected("Token")
|
|
|
|
w.authToken = widget.NewEntry()
|
|
w.authToken.PlaceHolder = "One-time provisioning token"
|
|
|
|
w.authUser = widget.NewEntry()
|
|
w.authUser.PlaceHolder = "admin"
|
|
|
|
w.authPass = widget.NewPasswordEntry()
|
|
w.authPass.PlaceHolder = "Password"
|
|
|
|
w.clientName = widget.NewEntry()
|
|
w.clientName.PlaceHolder = "my-client"
|
|
|
|
w.authMethod.OnChanged = func(s string) {
|
|
if s == "Token" {
|
|
w.authToken.Show()
|
|
w.authUser.Hide()
|
|
w.authPass.Hide()
|
|
w.clientName.Hide()
|
|
} else {
|
|
w.authToken.Hide()
|
|
w.authUser.Show()
|
|
w.authPass.Show()
|
|
w.clientName.Show()
|
|
}
|
|
}
|
|
|
|
w.container = container.NewStack()
|
|
w.buildStepIndicator()
|
|
w.showStep(stepWelcome)
|
|
}
|
|
|
|
func (w *Wizard) buildStepIndicator() {
|
|
labels := []string{"Welcome", "Server", "Auth", "Finish"}
|
|
items := []fyne.CanvasObject{}
|
|
|
|
for i, label := range labels {
|
|
isActive := i == int(w.currentStep)
|
|
isCompleted := i < int(w.currentStep)
|
|
|
|
var dotColor color.Color
|
|
if isActive {
|
|
dotColor = colorPrimary
|
|
} else if isCompleted {
|
|
dotColor = colorSuccess
|
|
} else {
|
|
dotColor = color.NRGBA{R: 75, G: 85, B: 99, A: 255}
|
|
}
|
|
|
|
dot := canvas.NewCircle(dotColor)
|
|
dot.Resize(fyne.NewSize(16, 16))
|
|
|
|
lbl := widget.NewLabel(label)
|
|
lbl.Alignment = fyne.TextAlignCenter
|
|
if isActive {
|
|
lbl.TextStyle = fyne.TextStyle{Bold: true}
|
|
lbl.Importance = widget.HighImportance
|
|
} else if isCompleted {
|
|
lbl.Importance = widget.MediumImportance
|
|
} else {
|
|
lbl.Importance = widget.LowImportance
|
|
}
|
|
|
|
stepItem := container.NewVBox(
|
|
container.NewHBox(layout.NewSpacer(), dot, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), lbl, layout.NewSpacer()),
|
|
)
|
|
items = append(items, stepItem)
|
|
|
|
if i < len(labels)-1 {
|
|
lineColor := color.NRGBA{R: 75, G: 85, B: 99, A: 255}
|
|
if isCompleted {
|
|
lineColor = colorSuccess
|
|
}
|
|
line := canvas.NewRectangle(lineColor)
|
|
line.SetMinSize(fyne.NewSize(60, 3))
|
|
items = append(items, container.NewVBox(
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), line, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
))
|
|
}
|
|
}
|
|
|
|
w.stepIndicator = container.NewHBox(items...)
|
|
}
|
|
|
|
func (w *Wizard) refreshStepIndicator() {
|
|
w.buildStepIndicator()
|
|
}
|
|
|
|
func (w *Wizard) showStep(step wizardStep) {
|
|
w.currentStep = step
|
|
w.refreshStepIndicator()
|
|
|
|
var content fyne.CanvasObject
|
|
switch step {
|
|
case stepWelcome:
|
|
content = w.welcomePage()
|
|
case stepServer:
|
|
content = w.serverPage()
|
|
case stepAuth:
|
|
content = w.authPage()
|
|
case stepFinish:
|
|
content = w.finishPage()
|
|
}
|
|
|
|
inner := container.NewBorder(
|
|
container.NewVBox(
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), w.stepIndicator, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
widget.NewSeparator(),
|
|
widget.NewLabel(""),
|
|
),
|
|
widget.NewLabel(""),
|
|
nil, nil,
|
|
container.NewVScroll(content),
|
|
)
|
|
|
|
w.container.Objects = []fyne.CanvasObject{inner}
|
|
w.container.Refresh()
|
|
}
|
|
|
|
func (w *Wizard) welcomePage() fyne.CanvasObject {
|
|
title := canvas.NewText("Welcome to kanholec", colorForeground)
|
|
title.TextSize = 32
|
|
title.TextStyle = fyne.TextStyle{Bold: true}
|
|
title.Alignment = fyne.TextAlignCenter
|
|
|
|
subtitle := canvas.NewText("Set up your reverse proxy client", colorPlaceholder)
|
|
subtitle.TextSize = 16
|
|
subtitle.Alignment = fyne.TextAlignCenter
|
|
|
|
desc := widget.NewRichTextFromMarkdown(`kanholec connects to a kanhole server to create secure tunnels.
|
|
|
|
**You will need:**
|
|
- A kanhole server address and port
|
|
- An authentication token or admin credentials
|
|
|
|
Click **Next** to get started.`)
|
|
|
|
nextBtn := widget.NewButton("Next", func() {
|
|
w.showStep(stepServer)
|
|
})
|
|
nextBtn.Importance = widget.HighImportance
|
|
|
|
return container.NewVBox(
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), subtitle, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
widget.NewSeparator(),
|
|
widget.NewLabel(""),
|
|
desc,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
layout.NewSpacer(),
|
|
container.NewHBox(layout.NewSpacer(), nextBtn),
|
|
)
|
|
}
|
|
|
|
func (w *Wizard) serverPage() fyne.CanvasObject {
|
|
title := canvas.NewText("Server Connection", colorForeground)
|
|
title.TextSize = 26
|
|
title.TextStyle = fyne.TextStyle{Bold: true}
|
|
|
|
desc := canvas.NewText("Enter your kanhole server details", colorPlaceholder)
|
|
desc.TextSize = 15
|
|
|
|
form := widget.NewForm(
|
|
widget.NewFormItem("Server Address", w.serverAddr),
|
|
widget.NewFormItem("Server Port", w.serverPort),
|
|
)
|
|
|
|
backBtn := widget.NewButton("Back", func() {
|
|
w.showStep(stepWelcome)
|
|
})
|
|
nextBtn := widget.NewButton("Next", func() {
|
|
if w.serverAddr.Text == "" {
|
|
w.app.showError("Server address is required")
|
|
return
|
|
}
|
|
w.showStep(stepAuth)
|
|
})
|
|
nextBtn.Importance = widget.HighImportance
|
|
|
|
return container.NewVBox(
|
|
title,
|
|
widget.NewLabel(""),
|
|
desc,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
widget.NewSeparator(),
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
form,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
layout.NewSpacer(),
|
|
container.NewHBox(backBtn, layout.NewSpacer(), nextBtn),
|
|
)
|
|
}
|
|
|
|
func (w *Wizard) authPage() fyne.CanvasObject {
|
|
title := canvas.NewText("Authentication", colorForeground)
|
|
title.TextSize = 26
|
|
title.TextStyle = fyne.TextStyle{Bold: true}
|
|
|
|
desc := canvas.NewText("Choose how to authenticate", colorPlaceholder)
|
|
desc.TextSize = 15
|
|
|
|
methodForm := widget.NewForm(
|
|
widget.NewFormItem("Method", w.authMethod),
|
|
)
|
|
|
|
tokenForm := widget.NewForm(
|
|
widget.NewFormItem("Token", w.authToken),
|
|
)
|
|
|
|
credForm := widget.NewForm(
|
|
widget.NewFormItem("Username", w.authUser),
|
|
widget.NewFormItem("Password", w.authPass),
|
|
widget.NewFormItem("Client Name", w.clientName),
|
|
)
|
|
|
|
authContent := container.NewStack(tokenForm, credForm)
|
|
w.authMethod.OnChanged(w.authMethod.Selected)
|
|
|
|
backBtn := widget.NewButton("Back", func() {
|
|
w.showStep(stepServer)
|
|
})
|
|
nextBtn := widget.NewButton("Connect", func() {
|
|
if w.authMethod.Selected == "Token" && w.authToken.Text == "" {
|
|
w.app.showError("Token is required")
|
|
return
|
|
}
|
|
if w.authMethod.Selected == "Username/Password" {
|
|
if w.authUser.Text == "" || w.authPass.Text == "" {
|
|
w.app.showError("Username and password are required")
|
|
return
|
|
}
|
|
}
|
|
w.showStep(stepFinish)
|
|
})
|
|
nextBtn.Importance = widget.HighImportance
|
|
|
|
return container.NewVBox(
|
|
title,
|
|
widget.NewLabel(""),
|
|
desc,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
widget.NewSeparator(),
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
methodForm,
|
|
widget.NewLabel(""),
|
|
authContent,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
layout.NewSpacer(),
|
|
container.NewHBox(backBtn, layout.NewSpacer(), nextBtn),
|
|
)
|
|
}
|
|
|
|
func (w *Wizard) finishPage() fyne.CanvasObject {
|
|
title := canvas.NewText("Setup Complete!", colorForeground)
|
|
title.TextSize = 32
|
|
title.TextStyle = fyne.TextStyle{Bold: true}
|
|
title.Alignment = fyne.TextAlignCenter
|
|
|
|
checkIcon := canvas.NewText("\u2713", colorSuccess)
|
|
checkIcon.TextSize = 64
|
|
checkIcon.Alignment = fyne.TextAlignCenter
|
|
|
|
summary := fmt.Sprintf(`**Server:** %s:%s
|
|
**Auth:** %s
|
|
**Client:** %s`,
|
|
w.serverAddr.Text, w.serverPort.Text,
|
|
w.authMethod.Selected,
|
|
func() string {
|
|
if w.authMethod.Selected == "Token" {
|
|
return "Token auth"
|
|
}
|
|
if w.clientName.Text != "" {
|
|
return w.clientName.Text
|
|
}
|
|
return w.authUser.Text
|
|
}(),
|
|
)
|
|
|
|
summaryText := widget.NewRichTextFromMarkdown(summary)
|
|
|
|
backBtn := widget.NewButton("Back", func() {
|
|
w.showStep(stepAuth)
|
|
})
|
|
finishBtn := widget.NewButton("Launch Dashboard", func() {
|
|
if w.onComplete != nil {
|
|
w.onComplete(WizardResult{
|
|
ServerAddr: w.serverAddr.Text,
|
|
ServerPort: w.serverPort.Text,
|
|
AuthMethod: w.authMethod.Selected,
|
|
Token: w.authToken.Text,
|
|
Username: w.authUser.Text,
|
|
Password: w.authPass.Text,
|
|
ClientName: w.clientName.Text,
|
|
})
|
|
}
|
|
})
|
|
finishBtn.Importance = widget.HighImportance
|
|
|
|
return container.NewVBox(
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), checkIcon, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
widget.NewSeparator(),
|
|
widget.NewLabel(""),
|
|
summaryText,
|
|
widget.NewLabel(""),
|
|
widget.NewLabel(""),
|
|
layout.NewSpacer(),
|
|
container.NewHBox(backBtn, layout.NewSpacer(), finishBtn),
|
|
)
|
|
}
|
|
|
|
func (w *Wizard) Content() *fyne.Container {
|
|
return w.container
|
|
}
|