package main import ( "fmt" "image/color" "io/fs" "log" "os" "os/exec" "path/filepath" "strings" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" ) var ( directory string configs []Config application fyne.App window fyne.Window menu *container.AppTabs ) type Config struct { Name string File string } type ButtonCallback func() func main() { application = app.New() window = application.NewWindow("Wireguard GUI") directory = "/etc/wireguard/" menu = container.NewAppTabs() err := initConfigs() if err != nil { log.Fatalln(err) } initMenu() content := container.New(layout.NewVBoxLayout(), menu) window.SetContent(content) window.Resize(fyne.NewSize(900, 400)) window.ShowAndRun() } func initMenu() { tabs := make([]fyne.Container, len(configs)) for i, config := range configs { tabs[i] = *createTab(config) } for i, config := range configs { menu.Append( container.NewTabItem( config.Name, &tabs[i], ), ) } } func initConfigs() error { err := filepath.WalkDir(directory, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } if info.IsDir() { return nil } basename := string(info.Name()) if !strings.HasSuffix(basename, ".conf") { return nil } if strings.Contains(strings.ReplaceAll(path, directory, ""), "/") { return nil } configs = append(configs, Config{ Name: strings.ReplaceAll(basename, ".conf", ""), File: path, }) return nil }) return err } func toggleNotice(notice *widget.Label, isVisible bool) { notice.Hidden = !isVisible notice.Refresh() } func updateNotice(notice *widget.Label, text string, importance widget.Importance, isVisible, isFlash bool) { notice.Text = text notice.Importance = importance notice.Hidden = !isVisible notice.Refresh() if isFlash { go func() { time.Sleep(2 * time.Second) toggleNotice(notice, false) }() } log.Println(text) } func wgUp(config Config, notice *widget.Label) { updateNotice(notice, fmt.Sprintf("Interface is starting"), widget.WarningImportance, true, false) exec.Command("wg-quick", "up", config.Name).Output() updateNotice(notice, fmt.Sprintf("Interface is up"), widget.SuccessImportance, true, true) } func wgDown(config Config, notice *widget.Label) { updateNotice(notice, fmt.Sprintf("Interface is stopping"), widget.WarningImportance, true, false) exec.Command("wg-quick", "down", config.Name).Output() updateNotice(notice, fmt.Sprintf("Interface is down"), widget.SuccessImportance, true, true) go func() { time.Sleep(2 * time.Second) notice.Hidden = true notice.Refresh() }() } func wgRestart(config Config, notice *widget.Label) { wgDown(config, notice) wgUp(config, notice) } func lintConfiguration(configuration string) string { configuration = strings.TrimSpace(configuration) configuration = fmt.Sprintf("%s\n", configuration) return configuration } func updateTextareaConfiguration(textarea *widget.Entry, content string) { textarea.SetText(content) textarea.OnChanged(content) } func updateConfigFile(config Config, content string) error { return os.WriteFile(config.File, []byte(content), 600) } func createTextarea() *widget.Entry { textarea := widget.NewMultiLineEntry() textarea.TextStyle.Monospace = true textarea.OnChanged = func(text string) { textarea.SetMinRowsVisible(strings.Count(text, "\n")) textarea.Refresh() } return textarea } func createColoredButton(label string, importance widget.Importance, callback ButtonCallback) *fyne.Container { button := widget.NewButton(label, callback) button.Importance = importance return container.NewMax(button) } func createNotice() *widget.Label { notice := widget.NewLabel("") notice.TextStyle.Bold = true notice.Importance = widget.LowImportance return notice } func createMargin() *canvas.Text { text := canvas.NewText("", color.Transparent) text.TextSize = 5 return text } func createTab(config Config) *fyne.Container { data, err := os.ReadFile(config.File) if err != nil { log.Fatalln(err) } notice := createNotice() buttonStart := createColoredButton("Start", widget.SuccessImportance, func() { wgUp(config, notice) }) buttonStop := createColoredButton("Stop", widget.DangerImportance, func() { wgDown(config, notice) }) buttonRestart := createColoredButton("Restart", widget.WarningImportance, func() { wgRestart(config, notice) }) top := container.New( layout.NewVBoxLayout(), createMargin(), container.New( layout.NewHBoxLayout(), notice, layout.NewSpacer(), buttonStart, buttonStop, buttonRestart, ), createMargin(), ) textareaConfiguration := createTextarea() updateTextareaConfiguration(textareaConfiguration, string(data)) form := &widget.Form{ Items: []*widget.FormItem{ { Text: "Configuration", Widget: textareaConfiguration, }, }, OnSubmit: func() { toggleNotice(notice, false) configuration := lintConfiguration(textareaConfiguration.Text) updateTextareaConfiguration(textareaConfiguration, configuration) err := updateConfigFile(config, configuration) log.Println("Configuration of", config.Name) if err != nil { updateNotice(notice, fmt.Sprintf("Error while updating: %s", err), widget.DangerImportance, true, false) } else { updateNotice(notice, fmt.Sprintf("Configuration updated"), widget.SuccessImportance, true, true) } }, SubmitText: "Save", } content := container.New( layout.NewVBoxLayout(), top, form, ) return content }