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 }