summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-20 21:14:11 +0200
committerPaul Buetow <paul@buetow.org>2026-02-20 21:14:11 +0200
commit0d3e15caf1d09e94e66946bdbc98001465d7f9f7 (patch)
tree6c72d1e20fc87e1d3e08644c02f1233b3033d6d7 /internal
parent59e86c9fd39308bc6b632e02ecf4d37265dabc91 (diff)
refactor: apply Go best practices and remove obsolete files
- Reorganize types.go: place types and constructors before methods - Simplify main.go: improve flag handling and reduce code complexity - Extract processRecordsFile() in aggregate.go: reduce function size, ensure immediate defer - Change Reporter receivers to value semantics for read-only operations - Fix wordWrap() function: properly account for line length with leading space - Remove guprecords.raku: Go implementation is the primary version now - Remove compare-with-raku.sh: no longer needed - Update README.md and .gitignore accordingly All tests pass: go test ./... and ./goprecords test Amp-Thread-ID: https://ampcode.com/threads/T-019c7c73-58f9-7516-958d-f30eb17a3bff Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/goprecords/aggregate.go94
-rw-r--r--internal/goprecords/report.go26
-rw-r--r--internal/goprecords/types.go134
3 files changed, 132 insertions, 122 deletions
diff --git a/internal/goprecords/aggregate.go b/internal/goprecords/aggregate.go
index 3fb9144..ba446cb 100644
--- a/internal/goprecords/aggregate.go
+++ b/internal/goprecords/aggregate.go
@@ -61,53 +61,61 @@ func (ag *Aggregator) Aggregate(ctx context.Context) (*Aggregates, error) {
return nil, fmt.Errorf("last kernel %s: %w", path, err)
}
out.Host[host] = NewHostAggregate(host, lastKernel)
- f, err := os.Open(path)
- if err != nil {
- return nil, fmt.Errorf("open %s: %w", path, err)
+ if err := processRecordsFile(ctx, path, host, out); err != nil {
+ return nil, err
}
- defer f.Close()
- sc := bufio.NewScanner(f)
- for sc.Scan() {
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- default:
- }
- line := strings.TrimSpace(sc.Text())
- if line == "" {
- continue
- }
- parts := strings.SplitN(line, ":", 3)
- if len(parts) != 3 {
- continue
- }
- uptime, _ := strconv.ParseUint(parts[0], 10, 64)
- bootTime, _ := strconv.ParseUint(parts[1], 10, 64)
- osStr := parts[2]
- uname := osStr
- if i := strings.Index(osStr, " "); i > 0 {
- uname = osStr[:i]
- }
- osMajor := uname + " "
- rest := osStr
- if i := strings.Index(osStr, " "); i >= 0 {
- rest = osStr[i+1:]
- }
- if j := strings.Index(rest, "."); j >= 0 {
- osMajor += rest[:j] + "..."
- } else {
- osMajor += rest + "..."
- }
- out.Host[host].AddRecord(uptime, bootTime)
- getOrNewAggregate(out.Kernel, osStr).AddRecord(uptime, bootTime)
- getOrNewAggregate(out.KernelName, uname).AddRecord(uptime, bootTime)
- getOrNewAggregate(out.KernelMajor, osMajor).AddRecord(uptime, bootTime)
+ }
+ return out, nil
+}
+
+func processRecordsFile(ctx context.Context, path, host string, out *Aggregates) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("open %s: %w", path, err)
+ }
+ defer f.Close()
+
+ sc := bufio.NewScanner(f)
+ for sc.Scan() {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
}
- if err := sc.Err(); err != nil {
- return nil, fmt.Errorf("scan %s: %w", path, err)
+ line := strings.TrimSpace(sc.Text())
+ if line == "" {
+ continue
+ }
+ parts := strings.SplitN(line, ":", 3)
+ if len(parts) != 3 {
+ continue
}
+ uptime, _ := strconv.ParseUint(parts[0], 10, 64)
+ bootTime, _ := strconv.ParseUint(parts[1], 10, 64)
+ osStr := parts[2]
+ uname := osStr
+ if i := strings.Index(osStr, " "); i > 0 {
+ uname = osStr[:i]
+ }
+ osMajor := uname + " "
+ rest := osStr
+ if i := strings.Index(osStr, " "); i >= 0 {
+ rest = osStr[i+1:]
+ }
+ if j := strings.Index(rest, "."); j >= 0 {
+ osMajor += rest[:j] + "..."
+ } else {
+ osMajor += rest + "..."
+ }
+ out.Host[host].AddRecord(uptime, bootTime)
+ getOrNewAggregate(out.Kernel, osStr).AddRecord(uptime, bootTime)
+ getOrNewAggregate(out.KernelName, uname).AddRecord(uptime, bootTime)
+ getOrNewAggregate(out.KernelMajor, osMajor).AddRecord(uptime, bootTime)
}
- return out, nil
+ if err := sc.Err(); err != nil {
+ return fmt.Errorf("scan %s: %w", path, err)
+ }
+ return nil
}
func getOrNewAggregate(m map[string]*Aggregate, name string) *Aggregate {
diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go
index af61b29..01ca570 100644
--- a/internal/goprecords/report.go
+++ b/internal/goprecords/report.go
@@ -34,7 +34,7 @@ func NewHostReporter(aggregates *Aggregates, limit uint, metric Metric, outputFo
}
// Report returns the formatted report string.
-func (r *Reporter) Report() string {
+func (r Reporter) Report() string {
var rows []tableRow
var hasLastKernel bool
if r.category == CategoryHost {
@@ -48,7 +48,7 @@ func (r *Reporter) Report() string {
return r.formatReport(rows, hasLastKernel)
}
-func (r *Reporter) buildHostTable() ([]tableRow, bool) {
+func (r Reporter) buildHostTable() ([]tableRow, bool) {
type keyVal struct {
agg *HostAggregate
key uint64
@@ -93,7 +93,7 @@ func (r *Reporter) buildHostTable() ([]tableRow, bool) {
return rows, true
}
-func (r *Reporter) buildCategoryTable() ([]tableRow, bool) {
+func (r Reporter) buildCategoryTable() ([]tableRow, bool) {
m := r.aggregates.Kernel
switch r.category {
case CategoryKernelMajor:
@@ -140,7 +140,7 @@ func (r *Reporter) buildCategoryTable() ([]tableRow, bool) {
return rows, false
}
-func (r *Reporter) humanStrHost(h *HostAggregate) string {
+func (r Reporter) humanStrHost(h *HostAggregate) string {
switch r.metric {
case MetricUptime:
return formatDuration(h.Uptime)
@@ -157,7 +157,7 @@ func (r *Reporter) humanStrHost(h *HostAggregate) string {
}
}
-func (r *Reporter) humanStrAgg(a *Aggregate) string {
+func (r Reporter) humanStrAgg(a *Aggregate) string {
switch r.metric {
case MetricUptime:
return formatDuration(a.Uptime)
@@ -170,7 +170,7 @@ func (r *Reporter) humanStrAgg(a *Aggregate) string {
}
}
-func (r *Reporter) formatReport(rows []tableRow, hasLastKernel bool) string {
+func (r Reporter) formatReport(rows []tableRow, hasLastKernel bool) string {
cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel)
border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel)
header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border)
@@ -183,7 +183,7 @@ func (r *Reporter) formatReport(rows []tableRow, hasLastKernel bool) string {
return out
}
-func (r *Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
+func (r Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) {
countW = 3
nameW = len(r.category.String())
valueW = len(r.metric.String())
@@ -207,7 +207,7 @@ func (r *Reporter) reportWidths(rows []tableRow, hasLastKernel bool) (countW, na
return countW, nameW, valueW, lastKernelW
}
-func (r *Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+func (r Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
parts := []string{
"+" + strings.Repeat("-", 2+countW),
"+" + strings.Repeat("-", 2+nameW),
@@ -219,7 +219,7 @@ func (r *Reporter) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKe
return strings.Join(parts, "") + "+\n"
}
-func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string) string {
+func (r Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string) string {
var h string
if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext {
h = strings.Repeat("#", int(r.headerIndent)) + " "
@@ -227,8 +227,8 @@ func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, has
h += fmt.Sprintf("Top %d %s's by %s\n\n", r.limit, r.metric, r.category)
desc := MetricDescription(r.metric)
lineLimit := len(border)
- if r.outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit {
- desc = " " + wordWrap(desc, lineLimit)
+ if r.outputFormat == FormatPlaintext && lineLimit > 0 && len(desc) > lineLimit-1 {
+ desc = " " + wordWrap(desc, lineLimit-1)
}
h += desc + "\n\n"
if r.outputFormat == FormatMarkdown || r.outputFormat == FormatGemtext {
@@ -245,14 +245,14 @@ func (r *Reporter) buildReportHeader(countW, nameW, valueW, lastKernelW int, has
return h
}
-func (r *Reporter) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
+func (r Reporter) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string {
if hasLastKernel {
return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW)
}
return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW)
}
-func (r *Reporter) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
+func (r Reporter) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string {
var b strings.Builder
for _, row := range rows {
if hasLastKernel {
diff --git a/internal/goprecords/types.go b/internal/goprecords/types.go
index ce44df8..05a68f8 100644
--- a/internal/goprecords/types.go
+++ b/internal/goprecords/types.go
@@ -24,6 +24,65 @@ const (
CategoryKernelName
)
+// Metric is the value to rank by (Boots, Uptime, etc.).
+type Metric int
+
+const (
+ MetricBoots Metric = iota
+ MetricUptime
+ MetricScore
+ MetricDowntime
+ MetricLifespan
+)
+
+// OutputFormat is the report output format.
+type OutputFormat int
+
+const (
+ FormatPlaintext OutputFormat = iota
+ FormatMarkdown
+ FormatGemtext
+)
+
+// Epoch is a Unix timestamp for duration/date formatting.
+type Epoch uint64
+
+// Aggregate holds per-entity stats (Host, Kernel, etc.).
+type Aggregate struct {
+ Name string
+ Uptime uint64
+ FirstBoot uint64
+ LastSeen uint64
+ Boots uint64
+}
+
+// NewAggregate constructs an Aggregate with the given name.
+func NewAggregate(name string) *Aggregate {
+ return &Aggregate{Name: name}
+}
+
+// HostAggregate adds last-kernel and lifespan/downtime for host reports.
+type HostAggregate struct {
+ Aggregate
+ LastKernel string
+}
+
+// NewHostAggregate constructs a HostAggregate.
+func NewHostAggregate(name, lastKernel string) *HostAggregate {
+ return &HostAggregate{
+ Aggregate: Aggregate{Name: name},
+ LastKernel: lastKernel,
+ }
+}
+
+// tableRow is one row in the report table.
+type tableRow struct {
+ Pos string
+ Name string
+ Value string
+ LastKernel string
+}
+
// String returns the category name.
func (c Category) String() string {
switch c {
@@ -40,17 +99,6 @@ func (c Category) String() string {
}
}
-// Metric is the value to rank by (Boots, Uptime, etc.).
-type Metric int
-
-const (
- MetricBoots Metric = iota
- MetricUptime
- MetricScore
- MetricDowntime
- MetricLifespan
-)
-
// String returns the metric name.
func (m Metric) String() string {
switch m {
@@ -69,15 +117,6 @@ func (m Metric) String() string {
}
}
-// OutputFormat is the report output format.
-type OutputFormat int
-
-const (
- FormatPlaintext OutputFormat = iota
- FormatMarkdown
- FormatGemtext
-)
-
// String returns the format name.
func (f OutputFormat) String() string {
switch f {
@@ -92,9 +131,6 @@ func (f OutputFormat) String() string {
}
}
-// Epoch is a Unix timestamp for duration/date formatting.
-type Epoch uint64
-
// HumanDuration returns a human-readable duration from epoch (e.g. "1 years, 2 months, 3 days").
func (e Epoch) HumanDuration() string {
t := time.Unix(int64(e), 0).UTC()
@@ -110,20 +146,6 @@ func (e Epoch) NewerThan(limitDays uint) bool {
return time.Since(then) < time.Duration(limitDays)*24*time.Hour
}
-// Aggregate holds per-entity stats (Host, Kernel, etc.).
-type Aggregate struct {
- Name string
- Uptime uint64
- FirstBoot uint64
- LastSeen uint64
- Boots uint64
-}
-
-// NewAggregate constructs an Aggregate with the given name.
-func NewAggregate(name string) *Aggregate {
- return &Aggregate{Name: name}
-}
-
// AddRecord adds one uptime record.
func (a *Aggregate) AddRecord(uptimeSec, bootTime uint64) {
lastSeen := uptimeSec + bootTime
@@ -151,20 +173,6 @@ func (a *Aggregate) MetaScore() uint64 {
return ((a.Uptime*2 + a.Boots*uint64(Day) + activeBonus) / 1000000)
}
-// HostAggregate adds last-kernel and lifespan/downtime for host reports.
-type HostAggregate struct {
- Aggregate
- LastKernel string
-}
-
-// NewHostAggregate constructs a HostAggregate.
-func NewHostAggregate(name, lastKernel string) *HostAggregate {
- return &HostAggregate{
- Aggregate: Aggregate{Name: name},
- LastKernel: lastKernel,
- }
-}
-
// Lifespan returns last-seen minus first-boot.
func (h *HostAggregate) Lifespan() uint64 { return h.LastSeen - h.FirstBoot }
@@ -176,14 +184,6 @@ func (h *HostAggregate) MetaScore() uint64 {
return uint64(h.Downtime()/2000000) + h.Aggregate.MetaScore()
}
-// tableRow is one row in the report table.
-type tableRow struct {
- Pos string
- Name string
- Value string
- LastKernel string
-}
-
// MetricDescription returns the description text for a metric.
func MetricDescription(m Metric) string {
switch m {
@@ -257,22 +257,24 @@ func wordWrap(s string, lineLimit int) string {
var b strings.Builder
chars := 0
for _, word := range strings.Fields(s) {
- wlen := len(word)
- if chars > 0 {
- wlen++
+ wordLen := len(word)
+ needsSpace := chars > 0
+ totalLen := wordLen
+ if needsSpace {
+ totalLen++
}
- if chars+wlen > lineLimit {
+ if chars+totalLen > lineLimit {
if chars > 0 {
b.WriteByte('\n')
}
b.WriteString(word)
- chars = len(word)
+ chars = wordLen
} else {
- if chars > 0 {
+ if needsSpace {
b.WriteByte(' ')
}
b.WriteString(word)
- chars += wlen
+ chars += totalLen
}
}
return b.String()