feat: add NO_COLOR support

Add support for disabling ANSI colour output via the --no-color flag
or the NO_COLOR environment variable. The suggested implementation was
inspired by the standards defined at no-color.org
This commit is contained in:
Dan Anglin 2024-05-31 21:18:11 +01:00
parent 88d6a8d498
commit bff1eba972
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
13 changed files with 140 additions and 91 deletions

View file

@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"os"
"strconv"
"codeflow.dananglin.me.uk/apollo/enbas/internal/executor"
)
@ -57,9 +58,23 @@ func run() error {
commandUnblock: "unblock a resource (e.g. an account)",
}
topLevelFlags := executor.TopLevelFlags{}
topLevelFlags := executor.TopLevelFlags{
ConfigDir: "",
NoColor: nil,
}
flag.StringVar(&topLevelFlags.ConfigDir, "config-dir", "", "specify your config directory")
flag.BoolFunc("no-color", "disable ANSI colour output when displaying text on screen", func(value string) error {
boolVal, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("unable to parse %q as a boolean; %w", value, err)
}
topLevelFlags.NoColor = new(bool)
*topLevelFlags.NoColor = boolVal
return nil
})
flag.Usage = usageFunc(commandSummaries)
@ -71,6 +86,16 @@ func run() error {
return nil
}
// If NoColor is still unspecified, check to see if the NO_COLOR environment variable is set
if topLevelFlags.NoColor == nil {
topLevelFlags.NoColor = new(bool)
if os.Getenv("NO_COLOR") != "" {
*topLevelFlags.NoColor = true
} else {
*topLevelFlags.NoColor = false
}
}
command := flag.Arg(0)
args := flag.Args()[1:]

View file

@ -112,7 +112,7 @@ func (c *CreateExecutor) createList(gtsClient *client.Client) error {
}
fmt.Println("Successfully created the following list:")
fmt.Printf("\n%s\n", list)
utilities.Display(list, *c.topLevelFlags.NoColor)
return nil
}
@ -193,7 +193,7 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
}
fmt.Println("Successfully created the following status:")
fmt.Println(status)
utilities.Display(status, *c.topLevelFlags.NoColor)
return nil
}

View file

@ -6,6 +6,7 @@ import (
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
)
type EditExecutor struct {
@ -40,7 +41,7 @@ func (e *EditExecutor) Execute() error {
}
funcMap := map[string]func(*client.Client) error{
resourceList: e.updateList,
resourceList: e.editList,
}
doFunc, ok := funcMap[e.resourceType]
@ -56,7 +57,7 @@ func (e *EditExecutor) Execute() error {
return doFunc(gtsClient)
}
func (e *EditExecutor) updateList(gtsClient *client.Client) error {
func (e *EditExecutor) editList(gtsClient *client.Client) error {
if e.listID == "" {
return FlagNotSetError{flagText: flagListID}
}
@ -85,7 +86,7 @@ func (e *EditExecutor) updateList(gtsClient *client.Client) error {
}
fmt.Println("Successfully updated the list.")
fmt.Println(updatedList)
utilities.Display(updatedList, *e.topLevelFlags.NoColor)
return nil
}

View file

@ -18,4 +18,5 @@ func (a *AccountNames) Set(value string) error {
type TopLevelFlags struct {
ConfigDir string
NoColor *bool
}

View file

@ -83,7 +83,7 @@ func (c *ShowExecutor) showInstance(gtsClient *client.Client) error {
return fmt.Errorf("unable to retrieve the instance details; %w", err)
}
fmt.Println(instance)
utilities.Display(instance, *c.topLevelFlags.NoColor)
return nil
}
@ -116,7 +116,7 @@ func (c *ShowExecutor) showAccount(gtsClient *client.Client) error {
return nil
}
fmt.Println(account)
utilities.Display(account, *c.topLevelFlags.NoColor)
if !c.myAccount && !c.skipAccountRelationship {
relationship, err := gtsClient.GetAccountRelationship(account.ID)
@ -124,7 +124,7 @@ func (c *ShowExecutor) showAccount(gtsClient *client.Client) error {
return fmt.Errorf("unable to retrieve the relationship to this account; %w", err)
}
fmt.Println(relationship)
utilities.Display(relationship, *c.topLevelFlags.NoColor)
}
if c.myAccount && c.showUserPreferences {
@ -133,7 +133,7 @@ func (c *ShowExecutor) showAccount(gtsClient *client.Client) error {
return fmt.Errorf("unable to retrieve the user preferences; %w", err)
}
fmt.Println(preferences)
utilities.Display(preferences, *c.topLevelFlags.NoColor)
}
return nil
@ -155,7 +155,7 @@ func (c *ShowExecutor) showStatus(gtsClient *client.Client) error {
return nil
}
fmt.Println(status)
utilities.Display(status, *c.topLevelFlags.NoColor)
return nil
}
@ -197,7 +197,7 @@ func (c *ShowExecutor) showTimeline(gtsClient *client.Client) error {
return nil
}
fmt.Println(timeline)
utilities.Display(timeline, *c.topLevelFlags.NoColor)
return nil
}
@ -226,7 +226,7 @@ func (c *ShowExecutor) showList(gtsClient *client.Client) error {
list.Accounts = accountMap
}
fmt.Println(list)
utilities.Display(list, *c.topLevelFlags.NoColor)
return nil
}
@ -243,8 +243,7 @@ func (c *ShowExecutor) showLists(gtsClient *client.Client) error {
return nil
}
fmt.Println(utilities.HeaderFormat("LISTS"))
fmt.Println(lists)
utilities.Display(lists, *c.topLevelFlags.NoColor)
return nil
}
@ -261,7 +260,7 @@ func (c *ShowExecutor) showFollowers(gtsClient *client.Client) error {
}
if len(followers.Accounts) > 0 {
fmt.Println(followers)
utilities.Display(followers, *c.topLevelFlags.NoColor)
} else {
fmt.Println("There are no followers for this account or the list is hidden.")
}
@ -281,7 +280,7 @@ func (c *ShowExecutor) showFollowing(gtsClient *client.Client) error {
}
if len(following.Accounts) > 0 {
fmt.Println(following)
utilities.Display(following, *c.topLevelFlags.NoColor)
} else {
fmt.Println("This account is not following anyone or the list is hidden.")
}
@ -296,7 +295,7 @@ func (c *ShowExecutor) showBlocked(gtsClient *client.Client) error {
}
if len(blocked.Accounts) > 0 {
fmt.Println(blocked)
utilities.Display(blocked, *c.topLevelFlags.NoColor)
} else {
fmt.Println("You have no blocked accounts.")
}

View file

