golang.zx2c4.com/wireguard/windows@v0.5.4-0.20230123132234-dcc0eb72a04b/ui/logpage.go (about)

     1  /* SPDX-License-Identifier: MIT
     2   *
     3   * Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved.
     4   */
     5  
     6  package ui
     7  
     8  import (
     9  	"fmt"
    10  	"os"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/lxn/walk"
    15  	"golang.zx2c4.com/wireguard/windows/l18n"
    16  	"golang.zx2c4.com/wireguard/windows/ringlogger"
    17  )
    18  
    19  const (
    20  	maxLogLinesDisplayed = 10000
    21  )
    22  
    23  type LogPage struct {
    24  	*walk.TabPage
    25  	logView *walk.TableView
    26  	model   *logModel
    27  }
    28  
    29  func NewLogPage() (*LogPage, error) {
    30  	lp := &LogPage{}
    31  
    32  	var err error
    33  	var disposables walk.Disposables
    34  	defer disposables.Treat()
    35  
    36  	if lp.TabPage, err = walk.NewTabPage(); err != nil {
    37  		return nil, err
    38  	}
    39  	disposables.Add(lp)
    40  
    41  	lp.Disposing().Attach(func() {
    42  		lp.model.quit <- true
    43  	})
    44  
    45  	lp.SetTitle(l18n.Sprintf("Log"))
    46  	lp.SetLayout(walk.NewVBoxLayout())
    47  
    48  	if lp.logView, err = walk.NewTableView(lp); err != nil {
    49  		return nil, err
    50  	}
    51  	lp.logView.SetAlternatingRowBG(true)
    52  	lp.logView.SetLastColumnStretched(true)
    53  	lp.logView.SetGridlines(true)
    54  
    55  	contextMenu, err := walk.NewMenu()
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	lp.logView.AddDisposable(contextMenu)
    60  	copyAction := walk.NewAction()
    61  	copyAction.SetText(l18n.Sprintf("&Copy"))
    62  	copyAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyC})
    63  	copyAction.Triggered().Attach(lp.onCopy)
    64  	contextMenu.Actions().Add(copyAction)
    65  	lp.ShortcutActions().Add(copyAction)
    66  	selectAllAction := walk.NewAction()
    67  	selectAllAction.SetText(l18n.Sprintf("Select &all"))
    68  	selectAllAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyA})
    69  	selectAllAction.Triggered().Attach(lp.onSelectAll)
    70  	contextMenu.Actions().Add(selectAllAction)
    71  	lp.ShortcutActions().Add(selectAllAction)
    72  	saveAction := walk.NewAction()
    73  	saveAction.SetText(l18n.Sprintf("&Save to file…"))
    74  	saveAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyS})
    75  	saveAction.Triggered().Attach(lp.onSave)
    76  	contextMenu.Actions().Add(saveAction)
    77  	lp.ShortcutActions().Add(saveAction)
    78  	lp.logView.SetContextMenu(contextMenu)
    79  	setSelectionStatus := func() {
    80  		copyAction.SetEnabled(len(lp.logView.SelectedIndexes()) > 0)
    81  		selectAllAction.SetEnabled(len(lp.logView.SelectedIndexes()) < len(lp.model.items))
    82  	}
    83  	lp.logView.SelectedIndexesChanged().Attach(setSelectionStatus)
    84  
    85  	stampCol := walk.NewTableViewColumn()
    86  	stampCol.SetName("Stamp")
    87  	stampCol.SetTitle(l18n.Sprintf("Time"))
    88  	stampCol.SetFormat("2006-01-02 15:04:05.000")
    89  	stampCol.SetWidth(140)
    90  	lp.logView.Columns().Add(stampCol)
    91  
    92  	msgCol := walk.NewTableViewColumn()
    93  	msgCol.SetName("Line")
    94  	msgCol.SetTitle(l18n.Sprintf("Log message"))
    95  	lp.logView.Columns().Add(msgCol)
    96  
    97  	lp.model = newLogModel(lp)
    98  	lp.model.RowsReset().Attach(setSelectionStatus)
    99  	lp.logView.SetModel(lp.model)
   100  	setSelectionStatus()
   101  
   102  	buttonsContainer, err := walk.NewComposite(lp)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	buttonsContainer.SetLayout(walk.NewHBoxLayout())
   107  	buttonsContainer.Layout().SetMargins(walk.Margins{})
   108  
   109  	walk.NewHSpacer(buttonsContainer)
   110  
   111  	saveButton, err := walk.NewPushButton(buttonsContainer)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	saveButton.SetText(l18n.Sprintf("&Save"))
   116  	saveButton.Clicked().Attach(lp.onSave)
   117  
   118  	disposables.Spare()
   119  
   120  	return lp, nil
   121  }
   122  
   123  func (lp *LogPage) isAtBottom() bool {
   124  	return len(lp.model.items) == 0 || lp.logView.ItemVisible(len(lp.model.items)-1)
   125  }
   126  
   127  func (lp *LogPage) scrollToBottom() {
   128  	lp.logView.EnsureItemVisible(len(lp.model.items) - 1)
   129  }
   130  
   131  func (lp *LogPage) onCopy() {
   132  	var logLines strings.Builder
   133  	selectedItemIndexes := lp.logView.SelectedIndexes()
   134  	if len(selectedItemIndexes) == 0 {
   135  		return
   136  	}
   137  	for i := 0; i < len(selectedItemIndexes); i++ {
   138  		logItem := lp.model.items[selectedItemIndexes[i]]
   139  		logLines.WriteString(fmt.Sprintf("%s: %s\r\n", logItem.Stamp.Format("2006-01-02 15:04:05.000"), logItem.Line))
   140  	}
   141  	walk.Clipboard().SetText(logLines.String())
   142  }
   143  
   144  func (lp *LogPage) onSelectAll() {
   145  	lp.logView.SetSelectedIndexes([]int{-1})
   146  }
   147  
   148  func (lp *LogPage) onSave() {
   149  	fd := walk.FileDialog{
   150  		Filter:   l18n.Sprintf("Text Files (*.txt)|*.txt|All Files (*.*)|*.*"),
   151  		FilePath: fmt.Sprintf("wireguard-log-%s.txt", time.Now().Format("2006-01-02T150405")),
   152  		Title:    l18n.Sprintf("Export log to file"),
   153  	}
   154  
   155  	form := lp.Form()
   156  
   157  	if ok, _ := fd.ShowSave(form); !ok {
   158  		return
   159  	}
   160  
   161  	if fd.FilterIndex == 1 && !strings.HasSuffix(fd.FilePath, ".txt") {
   162  		fd.FilePath = fd.FilePath + ".txt"
   163  	}
   164  
   165  	writeFileWithOverwriteHandling(form, fd.FilePath, func(file *os.File) error {
   166  		if _, err := ringlogger.Global.WriteTo(file); err != nil {
   167  			return fmt.Errorf("exportLog: Ringlogger.WriteTo failed: %w", err)
   168  		}
   169  
   170  		return nil
   171  	})
   172  }
   173  
   174  type logModel struct {
   175  	walk.ReflectTableModelBase
   176  	lp    *LogPage
   177  	quit  chan bool
   178  	items []ringlogger.FollowLine
   179  }
   180  
   181  func newLogModel(lp *LogPage) *logModel {
   182  	mdl := &logModel{lp: lp, quit: make(chan bool)}
   183  	go func() {
   184  		ticker := time.NewTicker(time.Second)
   185  		cursor := ringlogger.CursorAll
   186  
   187  		for {
   188  			select {
   189  			case <-ticker.C:
   190  				var items []ringlogger.FollowLine
   191  				items, cursor = ringlogger.Global.FollowFromCursor(cursor)
   192  				if len(items) == 0 {
   193  					continue
   194  				}
   195  				mdl.lp.Synchronize(func() {
   196  					isAtBottom := mdl.lp.isAtBottom() && len(lp.logView.SelectedIndexes()) <= 1
   197  
   198  					mdl.items = append(mdl.items, items...)
   199  					if len(mdl.items) > maxLogLinesDisplayed {
   200  						mdl.items = mdl.items[len(mdl.items)-maxLogLinesDisplayed:]
   201  					}
   202  					mdl.PublishRowsReset()
   203  
   204  					if isAtBottom {
   205  						mdl.lp.scrollToBottom()
   206  					}
   207  				})
   208  
   209  			case <-mdl.quit:
   210  				ticker.Stop()
   211  				break
   212  			}
   213  		}
   214  	}()
   215  
   216  	return mdl
   217  }
   218  
   219  func (mdl *logModel) Items() any {
   220  	return mdl.items
   221  }