diff --git a/go.mod b/go.mod index 785d9e1..6dbef67 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( fyne.io/fyne/v2 v2.6.1 github.com/emersion/go-autostart v0.0.0-20250403115856-34830d6457d2 github.com/gofrs/flock v0.12.1 - github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5 + github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f github.com/pelletier/go-toml/v2 v2.2.4 ) @@ -20,6 +20,7 @@ require ( github.com/DataDog/zstd v1.4.5 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.14.3 // indirect @@ -111,6 +112,7 @@ require ( github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.19.0 // indirect + github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.10.0 // indirect diff --git a/go.sum b/go.sum index 42c0a26..d5bf063 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= @@ -231,6 +233,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5 h1:J8IkR4rM3xQ/p0iCArRA4u3IcqczTvrpIuuKtUd2KCw= github.com/gregorybednov/lbc v0.0.0-20250901080446-e7a9e1092be5/go.mod h1:6JpnLLSnq0Lc/w4kYThFeksLfNGTDbKvpiS4vvWs9is= +github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f h1:TxTIxFNokRmFrVv97rWEwJqPXUz89UJPG6FwlxkMdbw= +github.com/gregorybednov/lbc v0.0.0-20250917212825-f35c528ba65f/go.mod h1:6JpnLLSnq0Lc/w4kYThFeksLfNGTDbKvpiS4vvWs9is= github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa h1:8EuqAmsS94ju83o4aEIV8e2fdocdAJ9xVerKRSI+nDA= github.com/gregorybednov/lbc_sdk v0.0.0-20250810123844-a90b874431fa/go.mod h1:DBE00+SaYBtD4qw+nOtSTLuF6h9Ia4TkuBMJB+6krik= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= @@ -388,6 +392,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 h1:2JL2wmHXWIAxDofCK+AdkFi1KEg3dgkefCsm7isADzQ= +github.com/sqweek/dialog v0.0.0-20240226140203-065105509627/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= diff --git a/main.go b/main.go index 12fd973..8a6fd61 100644 --- a/main.go +++ b/main.go @@ -13,25 +13,22 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/widget" + simpledialog "github.com/sqweek/dialog" + _ "embed" "github.com/emersion/go-autostart" "github.com/gofrs/flock" "github.com/gregorybednov/lbc/cli" - toml "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2" ) //go:embed icon.png var iconPNG []byte -func appIcon() fyne.Resource { - return fyne.NewStaticResource("icon.png", iconPNG) -} - /* --------------------------- Конфиг лончера --------------------------- */ type LauncherCfg struct { @@ -101,8 +98,18 @@ func (w *uiWriter) Write(p []byte) (int, error) { func main() { st := &AppState{} - must(initConfig(st)) - must(acquireSingleInstanceLock(st)) + + err := initConfig(st) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + err = acquireSingleInstanceLock(st) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } defer releaseSingleInstanceLock(st) exe, _ := os.Executable() @@ -115,6 +122,7 @@ func main() { a := app.New() w := a.NewWindow("LBС Launcher") + st.app, st.win = a, w // Статус + логи @@ -124,18 +132,15 @@ func main() { st.logArea.Disable() // только для чтения homeEntry := widget.NewEntry() - homeEntry.SetPlaceHolder("Каталог HOME узла (будут data/, config/)") + homeEntry.SetPlaceHolder("Полный путь до каталога (там будут данные и настройки)") homeEntry.SetText(st.cfg.NodeHome) - btnPickHome := widget.NewButton("Выбрать HOME…", func() { - dd := dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) { - if lu == nil || err != nil { - return - } - st.cfg.NodeHome = lu.Path() + btnPickHome := widget.NewButton("Выбрать каталог…", func() { + directory, err := simpledialog.Directory().Title("Выбрать каталог с настройками и данными").Browse() + if err == nil { + st.cfg.NodeHome = directory homeEntry.SetText(st.cfg.NodeHome) _ = saveConfig(st) - }, w) - dd.Show() + } }) topControls := container.NewVBox( @@ -151,7 +156,7 @@ func main() { go func() { w := &uiWriter{st} if err := cli.ExecuteWithArgs([]string{"init", "genesis"}, homeEntry.Text, w, w); err != nil { - st.alert("Ошибка: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error() } else { st.notify("init genesis: готово") } @@ -170,33 +175,45 @@ func main() { exportBtn := widget.NewButton("Экспортировать genesis.json…", func() { // genesis лежит в HOME/config/genesis.json if homeEntry.Text == "" { - st.alert("Сначала укажи HOME каталóg узла.") + simpledialog.Message("%s", "Сначала укажите домашний каталог узла").Title("Не найден HOME").Error() return } src := filepath.Join(homeEntry.Text, "config", "genesis.json") if _, err := os.Stat(src); err != nil { - st.alert("Не найден файл: " + src) + simpledialog.Message("%s", "Не найден файл"+src).Title("Файл не найден").Error() return } - fd := dialog.NewFileSave(func(uw fyne.URIWriteCloser, err error) { - if uw == nil || err != nil { - return - } - defer uw.Close() - f, e := os.Open(src) - if e != nil { - st.alert("Ошибка чтения genesis: " + e.Error()) - return - } - defer f.Close() - if _, e = io.Copy(uw, f); e != nil { - st.alert("Ошибка записи: " + e.Error()) - return - } - st.notify("Готово: экспортирован genesis.json") - }, w) - fd.SetFileName("genesis.json") - fd.Show() + + savePath, err := simpledialog.File().Filter("JSON files", "json").Title("Экспорт данных о сети").Save() + if err != nil { + return + } + + // открыть исходный файл + f, e := os.Open(src) + if e != nil { + simpledialog.Message("Ошибка чтения genesis: %s", e.Error()).Title("Ошибка").Error() + return + } + defer f.Close() + + // создать файл для сохранения + uw, e := os.Create(savePath) + if e != nil { + simpledialog.Message("Ошибка создания файла: %s", e.Error()).Title("Ошибка").Error() + return + } + defer uw.Close() + + // копируем содержимое + if _, e = io.Copy(uw, f); e != nil { + simpledialog.Message("Ошибка записи: %s", e.Error()).Title("Ошибка").Error() + return + } + + st.notify(fmt.Sprintf("Готово: экспортирован genesis.json -> %s", savePath)) + //fd.SetFileName("genesis.json") + //fd.Show() }) tabExportGenesis := container.NewVBox( widget.NewLabel("Экспорт genesis.json из текущего узла"), @@ -207,13 +224,11 @@ func main() { genesisPathEntry := widget.NewEntry() genesisPathEntry.SetPlaceHolder("Путь к существующему genesis.json") pickGenesis := widget.NewButton("Выбрать genesis.json…", func() { - fd := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) { - if uc == nil || err != nil { - return - } - genesisPathEntry.SetText(uc.URI().Path()) - }, w) - fd.Show() + filename, err := simpledialog.File().Filter("Genesis JSON file", "json").Load() + if err == nil { + genesisPathEntry.SetText(filename) + } + }) joinBtn := widget.NewButton("Создать узел-присоединение (init joiner)", func() { if !st.ensurePathsOk(homeEntry.Text) { @@ -222,21 +237,21 @@ func main() { // 1) скопировать genesis.json в HOME/config/genesis.json src := genesisPathEntry.Text if src == "" { - st.alert("Укажи путь к genesis.json") + simpledialog.Message("%s", "Укажите путь к genesis.json").Title("Не найден файл").Error() return } dest := filepath.Join(homeEntry.Text, "config") _ = os.MkdirAll(dest, 0o755) destFile := filepath.Join(dest, "genesis.json") if err := fileCopy(src, destFile); err != nil { - st.alert("Не удалось скопировать genesis.json: " + err.Error()) + simpledialog.Message("%s", "Не удалось скопировать genesis.json: "+err.Error()).Title("Ошибка генерации").Error() return } // 2) lbc init joiner go func() { w := &uiWriter{st} if err := cli.ExecuteWithArgs([]string{"init", "joiner", genesisPathEntry.Text}, homeEntry.Text, w, w); err != nil { - st.alert("Ошибка: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error() } else { st.notify("init joiner: готово") } @@ -258,12 +273,12 @@ func main() { loadCfgBtn := widget.NewButton("Загрузить config.toml", func() { if homeEntry.Text == "" { - st.alert("Укажи HOME узла.") + simpledialog.Message("%s", "Укажите домашнюю директорию узла").Title("Ошибка сохранения").Error() return } cfgp := filepath.Join(homeEntry.Text, "config", "config.toml") if cfg, err := loadTOML(cfgp); err != nil { - st.alert("Чтение TOML: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Ошибка чтения TOML").Error() } else { cfgPathLbl.SetText("Файл: " + cfgp) monikerEntry.SetText(cfg.Moniker) @@ -278,13 +293,13 @@ func main() { }) saveCfgBtn := widget.NewButton("Сохранить", func() { if homeEntry.Text == "" { - st.alert("Укажи HOME узла.") + simpledialog.Message("%s", "Укажите домашнюю директорию узла").Title("Ошибка сохранения").Error() return } cfgp := filepath.Join(homeEntry.Text, "config", "config.toml") - cfg, err := loadTOML(cfgp) // мягко читаем всё, меняем только интересующие поля + cfg, err := loadTOML(cfgp) if err != nil { - st.alert("Чтение TOML: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Ошибка чтения TOML").Error() return } cfg.Moniker = monikerEntry.Text @@ -292,7 +307,7 @@ func main() { cfg.P2P.PersistentPeers = ppeersEntry.Text cfg.LogLevel = logLevelSel.Selected if err := saveTOML(cfgp, cfg); err != nil { - st.alert("Запись TOML: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Ошибка записи TOML").Error() return } st.notify("config.toml сохранён") @@ -326,7 +341,7 @@ func main() { cbAutostart := widget.NewCheck("Запускать при входе в систему", func(v bool) { if v { if err := autoApp.Enable(); err != nil { - st.alert("Автозапуск: " + err.Error()) + simpledialog.Message("%s", err.Error()).Title("Произошла ошибка автозапуска").Error() return } } else { @@ -352,7 +367,7 @@ func main() { // Трей-меню if desk, ok := a.(desktop.App); ok { - desk.SetSystemTrayIcon(appIcon()) + desk.SetSystemTrayIcon(fyne.NewStaticResource("icon.png", iconPNG)) openItem := fyne.NewMenuItem("Открыть", func() { w.Show(); w.RequestFocus() }) autoItem := fyne.NewMenuItem("Автозапуск", nil) autoItem.Checked = autoApp.IsEnabled() @@ -360,7 +375,7 @@ func main() { ns := !autoItem.Checked if ns { if err := autoApp.Enable(); err != nil { - st.alert(err.Error()) + simpledialog.Message("%s", err.Error()).Title("Произошла ошибка").Error() return } } else { @@ -370,7 +385,7 @@ func main() { st.cfg.AutoStart = ns _ = saveConfig(st) } - quitItem := fyne.NewMenuItem("Выйти", func() { releaseSingleInstanceLock(st); a.Quit() }) + quitItem := fyne.NewMenuItem("Выход", func() { releaseSingleInstanceLock(st); a.Quit() }) desk.SetSystemTrayMenu(fyne.NewMenu("", openItem, autoItem, fyne.NewMenuItemSeparator(), quitItem)) } @@ -503,21 +518,21 @@ func (st *AppState) appendLog(s string) { st.logArea.CursorRow = 999999 }() } -func (st *AppState) alert(msg string) { - dialog.ShowInformation("Сообщение", msg, st.win) -} + func (st *AppState) notify(msg string) { st.app.SendNotification(&fyne.Notification{Title: "LBClient", Content: msg}) } + func (st *AppState) ensurePathsOk(home string) bool { if home == "" { - st.alert("Укажи HOME каталог узла") + simpledialog.Message("%s", "Каталог узла не найден").Title("Не найден HOME").Error() return false } st.cfg.NodeHome = home _ = saveConfig(st) return true } + func fileCopy(src, dst string) error { in, err := os.Open(src) if err != nil { @@ -537,12 +552,3 @@ func fileCopy(src, dst string) error { } return out.Close() } - -/* --------------------------------- misc -------------------------------- */ - -func must(err error) { - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -}