app-init
This commit is contained in:
@@ -0,0 +1,692 @@
|
||||
//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")
|
||||
)
|
||||
Reference in New Issue
Block a user