@ -59,7 +59,7 @@ type Field struct {
VerifiedAt string `json:"verified_at"`
}
func (a Account) String() string {
func (a Account) Display(noColor bool) string {
format := `
%s (@%s)
@ -87,28 +87,28 @@ func (a Account) String() string {
for _, field := range a.Fields {
metadata += fmt.Sprintf(
"\n %s: %s",
utilities.FieldFormat(field.Name),
utilities.FieldFormat(noColor, field.Name),
utilities.StripHTMLTags(field.Value),
)
}
return fmt.Sprintf(
format,
utilities.DisplayNameFormat(a.DisplayName),
utilities.DisplayNameFormat(noColor, a.DisplayName),
a.Username,
utilities.HeaderFormat("ACCOUNT ID:"),
utilities.HeaderFormat(noColor, "ACCOUNT ID:"),
a.ID,
utilities.HeaderFormat("JOINED ON:"),
utilities.HeaderFormat(noColor, "JOINED ON:"),
utilities.FormatDate(a.CreatedAt),
utilities.HeaderFormat("STATS:"),
utilities.FieldFormat("Followers:"), a.FollowersCount,
utilities.FieldFormat("Following:"), a.FollowingCount,
utilities.FieldFormat("Statuses:"), a.StatusCount,
utilities.HeaderFormat("BIOGRAPHY:"),
utilities.HeaderFormat(noColor, "STATS:"),
utilities.FieldFormat(noColor, "Followers:"), a.FollowersCount,
utilities.FieldFormat(noColor, "Following:"), a.FollowingCount,
utilities.FieldFormat(noColor, "Statuses:"), a.StatusCount,
utilities.HeaderFormat(noColor, "BIOGRAPHY:"),
utilities.WrapLines(utilities.StripHTMLTags(a.Note), "\n ", 80),
utilities.HeaderFormat("METADATA:"),
utilities.HeaderFormat(noColor, "METADATA:"),
metadata,
utilities.HeaderFormat("ACCOUNT URL:"),
utilities.HeaderFormat(noColor, "ACCOUNT URL:"),
a.URL,
)
}
@ -130,7 +130,7 @@ type AccountRelationship struct {
ShowingReblogs bool `json:"showing_reblogs"`
}
func (a AccountRelationship) String() string {
func (a AccountRelationship) Display(noColor bool) string {
format := `
%s
%s: %t
@ -151,25 +151,25 @@ func (a AccountRelationship) String() string {
output := fmt.Sprintf(
format,
utilities.HeaderFormat("YOUR RELATIONSHIP WITH THIS ACCOUNT:"),
utilities.FieldFormat("Following"), a.Following,
utilities.FieldFormat("Is following you"), a.FollowedBy,
utilities.FieldFormat("A follow request was sent and is pending"), a.FollowRequested,
utilities.FieldFormat("Received a pending follow request"), a.FollowRequestedBy,
utilities.FieldFormat("Endorsed"), a.Endorsed,
utilities.FieldFormat("Showing Reposts (boosts)"), a.ShowingReblogs,
utilities.FieldFormat("Muted"), a.Muting,
utilities.FieldFormat("Notifications muted"), a.MutingNotifications,
utilities.FieldFormat("Blocking"), a.Blocking,
utilities.FieldFormat("Is blocking you"), a.BlockedBy,
utilities.FieldFormat("Blocking account's domain"), a.DomainBlocking,
utilities.HeaderFormat(noColor, "YOUR RELATIONSHIP WITH THIS ACCOUNT:"),
utilities.FieldFormat(noColor, "Following"), a.Following,
utilities.FieldFormat(noColor, "Is following you"), a.FollowedBy,
utilities.FieldFormat(noColor, "A follow request was sent and is pending"), a.FollowRequested,
utilities.FieldFormat(noColor, "Received a pending follow request"), a.FollowRequestedBy,
utilities.FieldFormat(noColor, "Endorsed"), a.Endorsed,
utilities.FieldFormat(noColor, "Showing Reposts (boosts)"), a.ShowingReblogs,
utilities.FieldFormat(noColor, "Muted"), a.Muting,
utilities.FieldFormat(noColor, "Notifications muted"), a.MutingNotifications,
utilities.FieldFormat(noColor, "Blocking"), a.Blocking,
utilities.FieldFormat(noColor, "Is blocking you"), a.BlockedBy,
utilities.FieldFormat(noColor, "Blocking account's domain"), a.DomainBlocking,
)
if a.PrivateNote != "" {
output += "\n"
output += fmt.Sprintf(
privateNoteFormat,
utilities.HeaderFormat("YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:"),
utilities.HeaderFormat(noColor, "YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:"),
utilities.WrapLines(a.PrivateNote, "\n ", 80),
)
}
@ -190,18 +190,18 @@ type AccountList struct {
Accounts []Account
}
func (a AccountList) String() string {
func (a AccountList) Display(noColor bool) string {
output := "\n"
switch a.Type {
case AccountListFollowers:
output += utilities.HeaderFormat("FOLLOWED BY:")
output += utilities.HeaderFormat(noColor, "FOLLOWED BY:")
case AccountListFollowing:
output += utilities.HeaderFormat("FOLLOWING:")
output += utilities.HeaderFormat(noColor, "FOLLOWING:")
case AccountListBlockedAccount:
output += utilities.HeaderFormat("BLOCKED ACCOUNTS:")
output += utilities.HeaderFormat(noColor, "BLOCKED ACCOUNTS:")
default:
output += utilities.HeaderFormat("ACCOUNTS:")
output += utilities.HeaderFormat(noColor, "ACCOUNTS:")
}
if a.Type == AccountListBlockedAccount {
@ -216,7 +216,7 @@ func (a AccountList) String() string {
for i := range a.Accounts {
output += fmt.Sprintf(
"\n • %s (%s)",
utilities.DisplayNameFormat(a.Accounts[i].DisplayName),
utilities.DisplayNameFormat(noColor, a.Accounts[i].DisplayName),
a.Accounts[i].Acct,
)
}

View file

@ -113,7 +113,7 @@ type InstanceV2Users struct {
ActiveMonth int `json:"active_month"`
}
func (i InstanceV2) String() string {
func (i InstanceV2) Display(noColor bool) string {
format := `
%s
%s
@ -138,22 +138,22 @@ func (i InstanceV2) String() string {
return fmt.Sprintf(
format,
utilities.HeaderFormat("INSTANCE TITLE:"),
utilities.HeaderFormat(noColor, "INSTANCE TITLE:"),
i.Title,
utilities.HeaderFormat("INSTANCE DESCRIPTION:"),
utilities.HeaderFormat(noColor, "INSTANCE DESCRIPTION:"),
utilities.WrapLines(i.DescriptionText, "\n ", 80),
utilities.HeaderFormat("DOMAIN:"),
utilities.HeaderFormat(noColor, "DOMAIN:"),
i.Domain,
utilities.HeaderFormat("TERMS AND CONDITIONS:"),
utilities.HeaderFormat(noColor, "TERMS AND CONDITIONS:"),
utilities.WrapLines(i.TermsText, "\n ", 80),
utilities.HeaderFormat("VERSION:"),
utilities.HeaderFormat(noColor, "VERSION:"),
i.Version,
utilities.HeaderFormat("CONTACT:"),
utilities.FieldFormat("Name:"),
utilities.DisplayNameFormat(i.Contact.Account.DisplayName),
utilities.FieldFormat("Username:"),
utilities.HeaderFormat(noColor, "CONTACT:"),
utilities.FieldFormat(noColor, "Name:"),
utilities.DisplayNameFormat(noColor, i.Contact.Account.DisplayName),
utilities.FieldFormat(noColor, "Username:"),
i.Contact.Account.Username,
utilities.FieldFormat("Email:"),
utilities.FieldFormat(noColor, "Email:"),
i.Contact.Email,
)
}

View file

@ -83,7 +83,7 @@ type List struct {
Accounts map[string]string
}
func (l List) String() string {
func (l List) Display(noColor bool) string {
format := `
%s
%s
@ -98,17 +98,17 @@ func (l List) String() string {
output := fmt.Sprintf(
format,
utilities.HeaderFormat("LIST TITLE:"), l.Title,
utilities.HeaderFormat("LIST ID:"), l.ID,
utilities.HeaderFormat("REPLIES POLICY:"), l.RepliesPolicy,
utilities.HeaderFormat("ADDED ACCOUNTS:"),
utilities.HeaderFormat(noColor, "LIST TITLE:"), l.Title,
utilities.HeaderFormat(noColor, "LIST ID:"), l.ID,
utilities.HeaderFormat(noColor, "REPLIES POLICY:"), l.RepliesPolicy,
utilities.HeaderFormat(noColor, "ADDED ACCOUNTS:"),
)
if len(l.Accounts) > 0 {
for id, name := range l.Accounts {
output += fmt.Sprintf(
"\n • %s (%s)",
utilities.DisplayNameFormat(name),
utilities.DisplayNameFormat(noColor, name),
id,
)
}
@ -121,12 +121,12 @@ func (l List) String() string {
type Lists []List
func (l Lists) String() string {
output := ""
func (l Lists) Display(noColor bool) string {
output := "\n" + utilities.HeaderFormat(noColor, "LISTS")
for i := range l {
output += fmt.Sprintf(
"\n%s (%s)",
"\n%s (%s)",
l[i].Title,
l[i].ID,
)

View file

@ -15,7 +15,7 @@ type Preferences struct {
ReadingAutoplayGifs bool `json:"reading:autoplay:gifs"`
}
func (p Preferences) String() string {
func (p Preferences) Display(noColor bool) string {
format := `
%s
%s: %s
@ -24,9 +24,9 @@ func (p Preferences) String() string {
return fmt.Sprintf(
format,
utilities.HeaderFormat("YOUR PREFERENCES:"),
utilities.FieldFormat("Default post language"), p.PostingDefaultLanguage,
utilities.FieldFormat("Default post visibility"), p.PostingDefaultVisibility,
utilities.FieldFormat("Mark posts as sensitive by default"), p.PostingDefaultSensitive,
utilities.HeaderFormat(noColor, "YOUR PREFERENCES:"),
utilities.FieldFormat(noColor, "Default post language"), p.PostingDefaultLanguage,
utilities.FieldFormat(noColor, "Default post visibility"), p.PostingDefaultVisibility,
utilities.FieldFormat(noColor, "Mark posts as sensitive by default"), p.PostingDefaultSensitive,
)
}

View file

@ -152,7 +152,7 @@ type MediaDimensions struct {
Width int `json:"width"`
}
func (s Status) String() string {
func (s Status) Display(noColor bool) string {
format := `
%s (@%s)
@ -178,20 +178,20 @@ func (s Status) String() string {
return fmt.Sprintf(
format,
utilities.DisplayNameFormat(s.Account.DisplayName), s.Account.Username,
utilities.HeaderFormat("CONTENT:"),
utilities.DisplayNameFormat(noColor, s.Account.DisplayName), s.Account.Username,
utilities.HeaderFormat(noColor, "CONTENT:"),
utilities.WrapLines(utilities.StripHTMLTags(s.Content), "\n ", 80),
utilities.HeaderFormat("STATUS ID:"),
utilities.HeaderFormat(noColor, "STATUS ID:"),
s.ID,
utilities.HeaderFormat("CREATED AT:"),
utilities.HeaderFormat(noColor, "CREATED AT:"),
utilities.FormatTime(s.CreatedAt),
utilities.HeaderFormat("STATS:"),
utilities.HeaderFormat(noColor, "STATS:"),
s.ReblogsCount,
s.FavouritesCount,
s.RepliesCount,
utilities.HeaderFormat("VISIBILITY:"),
utilities.HeaderFormat(noColor, "VISIBILITY:"),
s.Visibility,
utilities.HeaderFormat("URL:"),
utilities.HeaderFormat(noColor, "URL:"),
s.URL,
)
}

View file

@ -11,27 +11,27 @@ type Timeline struct {
Statuses []Status
}
func (t Timeline) String() string {
func (t Timeline) Display(noColor bool) string {
var builder strings.Builder
separator := "────────────────────────────────────────────────────────────────────────────────"
builder.WriteString(utilities.HeaderFormat(t.Name) + "\n\n")
builder.WriteString(utilities.HeaderFormat(noColor, t.Name) + "\n")
for _, status := range t.Statuses {
builder.WriteString(utilities.DisplayNameFormat(status.Account.DisplayName) + " (@" + status.Account.Acct + ")\n")
builder.WriteString("\n" + utilities.DisplayNameFormat(noColor, status.Account.DisplayName) + " (@" + status.Account.Acct + ")\n")
statusID := status.ID
createdAt := status.CreatedAt
if status.Reblog != nil {
builder.WriteString("reposted this status from " + utilities.DisplayNameFormat(status.Reblog.Account.DisplayName) + " (@" + status.Reblog.Account.Acct + ")\n")
builder.WriteString("reposted this status from " + utilities.DisplayNameFormat(noColor, status.Reblog.Account.DisplayName) + " (@" + status.Reblog.Account.Acct + ")\n")
statusID = status.Reblog.ID
createdAt = status.Reblog.CreatedAt
}
builder.WriteString(utilities.WrapLines(utilities.StripHTMLTags(status.Content), "\n", 80) + "\n\n")
builder.WriteString(utilities.FieldFormat("ID:") + " " + statusID + "\t" + utilities.FieldFormat("Created at:") + " " + utilities.FormatTime(createdAt) + "\n")
builder.WriteString(utilities.FieldFormat(noColor, "ID:") + " " + statusID + "\t" + utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) + "\n")
builder.WriteString(separator + "\n")
}

View file

@ -0,0 +1,11 @@
package utilities
import "os"
type Displayer interface {
Display(noColor bool) string
}
func Display(d Displayer, noColor bool) {
os.Stdout.WriteString(d.Display(noColor) + "\n")
}

View file

@ -12,18 +12,30 @@ const (
green = "\033[32m"
)
func HeaderFormat(text string) string {
func HeaderFormat(noColor bool, text string) string {
if noColor {
return text
}
return boldblue + text + reset
}
func FieldFormat(text string) string {
func FieldFormat(noColor bool, text string) string {
if noColor {
return text
}
return green + text + reset
}
func DisplayNameFormat(text string) string {
func DisplayNameFormat(noColor bool, text string) string {
// use this pattern to remove all emoji strings
pattern := regexp.MustCompile(`\s:[A-Za-z0-9]*:`)
if noColor {
return pattern.ReplaceAllString(text, "")
}
return boldmagenta + pattern.ReplaceAllString(text, "") + reset
}