diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md index d9b56f2..55176cd 100644 --- a/docs/REFACTORING.md +++ b/docs/REFACTORING.md @@ -264,7 +264,7 @@ Track progress here. Mark tasks complete as they land and pass review. - [x] T3.2 — Add `src/app/events.go`; Event types + Observer - [x] T3.3 — Add state-mutating operations to service - [x] T3.4 — Convert `scheduler` to use service; inject Clock -- [ ] T3.5 — Move display helpers to `src/app/format.go` +- [x] T3.5 — Move display helpers to `src/app/format.go` - [ ] T3.6 — Add `src/app` unit tests (no Fyne) ### Phase 4 — Carve up the GUI diff --git a/src/app/format.go b/src/app/format.go new file mode 100644 index 0000000..b44989e --- /dev/null +++ b/src/app/format.go @@ -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 +} diff --git a/src/gui/app.go b/src/gui/app.go index 4c0a131..7b8ee83 100644 --- a/src/gui/app.go +++ b/src/gui/app.go @@ -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)