aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui
diff options
context:
space:
mode:
authorAlexander Neumann <alexander.neumann@picos-software.com>2019-04-24 15:29:38 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2019-04-24 15:42:51 +0200
commit48f11e502cc7ee03a9c1dd554c3665883fc90cff (patch)
tree5df163a3e81b948fdf05f26429739d12b41ffc2e /ui
parentui: programmatically compute colors (diff)
downloadwireguard-windows-48f11e502cc7ee03a9c1dd554c3665883fc90cff.tar.xz
wireguard-windows-48f11e502cc7ee03a9c1dd554c3665883fc90cff.zip
ui: use tabs in main window and refactor tunnels ui and log dialog into tab pages
requires https://github.com/lxn/walk/commit/edb74ee350e9585ddd212acad445ec383950f2cc for status image background Signed-off-by: Alexander Neumann <alexander.neumann@picos-software.com> Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'ui')
-rw-r--r--ui/logpage.go (renamed from ui/logdialog.go)108
-rw-r--r--ui/logview.go128
-rw-r--r--ui/manage_tunnels.go398
-rw-r--r--ui/tray.go10
-rw-r--r--ui/tunnelspage.go403
5 files changed, 478 insertions, 569 deletions
diff --git a/ui/logdialog.go b/ui/logpage.go
index a2f23903..4f39ae87 100644
--- a/ui/logdialog.go
+++ b/ui/logpage.go
@@ -17,104 +17,94 @@ import (
"golang.zx2c4.com/wireguard/windows/ringlogger"
)
-func runLogDialog(owner walk.Form, logger *ringlogger.Ringlogger) {
- dlg := &LogDialog{logger: logger}
- dlg.model = newLogModel(dlg, logger)
- defer func() {
- dlg.model.quit <- true
- }()
+func NewLogPage(logger *ringlogger.Ringlogger) (*LogPage, error) {
+ lp := &LogPage{logger: logger}
var disposables walk.Disposables
defer disposables.Treat()
- showError := func(err error) bool {
- if err == nil {
- return false
- }
-
- walk.MsgBox(owner, "Viewing log dialog failed", err.Error(), walk.MsgBoxIconError)
-
- return true
- }
-
var err error
- if dlg.Dialog, err = walk.NewDialog(owner); showError(err) {
- return
+ if lp.TabPage, err = walk.NewTabPage(); err != nil {
+ return nil, err
}
- disposables.Add(dlg)
+ disposables.Add(lp)
- dlg.SetTitle("WireGuard Log")
- dlg.SetLayout(walk.NewVBoxLayout())
- dlg.Layout().SetMargins(walk.Margins{18, 18, 18, 18})
- dlg.SetMinMaxSize(walk.Size{600, 400}, walk.Size{})
+ lp.Disposing().Attach(func() {
+ lp.model.quit <- true
+ })
- if dlg.logView, err = walk.NewTableView(dlg); showError(err) {
- return
+ lp.SetTitle("Log")
+ lp.SetLayout(walk.NewVBoxLayout())
+ lp.Layout().SetMargins(walk.Margins{18, 18, 18, 18})
+
+ if lp.logView, err = walk.NewTableView(lp); err != nil {
+ return nil, err
}
- dlg.logView.SetAlternatingRowBGColor(walk.Color(win.GetSysColor(win.COLOR_BTNFACE)))
- dlg.logView.SetLastColumnStretched(true)
+ lp.logView.SetAlternatingRowBGColor(walk.Color(win.GetSysColor(win.COLOR_BTNFACE)))
+ lp.logView.SetLastColumnStretched(true)
stampCol := walk.NewTableViewColumn()
stampCol.SetName("Stamp")
stampCol.SetTitle("Time")
stampCol.SetFormat("2006-01-02 15:04:05.000")
stampCol.SetWidth(150)
- dlg.logView.Columns().Add(stampCol)
+ lp.logView.Columns().Add(stampCol)
msgCol := walk.NewTableViewColumn()
msgCol.SetName("Line")
msgCol.SetTitle("Log message")
- dlg.logView.Columns().Add(msgCol)
+ lp.logView.Columns().Add(msgCol)
- dlg.logView.SetModel(dlg.model)
- dlg.scrollToBottom()
+ lp.model = newLogModel(lp, logger)
+ lp.logView.SetModel(lp.model)
- buttonsContainer, err := walk.NewComposite(dlg)
+ buttonsContainer, err := walk.NewComposite(lp)
+ if err != nil {
+ return nil, err
+ }
buttonsContainer.SetLayout(walk.NewHBoxLayout())
buttonsContainer.Layout().SetMargins(walk.Margins{0, 12, 0, 0})
- saveButton, err := walk.NewPushButton(buttonsContainer)
- saveButton.SetText("Save")
- saveButton.Clicked().Attach(dlg.onSaveButtonClicked)
-
walk.NewHSpacer(buttonsContainer)
- closeButton, err := walk.NewPushButton(buttonsContainer)
- closeButton.SetText("Close")
- closeButton.Clicked().Attach(dlg.Accept)
-
- dlg.SetDefaultButton(closeButton)
- dlg.SetCancelButton(closeButton)
+ saveButton, err := walk.NewPushButton(buttonsContainer)
+ if err != nil {
+ return nil, err
+ }
+ saveButton.SetText("Save")
+ saveButton.Clicked().Attach(lp.onSaveButtonClicked)
disposables.Spare()
- dlg.Run()
+ return lp, nil
}
-type LogDialog struct {
- *walk.Dialog
+type LogPage struct {
+ *walk.TabPage
logView *walk.TableView
logger *ringlogger.Ringlogger
model *logModel
}
-func (dlg *LogDialog) isAtBottom() bool {
- return dlg.logView.ItemVisible(len(dlg.model.items) - 1)
+func (lp *LogPage) isAtBottom() bool {
+ return lp.logView.ItemVisible(len(lp.model.items) - 1)
}
-func (dlg *LogDialog) scrollToBottom() {
- dlg.logView.EnsureItemVisible(len(dlg.model.items) - 1)
+func (lp *LogPage) scrollToBottom() {
+ lp.logView.EnsureItemVisible(len(lp.model.items) - 1)
}
-func (dlg *LogDialog) onSaveButtonClicked() {
+func (lp *LogPage) onSaveButtonClicked() {
fd := walk.FileDialog{
Filter: "Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
FilePath: fmt.Sprintf("wireguard-log-%s.txt", time.Now().Format("2006-01-02T150405")),
Title: "Export log to file",
}
- if ok, _ := fd.ShowSave(dlg); !ok {
+ form := lp.Form()
+
+ if ok, _ := fd.ShowSave(form); !ok {
return
}
@@ -123,8 +113,8 @@ func (dlg *LogDialog) onSaveButtonClicked() {
fd.FilePath = fd.FilePath + extensions[fd.FilterIndex-1]
}
- writeFileWithOverwriteHandling(dlg, fd.FilePath, func(file *os.File) error {
- if _, err := dlg.logger.WriteTo(file); err != nil {
+ writeFileWithOverwriteHandling(form, fd.FilePath, func(file *os.File) error {
+ if _, err := lp.logger.WriteTo(file); err != nil {
return fmt.Errorf("exportLog: Ringlogger.WriteTo failed: %v", err)
}
@@ -134,14 +124,14 @@ func (dlg *LogDialog) onSaveButtonClicked() {
type logModel struct {
walk.ReflectTableModelBase
- dlg *LogDialog
+ lp *LogPage
quit chan bool
logger *ringlogger.Ringlogger
items []ringlogger.FollowLine
}
-func newLogModel(dlg *LogDialog, logger *ringlogger.Ringlogger) *logModel {
- mdl := &logModel{dlg: dlg, quit: make(chan bool), logger: logger}
+func newLogModel(lp *LogPage, logger *ringlogger.Ringlogger) *logModel {
+ mdl := &logModel{lp: lp, quit: make(chan bool), logger: logger}
var lastCursor uint32
mdl.items, lastCursor = logger.FollowFromCursor(ringlogger.CursorAll)
@@ -167,14 +157,14 @@ func newLogModel(dlg *LogDialog, logger *ringlogger.Ringlogger) *logModel {
lastCursor = cursor
lastStamp = stamp
- mdl.dlg.Synchronize(func() {
- isAtBottom := mdl.dlg.isAtBottom()
+ mdl.lp.Synchronize(func() {
+ isAtBottom := mdl.lp.isAtBottom()
mdl.items = items
mdl.PublishRowsReset()
if isAtBottom {
- mdl.dlg.scrollToBottom()
+ mdl.lp.scrollToBottom()
}
})
}
diff --git a/ui/logview.go b/ui/logview.go
deleted file mode 100644
index e7a8a237..00000000
--- a/ui/logview.go
+++ /dev/null
@@ -1,128 +0,0 @@
-/* SPDX-License-Identifier: MIT
- *
- * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
- */
-
-package ui
-
-import (
- "errors"
- "fmt"
- "github.com/lxn/walk"
- "github.com/lxn/win"
- "golang.zx2c4.com/wireguard/windows/ringlogger"
- "strings"
- "syscall"
- "time"
- "unsafe"
-)
-
-type LogView struct {
- walk.WidgetBase
- logChan chan string
-}
-
-const (
- TEM_APPENDTEXT = win.WM_USER + 6
-)
-
-func NewLogView(parent walk.Container, rl *ringlogger.Ringlogger) (*LogView, error) {
- lc := make(chan string, 1024)
- lv := &LogView{logChan: lc}
-
- if err := walk.InitWidget(
- lv,
- parent,
- "EDIT",
- win.WS_TABSTOP|win.WS_VISIBLE|win.WS_VSCROLL|win.ES_MULTILINE|win.ES_WANTRETURN,
- win.WS_EX_CLIENTEDGE); err != nil {
- return nil, err
- }
- lv.setReadOnly(true)
- lv.SendMessage(win.EM_SETLIMITTEXT, 4294967295, 0)
-
- go func() {
- var lines []ringlogger.FollowLine
- cursor := ringlogger.CursorAll
- for {
- if lv.IsDisposed() {
- return
- }
- lines, cursor = rl.FollowFromCursor(cursor)
- sb := strings.Builder{}
- for _, line := range lines {
- sb.WriteString(fmt.Sprintf("%s: %s\r\n", line.Stamp.Format("2006-01-02 15:04:05.000000"), strings.ReplaceAll(line.Line, "\n", "\r\n")))
- }
- newLines := sb.String()
- if len(newLines) > 0 {
- lv.AppendText(newLines)
- }
- time.Sleep(300 * time.Millisecond)
- }
- }()
- return lv, nil
-}
-
-func (*LogView) LayoutFlags() walk.LayoutFlags {
- return walk.ShrinkableHorz | walk.ShrinkableVert | walk.GrowableHorz | walk.GrowableVert | walk.GreedyHorz | walk.GreedyVert
-}
-
-func (*LogView) MinSizeHint() walk.Size {
- return walk.Size{20, 12}
-}
-
-func (*LogView) SizeHint() walk.Size {
- return walk.Size{100, 100}
-}
-
-func (lv *LogView) setTextSelection(start, end int) {
- lv.SendMessage(win.EM_SETSEL, uintptr(start), uintptr(end))
-}
-
-func (lv *LogView) textLength() int {
- return int(lv.SendMessage(0x000E, uintptr(0), uintptr(0)))
-}
-
-func (lv *LogView) AppendText(value string) {
- textLength := lv.textLength()
- lv.setTextSelection(textLength, textLength)
- lv.SendMessage(win.EM_REPLACESEL, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(value))))
-}
-
-func (lv *LogView) setReadOnly(readOnly bool) error {
- if 0 == lv.SendMessage(win.EM_SETREADONLY, uintptr(win.BoolToBOOL(readOnly)), 0) {
- return errors.New("fail to call EM_SETREADONLY")
- }
-
- return nil
-}
-
-func (lv *LogView) PostAppendText(value string) {
- lv.logChan <- value
- win.PostMessage(lv.Handle(), TEM_APPENDTEXT, 0, 0)
-}
-
-func (lv *LogView) Write(p []byte) (int, error) {
- lv.PostAppendText(string(p) + "\r\n")
- return len(p), nil
-}
-
-func (lv *LogView) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
- switch msg {
- case win.WM_GETDLGCODE:
- if wParam == win.VK_RETURN {
- return win.DLGC_WANTALLKEYS
- }
-
- return win.DLGC_HASSETSEL | win.DLGC_WANTARROWS | win.DLGC_WANTCHARS
- case TEM_APPENDTEXT:
- select {
- case value := <-lv.logChan:
- lv.AppendText(value)
- default:
- return 0
- }
- }
-
- return lv.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
-}
diff --git a/ui/manage_tunnels.go b/ui/manage_tunnels.go
index 35c067a6..69d90a0f 100644
--- a/ui/manage_tunnels.go
+++ b/ui/manage_tunnels.go
@@ -6,17 +6,8 @@
package ui
import (
- "archive/zip"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- "time"
-
"github.com/lxn/walk"
"github.com/lxn/win"
- "golang.zx2c4.com/wireguard/windows/conf"
"golang.zx2c4.com/wireguard/windows/ringlogger"
"golang.zx2c4.com/wireguard/windows/service"
)
@@ -24,13 +15,10 @@ import (
type ManageTunnelsWindow struct {
*walk.MainWindow
- icon *walk.Icon
- logger *ringlogger.Ringlogger
- tunnelTracker *TunnelTracker
- tunnelsView *TunnelsView
- confView *ConfView
- tunnelAddedPublisher walk.StringEventPublisher
- tunnelDeletedPublisher walk.StringEventPublisher
+ icon *walk.Icon
+ logger *ringlogger.Ringlogger
+ tunnelsPage *TunnelsPage
+ logPage *LogPage
}
func NewManageTunnelsWindow(icon *walk.Icon, logger *ringlogger.Ringlogger) (*ManageTunnelsWindow, error) {
@@ -63,103 +51,23 @@ func NewManageTunnelsWindow(icon *walk.Icon, logger *ringlogger.Ringlogger) (*Ma
// "Close to tray" instead of exiting application
onQuit()
})
- mtw.Starting().Attach(func() {
- mtw.updateConfView()
- win.SetForegroundWindow(mtw.Handle())
- win.BringWindowToTop(mtw.Handle())
- })
-
- splitter, _ := walk.NewHSplitter(mtw)
- splitter.SetSuspended(true)
- defer func() {
- splitter.SetSuspended(false)
- }()
-
- tunnelsContainer, _ := walk.NewComposite(splitter)
- tunnelsContainer.SetLayout(walk.NewVBoxLayout())
-
- splitter.SetFixed(tunnelsContainer, true)
-
- mtw.tunnelsView, _ = NewTunnelsView(tunnelsContainer)
- mtw.tunnelsView.ItemActivated().Attach(mtw.onTunnelsViewItemActivated)
- mtw.tunnelsView.CurrentIndexChanged().Attach(mtw.updateConfView)
-
- // ToolBar actions
- {
- // HACK: Because of https://github.com/lxn/walk/issues/481
- // we need to put the ToolBar into its own Composite.
- toolBarContainer, _ := walk.NewComposite(tunnelsContainer)
- toolBarContainer.SetLayout(walk.NewHBoxLayout())
-
- tunnelsToolBar, _ := walk.NewToolBarWithOrientationAndButtonStyle(toolBarContainer, walk.Horizontal, walk.ToolBarButtonTextOnly)
-
- importAction := walk.NewAction()
- importAction.SetText("Import tunnels from file...")
- importAction.Triggered().Attach(mtw.onImport)
-
- addAction := walk.NewAction()
- addAction.SetText("Add empty tunnel")
- addAction.Triggered().Attach(mtw.onAddTunnel)
-
- viewLogAction := walk.NewAction()
- viewLogAction.SetText("View Log")
- viewLogAction.Triggered().Attach(mtw.onViewLog)
-
- exportTunnelsAction := walk.NewAction()
- exportTunnelsAction.SetText("Export tunnels to zip...")
- exportTunnelsAction.Triggered().Attach(mtw.onExportTunnels)
-
- addMenu, _ := walk.NewMenu()
- mtw.AddDisposable(addMenu)
- addMenu.Actions().Add(addAction)
- addMenu.Actions().Add(importAction)
- addMenuAction, _ := tunnelsToolBar.Actions().AddMenu(addMenu)
- addMenuAction.SetText("➕")
-
- deleteAction := walk.NewAction()
- tunnelsToolBar.Actions().Add(deleteAction)
- deleteAction.SetText("➖")
- deleteAction.Triggered().Attach(mtw.onDelete)
-
- settingsMenu, _ := walk.NewMenu()
- mtw.AddDisposable(settingsMenu)
- settingsMenu.Actions().Add(viewLogAction)
- settingsMenu.Actions().Add(exportTunnelsAction)
- settingsMenuAction, _ := tunnelsToolBar.Actions().AddMenu(settingsMenu)
- settingsMenuAction.SetText("⚙")
- }
+ mtw.VisibleChanged().Attach(func() {
+ if mtw.Visible() {
+ mtw.tunnelsPage.updateConfView()
+ win.SetForegroundWindow(mtw.Handle())
+ win.BringWindowToTop(mtw.Handle())
- currentTunnelContainer, _ := walk.NewComposite(splitter)
- currentTunnelContainer.SetLayout(walk.NewVBoxLayout())
- splitter.Layout().(interface{ SetStretchFactor(walk.Widget, int) error }).SetStretchFactor(currentTunnelContainer, 3)
-
- mtw.confView, _ = NewConfView(currentTunnelContainer)
-
- updateConfViewTicker := time.NewTicker(time.Second)
- mtw.Disposing().Attach(updateConfViewTicker.Stop)
- go func() {
- for range updateConfViewTicker.C {
- mtw.Synchronize(func() {
- mtw.updateConfView()
- })
+ mtw.logPage.scrollToBottom()
}
- }()
-
- controlsContainer, _ := walk.NewComposite(currentTunnelContainer)
- controlsContainer.SetLayout(walk.NewHBoxLayout())
- controlsContainer.Layout().SetMargins(walk.Margins{})
+ })
- walk.NewHSpacer(controlsContainer)
+ tabWidget, _ := walk.NewTabWidget(mtw)
- editTunnel, _ := walk.NewPushButton(controlsContainer)
- editTunnel.SetEnabled(false)
- mtw.tunnelsView.CurrentIndexChanged().Attach(func() {
- editTunnel.SetEnabled(mtw.tunnelsView.CurrentIndex() > -1)
- })
- editTunnel.SetText("Edit")
- editTunnel.Clicked().Attach(mtw.onEditTunnel)
+ mtw.tunnelsPage, _ = NewTunnelsPage()
+ tabWidget.Pages().Add(mtw.tunnelsPage.TabPage)
- mtw.tunnelsView.SetCurrentIndex(0)
+ mtw.logPage, _ = NewLogPage(logger)
+ tabWidget.Pages().Add(mtw.logPage.TabPage)
disposables.Spare()
@@ -167,286 +75,22 @@ func NewManageTunnelsWindow(icon *walk.Icon, logger *ringlogger.Ringlogger) (*Ma
}
func (mtw *ManageTunnelsWindow) TunnelTracker() *TunnelTracker {
- return mtw.tunnelTracker
+ return mtw.tunnelsPage.tunnelTracker
}
func (mtw *ManageTunnelsWindow) SetTunnelTracker(tunnelTracker *TunnelTracker) {
- mtw.tunnelTracker = tunnelTracker
+ mtw.tunnelsPage.tunnelTracker = tunnelTracker
- mtw.confView.SetTunnelTracker(tunnelTracker)
+ mtw.tunnelsPage.confView.SetTunnelTracker(tunnelTracker)
}
func (mtw *ManageTunnelsWindow) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) {
- mtw.tunnelsView.SetTunnelState(tunnel, state)
+ mtw.tunnelsPage.SetTunnelState(tunnel, state)
- icon, err := mtw.tunnelsView.imageProvider.IconWithOverlayForState(mtw.icon, state)
+ icon, err := mtw.tunnelsPage.tunnelsView.imageProvider.IconWithOverlayForState(mtw.icon, state)
if err != nil {
return
}
mtw.SetIcon(icon)
}
-
-func (mtw *ManageTunnelsWindow) updateConfView() {
- if !mtw.Visible() {
- return
- }
-
- mtw.confView.SetTunnel(mtw.tunnelsView.CurrentTunnel())
-}
-
-// importFiles tries to import a list of configurations.
-func (mtw *ManageTunnelsWindow) importFiles(paths []string) {
- type unparsedConfig struct {
- Name string
- Config string
- }
-
- var (
- unparsedConfigs []unparsedConfig
- lastErr error
- )
-
- // Note: other versions of WireGuard start with all .zip files, then all .conf files.
- // To reproduce that if needed, inverse-sort the array.
- for _, path := range paths {
- switch filepath.Ext(path) {
- case ".conf":
- textConfig, err := ioutil.ReadFile(path)
- if err != nil {
- lastErr = err
- continue
- }
- unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(path), ".conf"), Config: string(textConfig)})
- case ".zip":
- // 1 .conf + 1 error .zip edge case?
- r, err := zip.OpenReader(path)
- if err != nil {
- lastErr = err
- continue
- }
-
- for _, f := range r.File {
- if filepath.Ext(f.Name) != ".conf" {
- continue
- }
-
- rc, err := f.Open()
- if err != nil {
- lastErr = err
- continue
- }
- textConfig, err := ioutil.ReadAll(rc)
- rc.Close()
- if err != nil {
- lastErr = err
- continue
- }
- unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(f.Name), ".conf"), Config: string(textConfig)})
- }
-
- r.Close()
- }
- }
-
- if lastErr != nil || unparsedConfigs == nil {
- walk.MsgBox(mtw, "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
- return
- }
-
- var configs []*conf.Config
-
- for _, unparsedConfig := range unparsedConfigs {
- config, err := conf.FromWgQuick(unparsedConfig.Config, unparsedConfig.Name)
- if err != nil {
- lastErr = err
- continue
- }
- service.IPCClientNewTunnel(config)
- configs = append(configs, config)
- }
-
- m, n := len(configs), len(unparsedConfigs)
- switch {
- case n == 1 && m != n:
- walk.MsgBox(mtw, "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
- case n == 1 && m == n:
- // TODO: Select tunnel in the list
- case m == n:
- walk.MsgBox(mtw, "Imported tunnels", fmt.Sprintf("Imported %d tunnels", m), walk.MsgBoxOK)
- case m != n:
- walk.MsgBox(mtw, "Imported tunnels", fmt.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning)
- default:
- panic("unreachable case")
- }
-}
-
-func (mtw *ManageTunnelsWindow) exportTunnels(filePath string) {
- writeFileWithOverwriteHandling(mtw, filePath, func(file *os.File) error {
- writer := zip.NewWriter(file)
-
- for _, tunnel := range mtw.tunnelsView.model.tunnels {
- cfg, err := tunnel.StoredConfig()
- if err != nil {
- return fmt.Errorf("onExportTunnels: tunnel.StoredConfig failed: %v", err)
- }
-
- w, err := writer.Create(tunnel.Name + ".conf")
- if err != nil {
- return fmt.Errorf("onExportTunnels: writer.Create failed: %v", err)
- }
-
- if _, err := w.Write(([]byte)(cfg.ToWgQuick())); err != nil {
- return fmt.Errorf("onExportTunnels: cfg.ToWgQuick failed: %v", err)
- }
- }
-
- return writer.Close()
- })
-}
-
-func (mtw *ManageTunnelsWindow) addTunnel(config *conf.Config) {
- tunnel, err := service.IPCClientNewTunnel(config)
- if err != nil {
- walk.MsgBox(mtw, "Unable to create tunnel", err.Error(), walk.MsgBoxIconError)
- return
- }
-
- model := mtw.tunnelsView.model
- model.tunnels = append(model.tunnels, tunnel)
- model.PublishRowsReset()
- model.Sort(model.SortedColumn(), model.SortOrder())
-
- for i, t := range model.tunnels {
- if t.Name == tunnel.Name {
- mtw.tunnelsView.SetCurrentIndex(i)
- break
- }
- }
-
- mtw.confView.SetTunnel(&tunnel)
-
- mtw.tunnelAddedPublisher.Publish(tunnel.Name)
-}
-
-func (mtw *ManageTunnelsWindow) deleteTunnel(tunnel *service.Tunnel) {
- tunnel.Delete()
-
- model := mtw.tunnelsView.model
-
- for i, t := range model.tunnels {
- if t.Name == tunnel.Name {
- model.tunnels = append(model.tunnels[:i], model.tunnels[i+1:]...)
- model.PublishRowsRemoved(i, i)
- break
- }
- }
-
- mtw.tunnelDeletedPublisher.Publish(tunnel.Name)
-}
-
-func (mtw *ManageTunnelsWindow) TunnelAdded() *walk.StringEvent {
- return mtw.tunnelAddedPublisher.Event()
-}
-
-func (mtw *ManageTunnelsWindow) TunnelDeleted() *walk.StringEvent {
- return mtw.tunnelDeletedPublisher.Event()
-}
-
-// Handlers
-
-func (mtw *ManageTunnelsWindow) onTunnelsViewItemActivated() {
- if mtw.tunnelTracker.InTransition() {
- return
- }
-
- var err error
- var title string
- tunnel := mtw.tunnelsView.CurrentTunnel()
- activeTunnel := mtw.tunnelTracker.ActiveTunnel()
- if tunnel != nil && activeTunnel != nil && tunnel.Name == activeTunnel.Name {
- err, title = mtw.tunnelTracker.DeactivateTunnel(), "Deactivating tunnel failed"
- } else {
- err, title = mtw.tunnelTracker.ActivateTunnel(tunnel), "Activating tunnel failed"
- }
- if err != nil {
- walk.MsgBox(mtw, title, fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError)
- }
-}
-
-func (mtw *ManageTunnelsWindow) onEditTunnel() {
- tunnel := mtw.tunnelsView.CurrentTunnel()
- if tunnel == nil {
- // Misfired event?
- return
- }
-
- if config := runTunnelConfigDialog(mtw, tunnel); config != nil {
- // Delete old one
- mtw.deleteTunnel(tunnel)
-
- // Save new one
- mtw.addTunnel(config)
- }
-}
-
-func (mtw *ManageTunnelsWindow) onAddTunnel() {
- if config := runTunnelConfigDialog(mtw, nil); config != nil {
- // Save new
- mtw.addTunnel(config)
- }
-}
-
-func (mtw *ManageTunnelsWindow) onDelete() {
- currentTunnel := mtw.tunnelsView.CurrentTunnel()
- if currentTunnel == nil {
- // Misfired event?
- return
- }
-
- if walk.DlgCmdNo == walk.MsgBox(
- mtw,
- fmt.Sprintf(`Delete "%s"`, currentTunnel.Name),
- fmt.Sprintf(`Are you sure you want to delete "%s"?`, currentTunnel.Name),
- walk.MsgBoxYesNo|walk.MsgBoxIconWarning) {
- return
- }
-
- mtw.deleteTunnel(currentTunnel)
-
- mtw.tunnelDeletedPublisher.Publish(currentTunnel.Name)
-}
-
-func (mtw *ManageTunnelsWindow) onImport() {
- dlg := walk.FileDialog{
- Filter: "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*",
- Title: "Import tunnel(s) from file...",
- }
-
- if ok, _ := dlg.ShowOpenMultiple(mtw); !ok {
- return
- }
-
- mtw.importFiles(dlg.FilePaths)
-}
-
-func (mtw *ManageTunnelsWindow) onExportTunnels() {
- dlg := walk.FileDialog{
- Filter: "Configuration ZIP Files (*.zip)|*.zip",
- Title: "Export tunnels to zip...",
- }
-
- if ok, _ := dlg.ShowSave(mtw); !ok {
- return
- }
-
- if !strings.HasSuffix(dlg.FilePath, ".zip") {
- dlg.FilePath += ".zip"
- }
-
- mtw.exportTunnels(dlg.FilePath)
-}
-
-func (mtw *ManageTunnelsWindow) onViewLog() {
- runLogDialog(mtw, mtw.logger)
-}
diff --git a/ui/tray.go b/ui/tray.go
index e15d2549..2650530d 100644
--- a/ui/tray.go
+++ b/ui/tray.go
@@ -68,7 +68,7 @@ func (tray *Tray) setup() error {
{separator: true},
{separator: true},
{label: "&Manage tunnels...", handler: tray.mtw.Show, enabled: true},
- {label: "&Import tunnel(s) from file...", handler: tray.mtw.onImport, enabled: true},
+ {label: "&Import tunnel(s) from file...", handler: tray.mtw.tunnelsPage.onImport, enabled: true},
{separator: true},
{label: "&About WireGuard", handler: func() { onAbout(tray.mtw) }, enabled: true},
{label: "&Quit", handler: onQuit, enabled: true},
@@ -98,8 +98,8 @@ func (tray *Tray) setup() error {
tray.addTunnelAction(tunnel.Name)
}
- tray.mtw.TunnelAdded().Attach(tray.addTunnelAction)
- tray.mtw.TunnelDeleted().Attach(tray.removeTunnelAction)
+ tray.mtw.tunnelsPage.TunnelAdded().Attach(tray.addTunnelAction)
+ tray.mtw.tunnelsPage.TunnelDeleted().Attach(tray.removeTunnelAction)
return nil
}
@@ -110,7 +110,7 @@ func (tray *Tray) addTunnelAction(tunnelName string) {
tunnelAction.SetEnabled(true)
tunnelAction.SetCheckable(true)
tunnelAction.Triggered().Attach(func() {
- if activeTunnel := tray.mtw.tunnelTracker.activeTunnel; activeTunnel != nil && activeTunnel.Name == tunnelName {
+ if activeTunnel := tray.mtw.tunnelsPage.tunnelTracker.activeTunnel; activeTunnel != nil && activeTunnel.Name == tunnelName {
tray.onDeactivateTunnel()
} else {
tray.onActivateTunnel(tunnelName)
@@ -148,7 +148,7 @@ func (tray *Tray) SetTunnelState(tunnel *service.Tunnel, state service.TunnelSta
}
func (tray *Tray) SetTunnelStateWithNotification(tunnel *service.Tunnel, state service.TunnelState, showNotifications bool) {
- if icon, err := tray.mtw.tunnelsView.imageProvider.IconWithOverlayForState(tray.icon, state); err == nil {
+ if icon, err := tray.mtw.tunnelsPage.tunnelsView.imageProvider.IconWithOverlayForState(tray.icon, state); err == nil {
tray.SetIcon(icon)
}
diff --git a/ui/tunnelspage.go b/ui/tunnelspage.go
new file mode 100644
index 00000000..ac778b3a
--- /dev/null
+++ b/ui/tunnelspage.go
@@ -0,0 +1,403 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+package ui
+
+import (
+ "archive/zip"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/lxn/walk"
+ "golang.zx2c4.com/wireguard/windows/conf"
+ "golang.zx2c4.com/wireguard/windows/service"
+)
+
+type TunnelsPage struct {
+ *walk.TabPage
+
+ tunnelTracker *TunnelTracker
+ tunnelsView *TunnelsView
+ confView *ConfView
+ tunnelAddedPublisher walk.StringEventPublisher
+ tunnelDeletedPublisher walk.StringEventPublisher
+}
+
+func NewTunnelsPage() (*TunnelsPage, error) {
+ var err error
+
+ var disposables walk.Disposables
+ defer disposables.Treat()
+
+ tp := new(TunnelsPage)
+ if tp.TabPage, err = walk.NewTabPage(); err != nil {
+ return nil, err
+ }
+ disposables.Add(tp)
+
+ tp.SetTitle("Tunnels")
+ tp.SetLayout(walk.NewHBoxLayout())
+
+ tunnelsContainer, _ := walk.NewComposite(tp)
+ tunnelsContainer.SetLayout(walk.NewVBoxLayout())
+
+ tp.tunnelsView, _ = NewTunnelsView(tunnelsContainer)
+ tp.tunnelsView.ItemActivated().Attach(tp.onTunnelsViewItemActivated)
+ tp.tunnelsView.CurrentIndexChanged().Attach(tp.updateConfView)
+
+ // ToolBar actions
+ {
+ // HACK: Because of https://github.com/lxn/walk/issues/481
+ // we need to put the ToolBar into its own Composite.
+ toolBarContainer, _ := walk.NewComposite(tunnelsContainer)
+ toolBarContainer.SetLayout(walk.NewHBoxLayout())
+
+ tunnelsToolBar, _ := walk.NewToolBarWithOrientationAndButtonStyle(toolBarContainer, walk.Horizontal, walk.ToolBarButtonTextOnly)
+
+ importAction := walk.NewAction()
+ importAction.SetText("Import tunnels from file...")
+ importAction.Triggered().Attach(tp.onImport)
+
+ addAction := walk.NewAction()
+ addAction.SetText("Add empty tunnel")
+ addAction.Triggered().Attach(tp.onAddTunnel)
+
+ exportTunnelsAction := walk.NewAction()
+ exportTunnelsAction.SetText("Export tunnels to zip...")
+ exportTunnelsAction.Triggered().Attach(tp.onExportTunnels)
+
+ addMenu, _ := walk.NewMenu()
+ tp.AddDisposable(addMenu)
+ addMenu.Actions().Add(addAction)
+ addMenu.Actions().Add(importAction)
+ addMenuAction, _ := tunnelsToolBar.Actions().AddMenu(addMenu)
+ addMenuAction.SetText("➕")
+
+ deleteAction := walk.NewAction()
+ tunnelsToolBar.Actions().Add(deleteAction)
+ deleteAction.SetText("➖")
+ deleteAction.Triggered().Attach(tp.onDelete)
+
+ settingsMenu, _ := walk.NewMenu()
+ tp.AddDisposable(settingsMenu)
+ settingsMenu.Actions().Add(exportTunnelsAction)
+ settingsMenuAction, _ := tunnelsToolBar.Actions().AddMenu(settingsMenu)
+ settingsMenuAction.SetText("⚙")
+ }
+
+ currentTunnelContainer, _ := walk.NewComposite(tp)
+ currentTunnelContainer.SetLayout(walk.NewVBoxLayout())
+ tp.Layout().(interface{ SetStretchFactor(walk.Widget, int) error }).SetStretchFactor(currentTunnelContainer, 10)
+
+ tp.confView, _ = NewConfView(currentTunnelContainer)
+
+ updateConfViewTicker := time.NewTicker(time.Second)
+ tp.Disposing().Attach(updateConfViewTicker.Stop)
+ go func() {
+ for range updateConfViewTicker.C {
+ tp.Synchronize(func() {
+ tp.updateConfView()
+ })
+ }
+ }()
+
+ controlsContainer, _ := walk.NewComposite(currentTunnelContainer)
+ controlsContainer.SetLayout(walk.NewHBoxLayout())
+ controlsContainer.Layout().SetMargins(walk.Margins{})
+
+ walk.NewHSpacer(controlsContainer)
+
+ editTunnel, _ := walk.NewPushButton(controlsContainer)
+ editTunnel.SetEnabled(false)
+ tp.tunnelsView.CurrentIndexChanged().Attach(func() {
+ editTunnel.SetEnabled(tp.tunnelsView.CurrentIndex() > -1)
+ })
+ editTunnel.SetText("Edit")
+ editTunnel.Clicked().Attach(tp.onEditTunnel)
+
+ tp.tunnelsView.SetCurrentIndex(0)
+
+ disposables.Spare()
+
+ return tp, nil
+}
+
+func (tp *TunnelsPage) TunnelTracker() *TunnelTracker {
+ return tp.tunnelTracker
+}
+
+func (tp *TunnelsPage) SetTunnelTracker(tunnelTracker *TunnelTracker) {
+ tp.tunnelTracker = tunnelTracker
+
+ tp.confView.SetTunnelTracker(tunnelTracker)
+}
+
+func (tp *TunnelsPage) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) {
+ tp.tunnelsView.SetTunnelState(tunnel, state)
+}
+
+func (tp *TunnelsPage) updateConfView() {
+ if !tp.Visible() {
+ return
+ }
+
+ tp.confView.SetTunnel(tp.tunnelsView.CurrentTunnel())
+}
+
+// importFiles tries to import a list of configurations.
+func (tp *TunnelsPage) importFiles(paths []string) {
+ type unparsedConfig struct {
+ Name string
+ Config string
+ }
+
+ var (
+ unparsedConfigs []unparsedConfig
+ lastErr error
+ )
+
+ // Note: other versions of WireGuard start with all .zip files, then all .conf files.
+ // To reproduce that if needed, inverse-sort the array.
+ for _, path := range paths {
+ switch filepath.Ext(path) {
+ case ".conf":
+ textConfig, err := ioutil.ReadFile(path)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(path), ".conf"), Config: string(textConfig)})
+ case ".zip":
+ // 1 .conf + 1 error .zip edge case?
+ r, err := zip.OpenReader(path)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ for _, f := range r.File {
+ if filepath.Ext(f.Name) != ".conf" {
+ continue
+ }
+
+ rc, err := f.Open()
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ textConfig, err := ioutil.ReadAll(rc)
+ rc.Close()
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(f.Name), ".conf"), Config: string(textConfig)})
+ }
+
+ r.Close()
+ }
+ }
+
+ if lastErr != nil || unparsedConfigs == nil {
+ walk.MsgBox(tp.Form(), "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
+ return
+ }
+
+ var configs []*conf.Config
+
+ for _, unparsedConfig := range unparsedConfigs {
+ config, err := conf.FromWgQuick(unparsedConfig.Config, unparsedConfig.Name)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ service.IPCClientNewTunnel(config)
+ configs = append(configs, config)
+ }
+
+ m, n := len(configs), len(unparsedConfigs)
+ switch {
+ case n == 1 && m != n:
+ walk.MsgBox(tp.Form(), "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
+ case n == 1 && m == n:
+ // TODO: Select tunnel in the list
+ case m == n:
+ walk.MsgBox(tp.Form(), "Imported tunnels", fmt.Sprintf("Imported %d tunnels", m), walk.MsgBoxOK)
+ case m != n:
+ walk.MsgBox(tp.Form(), "Imported tunnels", fmt.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning)
+ default:
+ panic("unreachable case")
+ }
+}
+
+func (tp *TunnelsPage) exportTunnels(filePath string) {
+ writeFileWithOverwriteHandling(tp.Form(), filePath, func(file *os.File) error {
+ writer := zip.NewWriter(file)
+
+ for _, tunnel := range tp.tunnelsView.model.tunnels {
+ cfg, err := tunnel.StoredConfig()
+ if err != nil {
+ return fmt.Errorf("onExportTunnels: tunnel.StoredConfig failed: %v", err)
+ }
+
+ w, err := writer.Create(tunnel.Name + ".conf")
+ if err != nil {
+ return fmt.Errorf("onExportTunnels: writer.Create failed: %v", err)
+ }
+
+ if _, err := w.Write(([]byte)(cfg.ToWgQuick())); err != nil {
+ return fmt.Errorf("onExportTunnels: cfg.ToWgQuick failed: %v", err)
+ }
+ }
+
+ return writer.Close()
+ })
+}
+
+func (tp *TunnelsPage) addTunnel(config *conf.Config) {
+ tunnel, err := service.IPCClientNewTunnel(config)
+ if err != nil {
+ walk.MsgBox(tp.Form(), "Unable to create tunnel", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+
+ model := tp.tunnelsView.model
+ model.tunnels = append(model.tunnels, tunnel)
+ model.PublishRowsReset()
+ model.Sort(model.SortedColumn(), model.SortOrder())
+
+ for i, t := range model.tunnels {
+ if t.Name == tunnel.Name {
+ tp.tunnelsView.SetCurrentIndex(i)
+ break
+ }
+ }
+
+ tp.confView.SetTunnel(&tunnel)
+
+ tp.tunnelAddedPublisher.Publish(tunnel.Name)
+}
+
+func (tp *TunnelsPage) deleteTunnel(tunnel *service.Tunnel) {
+ tunnel.Delete()
+
+ model := tp.tunnelsView.model
+
+ for i, t := range model.tunnels {
+ if t.Name == tunnel.Name {
+ model.tunnels = append(model.tunnels[:i], model.tunnels[i+1:]...)
+ model.PublishRowsRemoved(i, i)
+ break
+ }
+ }
+
+ tp.tunnelDeletedPublisher.Publish(tunnel.Name)
+}
+
+func (tp *TunnelsPage) TunnelAdded() *walk.StringEvent {
+ return tp.tunnelAddedPublisher.Event()
+}
+
+func (tp *TunnelsPage) TunnelDeleted() *walk.StringEvent {
+ return tp.tunnelDeletedPublisher.Event()
+}
+
+// Handlers
+
+func (tp *TunnelsPage) onTunnelsViewItemActivated() {
+ if tp.tunnelTracker.InTransition() {
+ return
+ }
+
+ var err error
+ var title string
+ tunnel := tp.tunnelsView.CurrentTunnel()
+ activeTunnel := tp.tunnelTracker.ActiveTunnel()
+ if tunnel != nil && activeTunnel != nil && tunnel.Name == activeTunnel.Name {
+ err, title = tp.tunnelTracker.DeactivateTunnel(), "Deactivating tunnel failed"
+ } else {
+ err, title = tp.tunnelTracker.ActivateTunnel(tunnel), "Activating tunnel failed"
+ }
+ if err != nil {
+ walk.MsgBox(tp.Form(), title, fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError)
+ }
+}
+
+func (tp *TunnelsPage) onEditTunnel() {
+ tunnel := tp.tunnelsView.CurrentTunnel()
+ if tunnel == nil {
+ // Misfired event?
+ return
+ }
+
+ if config := runTunnelConfigDialog(tp.Form(), tunnel); config != nil {
+ // Delete old one
+ tp.deleteTunnel(tunnel)
+
+ // Save new one
+ tp.addTunnel(config)
+ }
+}
+
+func (tp *TunnelsPage) onAddTunnel() {
+ if config := runTunnelConfigDialog(tp.Form(), nil); config != nil {
+ // Save new
+ tp.addTunnel(config)
+ }
+}
+
+func (tp *TunnelsPage) onDelete() {
+ currentTunnel := tp.tunnelsView.CurrentTunnel()
+ if currentTunnel == nil {
+ // Misfired event?
+ return
+ }
+
+ if walk.DlgCmdNo == walk.MsgBox(
+ tp.Form(),
+ fmt.Sprintf(`Delete "%s"`, currentTunnel.Name),
+ fmt.Sprintf(`Are you sure you want to delete "%s"?`, currentTunnel.Name),
+ walk.MsgBoxYesNo|walk.MsgBoxIconWarning) {
+ return
+ }
+
+ tp.deleteTunnel(currentTunnel)
+
+ tp.tunnelDeletedPublisher.Publish(currentTunnel.Name)
+}
+
+func (tp *TunnelsPage) onImport() {
+ dlg := walk.FileDialog{
+ Filter: "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*",
+ Title: "Import tunnel(s) from file...",
+ }
+
+ if ok, _ := dlg.ShowOpenMultiple(tp.Form()); !ok {
+ return
+ }
+
+ tp.importFiles(dlg.FilePaths)
+}
+
+func (tp *TunnelsPage) onExportTunnels() {
+ dlg := walk.FileDialog{
+ Filter: "Configuration ZIP Files (*.zip)|*.zip",
+ Title: "Export tunnels to zip...",
+ }
+
+ if ok, _ := dlg.ShowSave(tp.Form()); !ok {
+ return
+ }
+
+ if !strings.HasSuffix(dlg.FilePath, ".zip") {
+ dlg.FilePath += ".zip"
+ }
+
+ tp.exportTunnels(dlg.FilePath)
+}