diff --git a/go.mod b/go.mod index 9e9d928..a53a20f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.6 require ( github.com/cockroachdb/pebble v1.1.5 + github.com/forPelevin/gomoji v1.4.1 github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/confmap v1.0.0 github.com/knadh/koanf/providers/file v1.2.1 @@ -40,6 +41,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/stretchr/testify v1.11.1 // indirect go.yaml.in/yaml/v3 v3.0.3 // indirect diff --git a/go.sum b/go.sum index b128f9f..02c6f34 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1: github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/forPelevin/gomoji v1.4.1 h1:7U+Bl8o6RV/dOQz7coQFWj/jX6Ram6/cWFOuFDEPEUo= +github.com/forPelevin/gomoji v1.4.1/go.mod h1:mM6GtmCgpoQP2usDArc6GjbXrti5+FffolyQfGgPboQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= @@ -82,6 +84,8 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= diff --git a/internal/calendar/handler.go b/internal/calendar/handler.go index eb857e1..612300d 100644 --- a/internal/calendar/handler.go +++ b/internal/calendar/handler.go @@ -116,7 +116,7 @@ func (s *Syncer) syncStatus(ctx context.Context) error { if err := s.store.SetStatus(stored); err != nil { errs = append(errs, fmt.Errorf("store status: %w", err)) } - s.logger.Info().Str("text", st.Text).Time("expiry", st.Expiration).Msg("synced status") + s.logger.Info().Str("emoji", st.Emoji).Str("text", st.Text).Time("expiry", st.Expiration).Msg("synced status") } else { if err := s.store.DeleteStatus(); err != nil { errs = append(errs, fmt.Errorf("delete status: %w", err)) diff --git a/internal/github/target.go b/internal/github/target.go index 517f96f..f90ebdd 100644 --- a/internal/github/target.go +++ b/internal/github/target.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/forPelevin/gomoji" "github.com/gldraphael/status/internal/target" ) @@ -18,8 +19,8 @@ import ( // // Required token scope: user type Target struct { - token string - client *http.Client + token string + client *http.Client } // NewTarget creates a GitHub target for the given personal access token. @@ -34,6 +35,14 @@ func NewTarget(token string) *Target { // Sync implements target.Target. A nil status clears the GitHub user profile status. func (t *Target) Sync(ctx context.Context, st *target.Status) error { + if st != nil { + emoji, text := extractFirstEmoji(st.Text) + if emoji != "" { + st.Emoji = emoji + st.Text = text + } + } + mutation := buildGraphQLMutation(st) payload := map[string]string{"query": mutation} @@ -119,3 +128,16 @@ func escapeGraphQLString(s string) string { s = strings.ReplaceAll(s, `"`, `\"`) return fmt.Sprintf(`"%s"`, s) } + +// extractFirstEmoji returns the first emoji found in the string and the remaining text. +func extractFirstEmoji(s string) (string, string) { + emojis := gomoji.CollectAll(s) + if len(emojis) == 0 { + return "", s + } + + first := emojis[0].Character + // Remove ONLY the first occurrence of the emoji character. + rest := strings.Replace(s, first, "", 1) + return first, strings.TrimSpace(rest) +} diff --git a/internal/github/target_test.go b/internal/github/target_test.go index fe7828f..1e5d4b4 100644 --- a/internal/github/target_test.go +++ b/internal/github/target_test.go @@ -124,3 +124,49 @@ func TestNewTarget(t *testing.T) { t.Errorf("http client should be initialized") } } + +func TestExtractFirstEmoji(t *testing.T) { + tests := []struct { + input string + wantEmoji string + wantText string + }{ + { + input: "πŸ’‘ Focusing... 🎯", + wantEmoji: "πŸ’‘", + wantText: "Focusing... 🎯", + }, + { + input: "🌘 Unwinding...", + wantEmoji: "🌘", + wantText: "Unwinding...", + }, + { + input: "Meeting", + wantEmoji: "", + wantText: "Meeting", + }, + { + input: "πŸš€ Rocket! πŸš€", + wantEmoji: "πŸš€", + wantText: "Rocket! πŸš€", + }, + { + input: "Flag πŸ‡ΊπŸ‡Έ in middle", + wantEmoji: "πŸ‡ΊπŸ‡Έ", + wantText: "Flag in middle", // Note: double space + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + gotEmoji, gotText := extractFirstEmoji(tt.input) + if gotEmoji != tt.wantEmoji { + t.Errorf("extractFirstEmoji() gotEmoji = %v, want %v", gotEmoji, tt.wantEmoji) + } + if gotText != tt.wantText { + t.Errorf("extractFirstEmoji() gotText = %v, want %v", gotText, tt.wantText) + } + }) + } +}