T3.5: Move display helpers to src/app/format.go

Extract pure display/formatting functions from the GUI:
- StatusText, EventText, DisplayFolder, DisplayArguments,
  DisplaySuccessExitCodes, DisplayRunMode, DisplayInvocation, DisplayIndex.

These have no Fyne dependencies and serve as a shared formatting layer for
the service to use (e.g. StatusText in the tests when mocking the GUI).
The GUI imports them as app.DisplayFolder(...) etc.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-06-19 08:25:37 +03:00
parent a4c93a5122
commit 6e5c42fbed
3 changed files with 102 additions and 77 deletions
+89
View File
@@ -0,0 +1,89 @@
package app
import (
"fmt"
"strings"
"gitea.mixdep.ru/mix/gosentry/src/domain"
)
// StatusText formats a job's current state for display: "Paused" if disabled,
// else its runtime LastState (Ready, Running, Success, etc).
func StatusText(j domain.Job, runtime *domain.JobRuntime) string {
if !j.Enabled {
return "Paused"
}
if runtime == nil {
return ""
}
return runtime.LastState
}
// EventText formats a run record for the History table, showing time, trigger,
// job name, outcome state, detail, and log file (if any).
func EventText(e domain.RunRecord) string {
trigger := e.Trigger
if trigger == "" {
trigger = "Unknown"
}
if e.LogFile != "" {
return fmt.Sprintf("%s %s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail, e.LogFile)
}
return fmt.Sprintf("%s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail)
}
// DisplayFolder formats a job's folder for display: "(No folder)" if empty,
// else the trimmed folder name.
func DisplayFolder(folder string) string {
if strings.TrimSpace(folder) == "" {
return "(No folder)"
}
return strings.TrimSpace(folder)
}
// DisplayArguments formats a job's arguments for display: "(none)" if empty,
// else the trimmed arguments.
func DisplayArguments(arguments string) string {
if strings.TrimSpace(arguments) == "" {
return "(none)"
}
return strings.TrimSpace(arguments)
}
// DisplaySuccessExitCodes formats a job's success exit codes for display:
// "0" (the default) if empty, else the trimmed codes.
func DisplaySuccessExitCodes(codes string) string {
if strings.TrimSpace(codes) == "" {
return "0"
}
return strings.TrimSpace(codes)
}
// DisplayRunMode formats a job's execution mode: "Start only" or
// "Wait for completion".
func DisplayRunMode(job domain.Job) string {
if job.StartOnly {
return "Start only"
}
return "Wait for completion"
}
// DisplayInvocation formats a job's command and arguments for the jobs list,
// joining them with spacing and collapsing newlines in arguments to spaces.
func DisplayInvocation(job domain.Job) string {
if strings.TrimSpace(job.Arguments) == "" {
return job.Command
}
return job.Command + " " + strings.ReplaceAll(strings.TrimSpace(job.Arguments), "\n", " ")
}
// DisplayIndex returns the position of jobIndex in the given slice of indexes,
// or 0 if not found.
func DisplayIndex(indexes []int, jobIndex int) int {
for display, index := range indexes {
if index == jobIndex {
return display
}
}
return 0
}
+12 -76
View File
@@ -211,8 +211,8 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
schedule := newJobDetailLabel(jobs[selected].Schedule)
command := newJobDetailLabel(jobs[selected].Command)
arguments := newJobDetailLabel(jobs[selected].Arguments)
successExitCodes := newJobDetailLabel(displaySuccessExitCodes(jobs[selected].SuccessExitCodes))
runMode := newJobDetailLabel(displayRunMode(jobs[selected]))
successExitCodes := newJobDetailLabel(app.DisplaySuccessExitCodes(jobs[selected].SuccessExitCodes))
runMode := newJobDetailLabel(app.DisplayRunMode(jobs[selected]))
selectedRuntime := runtimeFor(selected)
lastRun := newJobDetailLabel(selectedRuntime.LastRun)
nextRun := newJobDetailLabel(selectedRuntime.NextRun)
@@ -245,7 +245,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
},
func() fyne.CanvasObject { return widget.NewLabel("log") },
func(id widget.ListItemID, item fyne.CanvasObject) {
item.(*widget.Label).SetText(eventText(selectedLogs[id]))
item.(*widget.Label).SetText(app.EventText(selectedLogs[id]))
},
)
@@ -271,12 +271,12 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
current := jobs[selected]
runtime := runtimeFor(selected)
title.SetText(current.Name)
folder.SetText(displayFolder(current.Folder))
folder.SetText(app.DisplayFolder(current.Folder))
schedule.SetText(current.Schedule)
command.SetText(current.Command)
arguments.SetText(displayArguments(current.Arguments))
successExitCodes.SetText(displaySuccessExitCodes(current.SuccessExitCodes))
runMode.SetText(displayRunMode(current))
arguments.SetText(app.DisplayArguments(current.Arguments))
successExitCodes.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes))
runMode.SetText(app.DisplayRunMode(current))
lastRun.SetText(runtime.LastRun)
nextRun.SetText(runtime.NextRun)
state.SetText(runtime.LastState)
@@ -312,8 +312,8 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
name.SetText(current.Name)
// Keep each row compact: folder, schedule, and command are shown in one
// metadata line so the left pane stays useful even with many jobs.
meta.SetText(displayFolder(current.Folder) + " " + current.Schedule + " " + displayInvocation(current))
status.SetText(statusText(current, runtimes[current.ID]))
meta.SetText(app.DisplayFolder(current.Folder) + " " + current.Schedule + " " + app.DisplayInvocation(current))
status.SetText(app.StatusText(current, runtimes[current.ID]))
},
)
list.OnSelected = func(id widget.ListItemID) {
@@ -366,7 +366,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
selected = indexOfID(jobs, created.ID)
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
list.Refresh()
list.Select(displayIndex(filteredJobs, selected))
list.Select(app.DisplayIndex(filteredJobs, selected))
refresh()
})
})
@@ -480,7 +480,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
}
list.Refresh()
if selected >= 0 {
list.Select(displayIndex(filteredJobs, selected))
list.Select(app.DisplayIndex(filteredJobs, selected))
}
refresh()
}, w)
@@ -569,16 +569,6 @@ func (layout minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size)
}
}
func statusText(j job, runtime *domain.JobRuntime) string {
if !j.Enabled {
return "Paused"
}
if runtime == nil {
return ""
}
return runtime.LastState
}
func newEvent(jobID int, jobName string, state string, detail string) event {
// Use the same timestamp shape as command run records so the History tab is
// visually consistent across startup, UI actions, manual runs, and schedules.
@@ -592,17 +582,6 @@ func newEvent(jobID int, jobName string, state string, detail string) event {
}
}
func eventText(e event) string {
trigger := e.Trigger
if trigger == "" {
trigger = "Unknown"
}
if e.LogFile != "" {
return fmt.Sprintf("%s %s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail, e.LogFile)
}
return fmt.Sprintf("%s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail)
}
func collectActivity(jobs []job, runtimes map[int]*domain.JobRuntime) []event {
var events []event
for _, current := range jobs {
@@ -689,49 +668,6 @@ func filterValue(folder string) string {
return strings.TrimSpace(folder)
}
func displayFolder(folder string) string {
if strings.TrimSpace(folder) == "" {
return "(" + noFolder + ")"
}
return strings.TrimSpace(folder)
}
func displayArguments(arguments string) string {
if strings.TrimSpace(arguments) == "" {
return "(none)"
}
return strings.TrimSpace(arguments)
}
func displaySuccessExitCodes(codes string) string {
if strings.TrimSpace(codes) == "" {
return "0"
}
return strings.TrimSpace(codes)
}
func displayRunMode(current job) string {
if current.StartOnly {
return "Start only"
}
return "Wait for completion"
}
func displayInvocation(current job) string {
if strings.TrimSpace(current.Arguments) == "" {
return current.Command
}
return current.Command + " " + strings.ReplaceAll(strings.TrimSpace(current.Arguments), "\n", " ")
}
func displayIndex(indexes []int, jobIndex int) int {
for display, index := range indexes {
if index == jobIndex {
return display
}
}
return 0
}
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
name := widget.NewEntry()
@@ -751,7 +687,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
arguments.SetText(current.Arguments)
successExitCodes := widget.NewEntry()
successExitCodes.SetPlaceHolder("0")
successExitCodes.SetText(displaySuccessExitCodes(current.SuccessExitCodes))
successExitCodes.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes))
startOnly := widget.NewCheck("Start only, do not wait for exit", nil)
startOnly.SetChecked(current.StartOnly)
enabled := widget.NewCheck("Enabled", nil)