diff --git a/cmd/entries/edit.go b/cmd/entries/edit.go index b314ff8..9d79c9d 100644 --- a/cmd/entries/edit.go +++ b/cmd/entries/edit.go @@ -243,7 +243,7 @@ func EditCmd() *cobra.Command { os.Exit(1) } - descriptionInput = strings.TrimSpace(descriptionInput) + descriptionInput = ui.SanitizeSingleLine(descriptionInput) if descriptionInput == "" { descriptionInput = currentDescription } diff --git a/cmd/entries/manual.go b/cmd/entries/manual.go index babdb45..a24f86d 100644 --- a/cmd/entries/manual.go +++ b/cmd/entries/manual.go @@ -102,7 +102,7 @@ func ManualCmd() *cobra.Command { os.Exit(1) } - projectInput = strings.TrimSpace(projectInput) + projectInput = ui.SanitizeSingleLine(projectInput) if projectInput == "" { projectName = projectHint } else { @@ -249,7 +249,7 @@ func ManualCmd() *cobra.Command { os.Exit(1) } - descVal = strings.TrimSpace(descVal) + descVal = ui.SanitizeSingleLine(descVal) if descVal != "" { description = descVal } diff --git a/cmd/milestones/start.go b/cmd/milestones/start.go index 92fea4a..3bad29d 100644 --- a/cmd/milestones/start.go +++ b/cmd/milestones/start.go @@ -36,7 +36,7 @@ func StartCmd() *cobra.Command { os.Exit(1) } - milestoneName := args[0] + milestoneName := ui.SanitizeSingleLine(args[0]) // Check if there's already an active milestone activeMilestone, err := db.GetActiveMilestoneForProject(projectName) diff --git a/cmd/tracking/start.go b/cmd/tracking/start.go index 34fbb3d..23eb7f8 100644 --- a/cmd/tracking/start.go +++ b/cmd/tracking/start.go @@ -52,7 +52,7 @@ func StartCmd() *cobra.Command { description := "" if len(args) > 0 { - description = args[0] + description = ui.SanitizeSingleLine(args[0]) } var hourlyRate *float64 diff --git a/internal/ui/ui.go b/internal/ui/ui.go index a55791e..7cb467c 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "os" + "strings" "time" "github.com/DylanDevelops/tmpo/internal/shell" @@ -159,6 +160,13 @@ func FormatFileSize(bytes int64) string { return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) } +func SanitizeSingleLine(s string) string { + s = strings.ReplaceAll(s, "\r\n", " ") + s = strings.ReplaceAll(s, "\r", " ") + s = strings.ReplaceAll(s, "\n", " ") + return strings.TrimSpace(s) +} + func FormatDuration(d time.Duration) string { totalSeconds := int(d.Seconds()) days := totalSeconds / 86400 diff --git a/internal/ui/ui_test.go b/internal/ui/ui_test.go index 87be392..0fb1620 100644 --- a/internal/ui/ui_test.go +++ b/internal/ui/ui_test.go @@ -164,6 +164,77 @@ func TestFormatFileSize(t *testing.T) { } } +func TestSanitizeSingleLine(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "plain string unchanged", + input: "hello world", + expected: "hello world", + }, + { + name: "trims leading and trailing whitespace", + input: " hello world ", + expected: "hello world", + }, + { + name: "replaces newline with space", + input: "hello\nworld", + expected: "hello world", + }, + { + name: "replaces carriage return with space", + input: "hello\rworld", + expected: "hello world", + }, + { + name: "replaces CRLF with space", + input: "hello\r\nworld", + expected: "hello world", + }, + { + name: "replaces multiple newlines", + input: "hello\n\nworld", + expected: "hello world", + }, + { + name: "trims trailing newline", + input: "hello\n", + expected: "hello", + }, + { + name: "trims leading newline", + input: "\nhello", + expected: "hello", + }, + { + name: "empty string stays empty", + input: "", + expected: "", + }, + { + name: "only whitespace becomes empty", + input: " \n\t ", + expected: "", + }, + { + name: "mixed newlines and spaces", + input: " something blah blah blah \n ", + expected: "something blah blah blah", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SanitizeSingleLine(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestFormatDuration(t *testing.T) { tests := []struct { name string