package ui import ( "os/exec" "sync/atomic" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "viewsagent/internal/logging" ) // logWindow 是独立的「运行日志」窗口。 // // 程序是打包成无控制台窗口的桌面应用,日志不再往 cmd 输出, // 想看的时候点工具栏的日志按钮,这个窗口就会单独弹出来。 type logWindow struct { buf *logging.Buffer win fyne.Window list *widget.List lines []string // shown 是界面上已经显示的日志版本号,用原子变量跨 goroutine 传递。 shown atomic.Uint64 // stop 用来结束自动刷新的后台协程。 stop chan struct{} } // newLogWindow 创建运行日志窗口。logDir 是日志文件所在目录。 func newLogWindow(a fyne.App, buf *logging.Buffer, logDir string) *logWindow { w := &logWindow{buf: buf} w.win = a.NewWindow("运行日志") w.win.Resize(fyne.NewSize(900, 560)) // 点关闭只是隐藏窗口,方便下次再打开。 w.win.SetCloseIntercept(func() { w.stopTicker() w.win.Hide() }) w.win.SetContent(w.buildContent(logDir)) return w } // buildContent 组装窗口内容:上面一排按钮,下面是日志列表。 func (w *logWindow) buildContent(logDir string) fyne.CanvasObject { w.list = widget.NewList( func() int { return len(w.lines) }, func() fyne.CanvasObject { l := widget.NewLabel("") l.TextStyle = fyne.TextStyle{Monospace: true} l.Truncation = fyne.TextTruncateOff return l }, func(id widget.ListItemID, obj fyne.CanvasObject) { l, ok := obj.(*widget.Label) if !ok { return } if id < 0 || id >= len(w.lines) { l.SetText("") return } l.SetText(w.lines[id]) }, ) refreshBtn := widget.NewButtonWithIcon("刷新", theme.ViewRefreshIcon(), func() { w.refresh() }) copyBtn := widget.NewButtonWithIcon("复制全部", theme.ContentCopyIcon(), func() { w.copyAll() }) clearBtn := widget.NewButtonWithIcon("清空", theme.DeleteIcon(), func() { w.clear() }) dirBtn := widget.NewButtonWithIcon("打开日志目录", theme.FolderOpenIcon(), func() { openDir(logDir) }) bar := container.NewHBox(refreshBtn, copyBtn, clearBtn, layout.NewSpacer(), dirBtn) hint := widget.NewLabel("日志同时写入该目录下的 app.log 文件,重启程序后仍可查看。") hint.Importance = widget.LowImportance hint.Wrapping = fyne.TextWrapWord return container.NewBorder(bar, hint, nil, nil, w.list) } // show 显示日志窗口(已经显示时只刷新内容并置前)。 func (w *logWindow) show() { if w.win == nil { return } w.refresh() w.win.Show() w.win.RequestFocus() w.startTicker() } // refresh 把最新日志刷到列表里,并滚动到底部。 // 只能在 UI 线程调用。 func (w *logWindow) refresh() { if w.list == nil { return } w.lines = w.buf.Lines() w.shown.Store(w.buf.Seq()) w.list.Refresh() w.list.ScrollToBottom() } // copyAll 把全部日志复制到剪贴板。 func (w *logWindow) copyAll() { text := w.buf.Text() if text == "" { return } fyne.CurrentApp().Clipboard().SetContent(text) } // clear 清空内存里的日志显示(磁盘上的文件不受影响)。 func (w *logWindow) clear() { w.buf.Clear() w.refresh() } // startTicker 启动自动刷新:每秒检查一次有没有新日志。 func (w *logWindow) startTicker() { if w.stop != nil { return } stop := make(chan struct{}) w.stop = stop go func() { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-stop: return case <-t.C: if w.buf.Seq() == w.shown.Load() { continue } fyne.Do(w.refresh) } } }() } // stopTicker 停止自动刷新,窗口隐藏时调用。 func (w *logWindow) stopTicker() { if w.stop == nil { return } close(w.stop) w.stop = nil } // close 停掉后台刷新,程序退出时调用。 // 退出后 fyne.Do 不再走主线程队列,回调会直接执行, // 这时再去刷界面已经没有意义了。 func (w *logWindow) close() { if w == nil { return } w.stopTicker() } // openDir 用资源管理器打开目录。 func openDir(dir string) { if dir == "" { return } _ = exec.Command("explorer", dir).Start() }