Files
2026-10-28 17:39:33 +08:00

173 lines
4.2 KiB
Go

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()
}