3 Commits

Author SHA1 Message Date
messypy
f9968895ac Allow smaller GUI font sizes
All checks were successful
build-windows-exe / build (push) Successful in 1m31s
2026-07-27 23:57:57 +09:00
messypy
452c7c59b6 Include full day details in history export
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:53:00 +09:00
messypy
b059c91388 Enable custom window control buttons
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:45:09 +09:00
3 changed files with 216 additions and 23 deletions

View File

@@ -591,9 +591,11 @@ func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (str
}
func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
events := historyEventsFromSnapshot()
visits := historyVisitSource()
rows := append([]historyDetailRow(nil), req.Rows...)
if len(rows) == 0 {
rows = historyVisitsForDayFromData(nil, req.Day, historyVisitSource(), historyEventsFromSnapshot(), req.Query)
rows = historyVisitsForDayFromData(nil, req.Day, visits, events, req.Query)
}
if len(req.Selected) > 0 {
filtered := rows[:0]
@@ -679,6 +681,9 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
}
}
}
appendExportDayVisits(&b, req.Day, visits, req.Query)
appendExportDayEvents(&b, req.Day, events, req.Query)
appendExportGuestSnapshot(&b, req.Day, req.Query)
exportDir := runtimeDir()
if strings.TrimSpace(req.ExportDir) != "" {
exportDir = normalizeHistoryExportDir(req.ExportDir)
@@ -696,6 +701,153 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
return path, nil
}
func appendExportDayVisits(b *strings.Builder, day time.Time, visits []historyVisitRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyVisitRecord, 0, len(visits))
for _, visit := range visits {
start := visit.Start
if start.IsZero() {
continue
}
end := visit.End
if end.IsZero() {
end = time.Now()
}
if !start.Before(dayEnd) || !end.After(dayStart) {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(visit.WorldLabel) && !q.Pattern.MatchString(visit.WorldID) && !q.Pattern.MatchString(visit.InstanceID) {
continue
}
matched = append(matched, visit)
}
sort.SliceStable(matched, func(i, j int) bool {
return matched[i].Start.Before(matched[j].Start)
})
b.WriteString("\nAll visits:\n")
if len(matched) == 0 {
b.WriteString("(no visits)\n")
return
}
for _, visit := range matched {
b.WriteString("- ")
b.WriteString(historyTimeRangeLabel(visit.Start, visit.End, visit.Current))
b.WriteString(" ")
b.WriteString(strings.TrimSpace(visit.WorldLabel))
if strings.TrimSpace(visit.WorldID) != "" {
b.WriteString(" world_id=")
b.WriteString(strings.TrimSpace(visit.WorldID))
}
if strings.TrimSpace(visit.InstanceID) != "" {
b.WriteString(" instance_id=")
b.WriteString(strings.TrimSpace(visit.InstanceID))
}
b.WriteString("\n")
}
}
func appendExportDayEvents(b *strings.Builder, day time.Time, events []historyEventRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyEventRecord, 0, len(events))
for _, ev := range events {
if ev.At.IsZero() || ev.At.Before(dayStart) || !ev.At.Before(dayEnd) {
continue
}
raw := fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name)
if q.Pattern != nil && !q.Pattern.MatchString(ev.Name) && !q.Pattern.MatchString(raw) {
continue
}
matched = append(matched, ev)
}
sort.SliceStable(matched, func(i, j int) bool {
if matched[i].At.Equal(matched[j].At) {
return strings.ToLower(matched[i].Name) < strings.ToLower(matched[j].Name)
}
return matched[i].At.Before(matched[j].At)
})
b.WriteString("\nAll join/leave events:\n")
if len(matched) == 0 {
b.WriteString("(no events)\n")
return
}
for _, ev := range matched {
b.WriteString("- ")
b.WriteString(ev.At.Format("15:04"))
b.WriteString(" ")
b.WriteString(ev.Kind)
b.WriteString(" ")
b.WriteString(strings.TrimSpace(ev.Name))
if ev.Raw != "" {
b.WriteString(" | ")
b.WriteString(strings.TrimSpace(ev.Raw))
}
b.WriteString("\n")
}
}
func appendExportGuestSnapshot(b *strings.Builder, day time.Time, q historyQuery) {
if b == nil {
return
}
guests := readGuestSnapshot()
dayStart, dayEnd := historyDayBounds(day)
type guestLine struct {
name string
present bool
join time.Time
leave time.Time
}
lines := make([]guestLine, 0, len(guests))
for _, guest := range guests {
name := strings.TrimSpace(guest.Name)
if name == "" {
continue
}
inDay := guest.Present ||
(!guest.LastJoin.IsZero() && !guest.LastJoin.Before(dayStart) && guest.LastJoin.Before(dayEnd)) ||
(!guest.LastLeave.IsZero() && !guest.LastLeave.Before(dayStart) && guest.LastLeave.Before(dayEnd))
if !inDay {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(name) {
continue
}
lines = append(lines, guestLine{name: name, present: guest.Present, join: guest.LastJoin, leave: guest.LastLeave})
}
sort.SliceStable(lines, func(i, j int) bool {
return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name)
})
b.WriteString("\nGuest snapshot:\n")
if len(lines) == 0 {
b.WriteString("(no guests)\n")
return
}
for _, line := range lines {
b.WriteString("- ")
if line.present {
b.WriteString("present ")
} else {
b.WriteString("left ")
}
b.WriteString(line.name)
if !line.join.IsZero() {
b.WriteString(" join=")
b.WriteString(line.join.Format("15:04"))
}
if !line.leave.IsZero() {
b.WriteString(" leave=")
b.WriteString(line.leave.Format("15:04"))
}
b.WriteString("\n")
}
}
func cloneHistorySelection(selected map[string]bool) map[string]bool {
if len(selected) == 0 {
return nil

View File

@@ -121,6 +121,12 @@ const (
const translateTabEnabled = false
const (
defaultGUIFontSize = 14
minGUIFontSize = 8
maxGUIFontSize = 30
)
var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`)
var worldIdPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+)`)
@@ -140,6 +146,7 @@ var hwndNoZOrder uintptr = 0x0004
var hwndNoSize uintptr = 0x0001
var hwndNoActivate uintptr = 0x0010
var hwndShowWindow uintptr = 0x0040
var swMinimize uintptr = 6
var swRestore uintptr = 9
var swHideControl uintptr = 0
var swShowControl uintptr = 5
@@ -449,7 +456,7 @@ func runNativeGUI(initialTab string) error {
if guiCfg.FontSize > 0 {
app.fontSize = guiCfg.FontSize
} else {
app.fontSize = 18
app.fontSize = defaultGUIFontSize
}
app.refreshIntervalValue = guiCfg.RefreshIntervalValue
if app.refreshIntervalValue <= 0 {
@@ -748,8 +755,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) {
return 0
}
if app.fontSize > 10 {
if app.fontSize > minGUIFontSize {
app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
oldFont := app.hFont
app.hFont = createAppFont(app.fontSize)
applyFont(&app)
@@ -763,8 +773,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) {
return 0
}
if app.fontSize < 30 {
if app.fontSize < maxGUIFontSize {
app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
oldFont := app.hFont
app.hFont = createAppFont(app.fontSize)
applyFont(&app)
@@ -794,8 +807,25 @@ func runNativeGUI(initialTab string) error {
width = rc.Right
}
}
if x >= width-42 && x <= width-22 && y >= 12 && y <= 32 {
guiLog("close button ignored to keep GUI visible")
switch {
case pointInHeaderDot(x, y, width-92, 22):
guiLog("window control minimize")
if showWindowProc != nil {
showWindowProc.Call(hwnd, swMinimize)
}
return 0
case pointInHeaderDot(x, y, width-62, 22):
guiLog("window control topmost toggle")
app.topMost = !app.topMost
saveGUISettings(&app)
applyTopMost(hwnd, &app)
if invalidateRectProc != nil {
invalidateRectProc.Call(hwnd, 0, 1)
}
return 0
case pointInHeaderDot(x, y, width-32, 22):
guiLog("window control close")
postQuitMessage.Call(0)
return 0
}
releaseCapture.Call()
@@ -898,7 +928,8 @@ func runNativeGUI(initialTab string) error {
postQuitMessage.Call(0)
return 0
case wmCloseGUI:
guiLog("wmClose ignored to keep GUI visible")
guiLog("wmClose received")
postQuitMessage.Call(0)
return 0
}
ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam)
@@ -1414,7 +1445,7 @@ func applyJoinLogFont(app *guiApp, presentCount int) {
size = app.fontSize
}
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinHeadlineHFont != 0 &&
app.joinFontSize == size && app.joinBoldFontSize == size && app.joinHeadlineSize == size+2 {
@@ -1489,7 +1520,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
return
}
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinFontSize == size && app.joinBoldFontSize == size {
return
@@ -1519,7 +1550,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
size := baseSize
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if lineCount <= 0 {
lineCount = 1
@@ -1527,14 +1558,14 @@ func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
if availableHeight <= 0 {
availableHeight = 300
}
for size > 8 {
for size > minGUIFontSize {
if joinPaneContentHeight(size, lineCount) <= availableHeight {
break
}
size -= 2
}
if size < 8 {
size = 8
if size < minGUIFontSize {
size = minGUIFontSize
}
return size
}
@@ -1732,6 +1763,10 @@ func paintMainWindow(hwnd uintptr, app *guiApp) uintptr {
return 0
}
func pointInHeaderDot(x, y, cx, cy int32) bool {
return x >= cx-11 && x <= cx+11 && y >= cy-11 && y <= cy+11
}
func paintFooter(hdc uintptr, rc winRect, app *guiApp) {
if app == nil {
return
@@ -1949,15 +1984,21 @@ func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowTe
applyRefreshTimer(app.hwnd, app)
}
case pointInRect(x, y, settingsFontDownRect()):
if app.fontSize > 10 {
if app.fontSize > minGUIFontSize {
app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
app.hFont = createAppFont(app.fontSize)
applyFont(app)
saveGUISettings(app)
}
case pointInRect(x, y, settingsFontUpRect()):
if app.fontSize < 30 {
if app.fontSize < maxGUIFontSize {
app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
app.hFont = createAppFont(app.fontSize)
applyFont(app)
saveGUISettings(app)
@@ -3287,7 +3328,7 @@ func formatDurationShort(d time.Duration) string {
func joinLogFontSize(baseSize, presentCount int) int {
size := baseSize
if size <= 0 {
size = 18
size = defaultGUIFontSize
}
if presentCount <= 32 {
return size
@@ -3300,8 +3341,8 @@ func joinLogFontSize(baseSize, presentCount int) int {
case presentCount >= 33:
size -= 2
}
if size < 8 {
size = 8
if size < minGUIFontSize {
size = minGUIFontSize
}
return size
}
@@ -3756,7 +3797,7 @@ func loadGUISettings() config.GUIConfig {
if err != nil || cfg == nil {
return config.GUIConfig{
TopMost: false,
FontSize: 18,
FontSize: defaultGUIFontSize,
RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true,
@@ -3765,7 +3806,7 @@ func loadGUISettings() config.GUIConfig {
}
}
if cfg.GUI.FontSize <= 0 {
cfg.GUI.FontSize = 18
cfg.GUI.FontSize = defaultGUIFontSize
}
cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalSettings(cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit)
cfg.GUI.HistoryRegex = strings.TrimSpace(cfg.GUI.HistoryRegex)
@@ -4116,7 +4157,7 @@ func formatRightPane(tab string, state map[string]any, worldLabel string, curren
b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int)
if fontSize == 0 {
fontSize = 18
fontSize = defaultGUIFontSize
}
b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ")
@@ -4154,7 +4195,7 @@ func formatSettingsPane(state map[string]any) string {
b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int)
if fontSize == 0 {
fontSize = 18
fontSize = defaultGUIFontSize
}
b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ")

View File

@@ -53,7 +53,7 @@ func Load(path string) (*Config, error) {
cfg := &Config{
OSC: OSCConfig{Host: "127.0.0.1", Port: 9001},
GUI: GUIConfig{
FontSize: 18,
FontSize: 14,
RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true,