693 lines
18 KiB
Go
693 lines
18 KiB
Go
//go:build windows
|
||
|
||
package gui
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
_ "embed"
|
||
"fmt"
|
||
"image/png"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"unsafe"
|
||
|
||
"github.com/lxn/walk"
|
||
. "github.com/lxn/walk/declarative"
|
||
"github.com/lxn/win"
|
||
"golang.org/x/sys/windows"
|
||
|
||
"mysql-tunnel/internal/app"
|
||
"mysql-tunnel/internal/config"
|
||
"mysql-tunnel/internal/logger"
|
||
)
|
||
|
||
//go:embed logo.png
|
||
var logoPNG []byte
|
||
|
||
func init() {
|
||
runtime.LockOSThread()
|
||
}
|
||
|
||
func Run(configFlag string) int {
|
||
hideConsoleIfStandalone()
|
||
path := strings.TrimSpace(configFlag)
|
||
if path == "" {
|
||
path = "config.yaml"
|
||
}
|
||
if resolved, err := config.ResolveConfigPath(path); err == nil {
|
||
path = resolved
|
||
} else if path == "config.yaml" {
|
||
path = config.DefaultSavePath()
|
||
} else if abs, err := filepath.Abs(path); err == nil {
|
||
path = abs
|
||
}
|
||
if err := runWindow(config.LoadOrEmpty(path)); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
type ui struct {
|
||
mw *walk.MainWindow
|
||
ni *walk.NotifyIcon
|
||
|
||
listenHost *walk.LineEdit
|
||
listenPort *walk.NumberEdit
|
||
localUser *walk.LineEdit
|
||
localPass *walk.LineEdit
|
||
tunnelURL *walk.LineEdit
|
||
encodeB64 *walk.CheckBox
|
||
tlsSkip *walk.CheckBox
|
||
remoteHost *walk.LineEdit
|
||
remotePort *walk.NumberEdit
|
||
remoteUser *walk.LineEdit
|
||
remotePass *walk.LineEdit
|
||
remoteDB *walk.LineEdit
|
||
charset *walk.LineEdit
|
||
timeout *walk.NumberEdit
|
||
maxConn *walk.NumberEdit
|
||
logLevel *walk.ComboBox
|
||
logSQL *walk.CheckBox
|
||
logFile *walk.LineEdit
|
||
status *walk.TextEdit
|
||
saveBtn *walk.PushButton
|
||
testBtn *walk.PushButton
|
||
startBtn *walk.PushButton
|
||
stopBtn *walk.PushButton
|
||
pathLabel *walk.Label
|
||
hint *walk.StatusBarItem
|
||
logoView *walk.ImageView
|
||
appIcon *walk.Icon
|
||
appBmp *walk.Bitmap
|
||
|
||
cfgPath string
|
||
|
||
mu sync.Mutex
|
||
running bool
|
||
busy bool
|
||
cancel context.CancelFunc
|
||
log *logger.Logger
|
||
exiting bool
|
||
hidOnce bool
|
||
}
|
||
|
||
func runWindow(cfg *config.Config) error {
|
||
u := &ui{cfgPath: cfg.Path}
|
||
if err := u.build(cfg); err != nil {
|
||
return err
|
||
}
|
||
defer u.dispose()
|
||
u.mw.Run()
|
||
return nil
|
||
}
|
||
|
||
func (u *ui) dispose() {
|
||
u.stopServer()
|
||
if u.ni != nil {
|
||
_ = u.ni.Dispose()
|
||
}
|
||
}
|
||
|
||
func (u *ui) build(cfg *config.Config) error {
|
||
logFile := relativeLogFile(cfg)
|
||
levels := []string{"debug", "info", "warn", "error"}
|
||
levelIdx := 1
|
||
for i, s := range levels {
|
||
if s == cfg.LogLevel {
|
||
levelIdx = i
|
||
break
|
||
}
|
||
}
|
||
|
||
if err := (MainWindow{
|
||
AssignTo: &u.mw,
|
||
Title: "MySQL Tunnel",
|
||
MinSize: Size{Width: 780, Height: 560},
|
||
Size: Size{Width: 840, Height: 620},
|
||
Layout: VBox{Margins: Margins{Left: 12, Top: 10, Right: 12, Bottom: 8}, Spacing: 8},
|
||
StatusBarItems: []StatusBarItem{
|
||
{AssignTo: &u.hint, Text: "就绪", Width: 800},
|
||
},
|
||
Children: []Widget{
|
||
Composite{
|
||
Layout: HBox{Spacing: 10, MarginsZero: true},
|
||
Children: []Widget{
|
||
ImageView{
|
||
AssignTo: &u.logoView,
|
||
MinSize: Size{Width: 40, Height: 40},
|
||
MaxSize: Size{Width: 40, Height: 40},
|
||
Mode: ImageViewModeZoom,
|
||
},
|
||
Composite{
|
||
Layout: VBox{MarginsZero: true, Spacing: 2},
|
||
Children: []Widget{
|
||
Label{Text: "MySQL Tunnel", Font: Font{Family: "Segoe UI", PointSize: 13, Bold: true}},
|
||
Label{AssignTo: &u.pathLabel, Text: cfg.Path},
|
||
},
|
||
},
|
||
HSpacer{},
|
||
},
|
||
},
|
||
Composite{
|
||
Layout: HBox{Spacing: 10, MarginsZero: true},
|
||
Children: []Widget{
|
||
Composite{
|
||
StretchFactor: 1,
|
||
MinSize: Size{Width: 380},
|
||
Layout: VBox{MarginsZero: true, Spacing: 8},
|
||
Children: []Widget{
|
||
GroupBox{
|
||
Title: "本机监听(影刀连这里)",
|
||
Layout: Grid{Columns: 2, Spacing: 6},
|
||
Children: []Widget{
|
||
fieldLabel("地址"),
|
||
LineEdit{AssignTo: &u.listenHost, Text: cfg.ListenHost},
|
||
fieldLabel("端口"),
|
||
NumberEdit{AssignTo: &u.listenPort, Value: float64(cfg.ListenPort), MinValue: 1, MaxValue: 65535, Decimals: 0, MinSize: Size{Width: 90}},
|
||
fieldLabel("本机用户"),
|
||
LineEdit{AssignTo: &u.localUser, Text: cfg.LocalUsername},
|
||
fieldLabel("本机密码"),
|
||
LineEdit{AssignTo: &u.localPass, Text: cfg.LocalPassword, PasswordMode: true},
|
||
},
|
||
},
|
||
GroupBox{
|
||
Title: "HTTP 隧道",
|
||
StretchFactor: 1,
|
||
Layout: Grid{Columns: 2, Spacing: 6},
|
||
Children: []Widget{
|
||
fieldLabel("URL"),
|
||
LineEdit{AssignTo: &u.tunnelURL, Text: cfg.TunnelURL},
|
||
CheckBox{AssignTo: &u.encodeB64, Text: "SQL Base64(WAF)", Checked: cfg.EncodeBase64, ColumnSpan: 2},
|
||
CheckBox{AssignTo: &u.tlsSkip, Text: "跳过 TLS 证书校验(仅排障)", Checked: cfg.TLSInsecureSkipVerify, ColumnSpan: 2},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
Composite{
|
||
StretchFactor: 1,
|
||
MinSize: Size{Width: 380},
|
||
Layout: VBox{MarginsZero: true, Spacing: 8},
|
||
Children: []Widget{
|
||
GroupBox{
|
||
Title: "远程 MySQL(PHP 主机访问)",
|
||
Layout: Grid{Columns: 2, Spacing: 6},
|
||
Children: []Widget{
|
||
fieldLabel("主机"),
|
||
LineEdit{AssignTo: &u.remoteHost, Text: cfg.RemoteHost},
|
||
fieldLabel("端口"),
|
||
NumberEdit{AssignTo: &u.remotePort, Value: float64(nonzero(cfg.RemotePort, 3306)), MinValue: 1, MaxValue: 65535, Decimals: 0, MinSize: Size{Width: 90}},
|
||
fieldLabel("用户名"),
|
||
LineEdit{AssignTo: &u.remoteUser, Text: cfg.RemoteUsername},
|
||
fieldLabel("密码"),
|
||
LineEdit{AssignTo: &u.remotePass, Text: cfg.RemotePassword, PasswordMode: true},
|
||
fieldLabel("默认库"),
|
||
LineEdit{AssignTo: &u.remoteDB, Text: cfg.RemoteDatabase},
|
||
fieldLabel("字符集"),
|
||
LineEdit{AssignTo: &u.charset, Text: orDefault(cfg.RemoteCharset, "utf8mb4")},
|
||
},
|
||
},
|
||
GroupBox{
|
||
Title: "选项",
|
||
StretchFactor: 1,
|
||
Layout: Grid{Columns: 2, Spacing: 6},
|
||
Children: []Widget{
|
||
fieldLabel("超时(秒)"),
|
||
NumberEdit{AssignTo: &u.timeout, Value: float64(timeoutSec(cfg)), MinValue: 1, MaxValue: 600, Decimals: 0, MinSize: Size{Width: 90}},
|
||
fieldLabel("最大连接"),
|
||
NumberEdit{AssignTo: &u.maxConn, Value: float64(nonzero(cfg.MaxConnections, 5)), MinValue: 1, MaxValue: 256, Decimals: 0, MinSize: Size{Width: 90}},
|
||
fieldLabel("日志级别"),
|
||
ComboBox{AssignTo: &u.logLevel, Model: levels, CurrentIndex: levelIdx, Editable: false},
|
||
fieldLabel("日志文件"),
|
||
LineEdit{AssignTo: &u.logFile, Text: logFile},
|
||
CheckBox{AssignTo: &u.logSQL, Text: "记录 SQL(仍会脱敏)", Checked: cfg.LogSQL, ColumnSpan: 2},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
Composite{
|
||
Layout: HBox{Spacing: 8},
|
||
Children: []Widget{
|
||
PushButton{AssignTo: &u.saveBtn, Text: "保存", MinSize: Size{Width: 88}},
|
||
PushButton{AssignTo: &u.testBtn, Text: "测试连接", MinSize: Size{Width: 88}},
|
||
PushButton{AssignTo: &u.startBtn, Text: "启动", MinSize: Size{Width: 88}},
|
||
PushButton{AssignTo: &u.stopBtn, Text: "停止", Enabled: false, MinSize: Size{Width: 88}},
|
||
HSpacer{},
|
||
},
|
||
},
|
||
GroupBox{
|
||
Title: "运行日志",
|
||
StretchFactor: 1,
|
||
MinSize: Size{Height: 140},
|
||
Layout: VBox{MarginsZero: true},
|
||
Children: []Widget{
|
||
TextEdit{AssignTo: &u.status, ReadOnly: true, VScroll: true, MaxLength: 64 * 1024},
|
||
},
|
||
},
|
||
},
|
||
}).Create(); err != nil {
|
||
return err
|
||
}
|
||
|
||
if u.saveBtn != nil {
|
||
u.saveBtn.Clicked().Attach(u.onSave)
|
||
}
|
||
if u.testBtn != nil {
|
||
u.testBtn.Clicked().Attach(u.onTest)
|
||
}
|
||
if u.startBtn != nil {
|
||
u.startBtn.Clicked().Attach(u.onStart)
|
||
}
|
||
if u.stopBtn != nil {
|
||
u.stopBtn.Clicked().Attach(u.onStop)
|
||
u.stopBtn.SetEnabled(false)
|
||
}
|
||
u.appendStatus("填写后点「保存」,再「测试连接」。通过后点「启动」,影刀连 本机地址:端口。")
|
||
u.appendStatus("最小化或关闭窗口会藏到系统托盘(监听可继续)。托盘左键打开,右键退出。")
|
||
for _, w := range cfg.Warnings {
|
||
u.appendStatus("注意: " + w)
|
||
}
|
||
|
||
if err := u.applyBranding(); err != nil {
|
||
return err
|
||
}
|
||
|
||
ni, err := walk.NewNotifyIcon(u.mw)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
u.ni = ni
|
||
if u.appIcon != nil {
|
||
_ = ni.SetIcon(u.appIcon)
|
||
_ = u.mw.SetIcon(u.appIcon)
|
||
}
|
||
_ = ni.SetToolTip("MySQL Tunnel")
|
||
_ = ni.SetVisible(true)
|
||
ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) {
|
||
if button == walk.LeftButton {
|
||
u.showWindow()
|
||
}
|
||
})
|
||
|
||
showAct := walk.NewAction()
|
||
_ = showAct.SetText("打开窗口")
|
||
showAct.Triggered().Attach(u.showWindow)
|
||
exitAct := walk.NewAction()
|
||
_ = exitAct.SetText("退出")
|
||
exitAct.Triggered().Attach(u.onExit)
|
||
if menu := ni.ContextMenu(); menu != nil {
|
||
_ = menu.Actions().Add(showAct)
|
||
_ = menu.Actions().Add(exitAct)
|
||
}
|
||
|
||
// Minimize → tray (not taskbar). Close also → tray unless exiting.
|
||
u.mw.SizeChanged().Attach(func() {
|
||
hwnd := win.HWND(u.mw.Handle())
|
||
if win.IsIconic(hwnd) {
|
||
u.hideToTray()
|
||
}
|
||
})
|
||
u.mw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
|
||
u.mu.Lock()
|
||
exiting := u.exiting
|
||
u.mu.Unlock()
|
||
if exiting {
|
||
return
|
||
}
|
||
*canceled = true
|
||
u.hideToTray()
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func (u *ui) hideToTray() {
|
||
if u.mw == nil {
|
||
return
|
||
}
|
||
u.mw.Hide()
|
||
hwnd := win.HWND(u.mw.Handle())
|
||
win.ShowWindow(hwnd, win.SW_HIDE)
|
||
if u.ni != nil && !u.hidOnce {
|
||
u.hidOnce = true
|
||
_ = u.ni.ShowInfo("MySQL Tunnel", "已放到系统托盘。左键打开窗口,右键退出。")
|
||
}
|
||
}
|
||
|
||
func (u *ui) showWindow() {
|
||
if u.mw == nil {
|
||
return
|
||
}
|
||
u.mw.Show()
|
||
hwnd := win.HWND(u.mw.Handle())
|
||
win.ShowWindow(hwnd, win.SW_RESTORE)
|
||
win.SetForegroundWindow(hwnd)
|
||
}
|
||
|
||
func (u *ui) onExit() {
|
||
u.mu.Lock()
|
||
u.exiting = true
|
||
u.mu.Unlock()
|
||
u.stopServer()
|
||
if u.ni != nil {
|
||
_ = u.ni.SetVisible(false)
|
||
}
|
||
u.mw.Close()
|
||
}
|
||
|
||
func (u *ui) collect() (*config.Config, error) {
|
||
c := config.Defaults()
|
||
c.Path = u.cfgPath
|
||
c.ListenHost = strings.TrimSpace(lineText(u.listenHost))
|
||
c.ListenPort = numValue(u.listenPort, config.DefaultListenPort)
|
||
c.LocalUsername = strings.TrimSpace(lineText(u.localUser))
|
||
c.LocalPassword = lineText(u.localPass)
|
||
c.TunnelURL = strings.TrimSpace(lineText(u.tunnelURL))
|
||
c.EncodeBase64 = boxChecked(u.encodeB64)
|
||
c.TLSInsecureSkipVerify = boxChecked(u.tlsSkip)
|
||
c.RemoteHost = strings.TrimSpace(lineText(u.remoteHost))
|
||
c.RemotePort = numValue(u.remotePort, 3306)
|
||
c.RemoteUsername = strings.TrimSpace(lineText(u.remoteUser))
|
||
c.RemotePassword = lineText(u.remotePass)
|
||
c.RemoteDatabase = strings.TrimSpace(lineText(u.remoteDB))
|
||
c.RemoteCharset = strings.TrimSpace(lineText(u.charset))
|
||
c.RequestTimeout = time.Duration(numValue(u.timeout, config.DefaultTimeoutSec)) * time.Second
|
||
c.MaxConnections = numValue(u.maxConn, config.DefaultMaxConnections)
|
||
if u.logLevel != nil {
|
||
c.LogLevel = strings.ToLower(strings.TrimSpace(u.logLevel.Text()))
|
||
}
|
||
if c.LogLevel == "" {
|
||
c.LogLevel = config.DefaultLogLevel
|
||
}
|
||
c.LogSQL = boxChecked(u.logSQL)
|
||
c.LogFile = strings.TrimSpace(lineText(u.logFile))
|
||
if c.LogFile == "" {
|
||
c.LogFile = config.DefaultLogFile
|
||
}
|
||
if err := c.Validate(); err != nil {
|
||
return &c, err
|
||
}
|
||
c.ResolvePaths()
|
||
return &c, nil
|
||
}
|
||
|
||
func (u *ui) onSave() {
|
||
c, err := u.collect()
|
||
if err != nil {
|
||
u.appendStatus("保存失败: " + err.Error())
|
||
walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
if err := config.Save(u.cfgPath, c); err != nil {
|
||
u.appendStatus("保存失败: " + err.Error())
|
||
walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
u.appendStatus("已保存 " + u.cfgPath)
|
||
for _, w := range c.Warnings {
|
||
u.appendStatus("注意: " + w)
|
||
}
|
||
}
|
||
|
||
func (u *ui) onTest() {
|
||
c, err := u.collect()
|
||
if err != nil {
|
||
u.appendStatus("无法测试: " + err.Error())
|
||
walk.MsgBox(u.mw, "MySQL Tunnel", "无法测试:\n"+err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
u.setBusy(true)
|
||
u.setHint("正在测试隧道…")
|
||
u.appendStatus("正在测试 " + config.RedactedTunnelURL(c.TunnelURL) + " …")
|
||
go func() {
|
||
defer func() {
|
||
if rec := recover(); rec != nil {
|
||
u.mw.Synchronize(func() {
|
||
msg := fmt.Sprintf("测试异常: %v", rec)
|
||
u.appendStatus(msg)
|
||
u.setHint("测试异常")
|
||
walk.MsgBox(u.mw, "测试结果", msg, walk.MsgBoxIconError)
|
||
u.setBusy(false)
|
||
})
|
||
}
|
||
}()
|
||
log, _ := logger.New(c.LogFile, c.LogLevel, c.LogSQL)
|
||
r := app.Test(context.Background(), c, log)
|
||
if log != nil {
|
||
_ = log.Close()
|
||
}
|
||
u.mw.Synchronize(func() {
|
||
u.appendStatus(strings.ReplaceAll(r.Text(), "\n", "\r\n"))
|
||
icon := walk.MsgBoxIconError
|
||
hint := "测试未通过"
|
||
if r.OK() {
|
||
icon = walk.MsgBoxIconInformation
|
||
hint = "测试通过"
|
||
u.appendStatus("测试通过,可以启动监听。")
|
||
} else {
|
||
u.appendStatus("测试未通过。")
|
||
}
|
||
u.setHint(hint)
|
||
walk.MsgBox(u.mw, "测试结果", strings.ReplaceAll(r.Text(), "\r\n", "\n"), icon)
|
||
u.setBusy(false)
|
||
})
|
||
}()
|
||
}
|
||
|
||
func (u *ui) onStart() {
|
||
u.mu.Lock()
|
||
if u.running {
|
||
u.mu.Unlock()
|
||
return
|
||
}
|
||
u.mu.Unlock()
|
||
c, err := u.collect()
|
||
if err != nil {
|
||
u.appendStatus("无法启动: " + err.Error())
|
||
walk.MsgBox(u.mw, "MySQL Tunnel", err.Error(), walk.MsgBoxIconError)
|
||
return
|
||
}
|
||
log, err := logger.New(c.LogFile, c.LogLevel, c.LogSQL)
|
||
if err != nil {
|
||
u.appendStatus("日志: " + err.Error())
|
||
return
|
||
}
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
u.mu.Lock()
|
||
u.cancel = cancel
|
||
u.log = log
|
||
u.running = true
|
||
u.mu.Unlock()
|
||
u.refreshButtons()
|
||
u.appendStatus(fmt.Sprintf("正在监听 %s:%d 影刀用户=%s", c.ListenHost, c.ListenPort, c.LocalUsername))
|
||
u.appendStatus("隧道: " + config.RedactedTunnelURL(c.TunnelURL))
|
||
if u.ni != nil {
|
||
_ = u.ni.SetToolTip(fmt.Sprintf("MySQL Tunnel · %s:%d", c.ListenHost, c.ListenPort))
|
||
}
|
||
go func() {
|
||
err := app.Serve(ctx, c, log)
|
||
u.mw.Synchronize(func() {
|
||
u.mu.Lock()
|
||
u.running = false
|
||
u.cancel = nil
|
||
if u.log != nil {
|
||
_ = u.log.Close()
|
||
u.log = nil
|
||
}
|
||
u.mu.Unlock()
|
||
if err != nil && ctx.Err() == nil {
|
||
u.appendStatus("监听结束: " + logger.RedactError(err))
|
||
} else {
|
||
u.appendStatus("监听已停止")
|
||
}
|
||
if u.ni != nil {
|
||
_ = u.ni.SetToolTip("MySQL Tunnel")
|
||
}
|
||
u.refreshButtons()
|
||
})
|
||
}()
|
||
}
|
||
|
||
func (u *ui) onStop() {
|
||
u.stopServer()
|
||
}
|
||
|
||
func (u *ui) stopServer() {
|
||
u.mu.Lock()
|
||
cancel := u.cancel
|
||
u.mu.Unlock()
|
||
if cancel != nil {
|
||
cancel()
|
||
}
|
||
}
|
||
|
||
func (u *ui) setBusy(b bool) {
|
||
u.mu.Lock()
|
||
u.busy = b
|
||
u.mu.Unlock()
|
||
u.refreshButtons()
|
||
}
|
||
|
||
func (u *ui) refreshButtons() {
|
||
u.mu.Lock()
|
||
running, busy := u.running, u.busy
|
||
u.mu.Unlock()
|
||
if u.saveBtn != nil {
|
||
u.saveBtn.SetEnabled(!busy)
|
||
}
|
||
if u.testBtn != nil {
|
||
u.testBtn.SetEnabled(!busy)
|
||
if busy {
|
||
_ = u.testBtn.SetText("测试中…")
|
||
} else {
|
||
_ = u.testBtn.SetText("测试连接")
|
||
}
|
||
}
|
||
if u.startBtn != nil {
|
||
u.startBtn.SetEnabled(!running && !busy)
|
||
}
|
||
if u.stopBtn != nil {
|
||
u.stopBtn.SetEnabled(running)
|
||
}
|
||
}
|
||
|
||
func (u *ui) setHint(s string) {
|
||
if u.hint == nil {
|
||
return
|
||
}
|
||
_ = u.hint.SetText(s)
|
||
}
|
||
|
||
func (u *ui) appendStatus(line string) {
|
||
line = strings.ReplaceAll(strings.TrimRight(line, "\r\n"), "\n", "\r\n")
|
||
if u.status != nil {
|
||
prev := u.status.Text()
|
||
if prev != "" {
|
||
prev += "\r\n"
|
||
}
|
||
text := prev + time.Now().Format("15:04:05") + " " + line
|
||
_ = u.status.SetText(text)
|
||
end := len([]rune(text))
|
||
u.status.SetTextSelection(end, end)
|
||
}
|
||
first := line
|
||
if i := strings.Index(first, "\r\n"); i >= 0 {
|
||
first = first[:i]
|
||
}
|
||
if len([]rune(first)) > 80 {
|
||
first = string([]rune(first)[:80]) + "…"
|
||
}
|
||
u.setHint(first)
|
||
}
|
||
|
||
func relativeLogFile(cfg *config.Config) string {
|
||
if cfg == nil || cfg.LogFile == "" {
|
||
return config.DefaultLogFile
|
||
}
|
||
if !filepath.IsAbs(cfg.LogFile) {
|
||
return filepath.ToSlash(cfg.LogFile)
|
||
}
|
||
rel, err := filepath.Rel(filepath.Dir(cfg.Path), cfg.LogFile)
|
||
if err != nil {
|
||
return cfg.LogFile
|
||
}
|
||
return filepath.ToSlash(rel)
|
||
}
|
||
|
||
func lineText(e *walk.LineEdit) string {
|
||
if e == nil {
|
||
return ""
|
||
}
|
||
return e.Text()
|
||
}
|
||
|
||
func numValue(e *walk.NumberEdit, def int) int {
|
||
if e == nil {
|
||
return def
|
||
}
|
||
v := int(e.Value())
|
||
if v == 0 {
|
||
return def
|
||
}
|
||
return v
|
||
}
|
||
|
||
func boxChecked(b *walk.CheckBox) bool {
|
||
return b != nil && b.Checked()
|
||
}
|
||
|
||
func nonzero(v, def int) int {
|
||
if v == 0 {
|
||
return def
|
||
}
|
||
return v
|
||
}
|
||
|
||
func orDefault(s, def string) string {
|
||
if strings.TrimSpace(s) == "" {
|
||
return def
|
||
}
|
||
return s
|
||
}
|
||
|
||
func fieldLabel(text string) Label {
|
||
return Label{Text: text, Alignment: AlignHFarVCenter, MinSize: Size{Width: 72}}
|
||
}
|
||
|
||
func (u *ui) applyBranding() error {
|
||
if len(logoPNG) == 0 {
|
||
return nil
|
||
}
|
||
img, err := png.Decode(bytes.NewReader(logoPNG))
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
if ico, err := walk.NewIconFromImage(img); err == nil {
|
||
u.appIcon = ico
|
||
if u.mw != nil {
|
||
_ = u.mw.SetIcon(ico)
|
||
}
|
||
}
|
||
if bmp, err := walk.NewBitmapFromImage(img); err == nil {
|
||
u.appBmp = bmp
|
||
if u.logoView != nil {
|
||
_ = u.logoView.SetImage(bmp)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func timeoutSec(cfg *config.Config) int {
|
||
if cfg == nil || cfg.RequestTimeout <= 0 {
|
||
return config.DefaultTimeoutSec
|
||
}
|
||
return int(cfg.RequestTimeout.Seconds())
|
||
}
|
||
|
||
func hideConsoleIfStandalone() {
|
||
var pids [16]uint32
|
||
n, _, _ := procGetConsoleProcessList.Call(uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids)))
|
||
if n == 0 || n > 1 {
|
||
return
|
||
}
|
||
hwnd, _, _ := procGetConsoleWindow.Call()
|
||
if hwnd == 0 {
|
||
return
|
||
}
|
||
procShowWindow.Call(hwnd, 0)
|
||
}
|
||
|
||
var (
|
||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||
procShowWindow = user32.NewProc("ShowWindow")
|
||
procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow")
|
||
procGetConsoleProcessList = kernel32.NewProc("GetConsoleProcessList")
|
||
)
|