native dialogs

This commit is contained in:
Gregory Bednov 2025-09-21 15:18:18 +03:00
commit fda504005e
3 changed files with 87 additions and 73 deletions

4
go.mod
View file

@ -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

6
go.sum
View file

@ -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=

130
main.go
View file

@ -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 {
savePath, err := simpledialog.File().Filter("JSON files", "json").Title("Экспорт данных о сети").Save()
if err != nil {
return
}
defer uw.Close()
// открыть исходный файл
f, e := os.Open(src)
if e != nil {
st.alert("Ошибка чтения genesis: " + e.Error())
simpledialog.Message("Ошибка чтения genesis: %s", e.Error()).Title("Ошибка").Error()
return
}
defer f.Close()
if _, e = io.Copy(uw, f); e != nil {
st.alert("Ошибка записи: " + e.Error())
// создать файл для сохранения
uw, e := os.Create(savePath)
if e != nil {
simpledialog.Message("Ошибка создания файла: %s", e.Error()).Title("Ошибка").Error()
return
}
st.notify("Готово: экспортирован genesis.json")
}, w)
fd.SetFileName("genesis.json")
fd.Show()
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
filename, err := simpledialog.File().Filter("Genesis JSON file", "json").Load()
if err == nil {
genesisPathEntry.SetText(filename)
}
genesisPathEntry.SetText(uc.URI().Path())
}, w)
fd.Show()
})
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)
}
}