diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index 9c4274b..e5105dd 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -11,6 +11,7 @@ import ( "strconv" "codeflow.dananglin.me.uk/apollo/enbas/internal/executor" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) var ( @@ -22,47 +23,33 @@ var ( func main() { if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "ERROR: %v.\n", err) os.Exit(1) } } func run() error { - topLevelFlags := executor.TopLevelFlags{ - ConfigDir: "", - NoColor: nil, - Pager: "", - } - - flag.StringVar( - &topLevelFlags.ConfigDir, - "config-dir", - "", - "Specify your config directory", + var ( + configDir string + pager string + maxTerminalWidth int + noColor *bool ) - 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) - } + flag.StringVar(&configDir, "config-dir", "", "Specify your config directory") + flag.StringVar(&pager, "pager", "", "Specify your preferred pager to page through long outputs. This is disabled by default.") + flag.IntVar(&maxTerminalWidth, "max-terminal-width", 80, "Specify the maximum terminal width when displaying resources on screen.") - topLevelFlags.NoColor = new(bool) - *topLevelFlags.NoColor = boolVal + 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) + } - return nil - }, - ) + noColor = new(bool) + *noColor = boolVal - flag.StringVar( - &topLevelFlags.Pager, - "pager", - "", - "Specify your preferred pager to page through long outputs. This is disabled by default.", - ) + return nil + }) flag.Usage = usageFunc(executor.CommandSummaryMap()) @@ -75,90 +62,107 @@ func run() error { } // 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 noColor == nil { + noColor = new(bool) if os.Getenv("NO_COLOR") != "" { - *topLevelFlags.NoColor = true + *noColor = true } else { - *topLevelFlags.NoColor = false + *noColor = false } } command := flag.Arg(0) args := flag.Args()[1:] + printer := printer.NewPrinter(*noColor, pager, maxTerminalWidth) + executorMap := map[string]executor.Executor{ executor.CommandAccept: executor.NewAcceptOrRejectExecutor( - topLevelFlags, + printer, + configDir, executor.CommandAccept, executor.CommandSummaryLookup(executor.CommandAccept), ), executor.CommandAdd: executor.NewAddExecutor( - topLevelFlags, + printer, + configDir, executor.CommandAdd, executor.CommandSummaryLookup(executor.CommandAdd), ), executor.CommandBlock: executor.NewBlockOrUnblockExecutor( - topLevelFlags, + printer, + configDir, executor.CommandBlock, executor.CommandSummaryLookup(executor.CommandBlock), ), executor.CommandCreate: executor.NewCreateExecutor( - topLevelFlags, + printer, + configDir, executor.CommandCreate, executor.CommandSummaryLookup(executor.CommandCreate), ), executor.CommandDelete: executor.NewDeleteExecutor( - topLevelFlags, + printer, + configDir, executor.CommandDelete, executor.CommandSummaryLookup(executor.CommandDelete), ), executor.CommandEdit: executor.NewEditExecutor( - topLevelFlags, + printer, + configDir, executor.CommandEdit, executor.CommandSummaryLookup(executor.CommandEdit), ), executor.CommandFollow: executor.NewFollowOrUnfollowExecutor( - topLevelFlags, + printer, + configDir, executor.CommandFollow, executor.CommandSummaryLookup(executor.CommandFollow), ), executor.CommandLogin: executor.NewLoginExecutor( - topLevelFlags, + printer, + configDir, executor.CommandLogin, executor.CommandSummaryLookup(executor.CommandLogin), ), executor.CommandReject: executor.NewAcceptOrRejectExecutor( - topLevelFlags, + printer, + configDir, executor.CommandReject, executor.CommandSummaryLookup(executor.CommandReject), ), executor.CommandRemove: executor.NewRemoveExecutor( - topLevelFlags, + printer, + configDir, executor.CommandRemove, executor.CommandSummaryLookup(executor.CommandRemove), ), executor.CommandSwitch: executor.NewSwitchExecutor( - topLevelFlags, + printer, + configDir, executor.CommandSwitch, executor.CommandSummaryLookup(executor.CommandSwitch), ), executor.CommandUnfollow: executor.NewFollowOrUnfollowExecutor( - topLevelFlags, + printer, + configDir, executor.CommandUnfollow, executor.CommandSummaryLookup(executor.CommandUnfollow), ), executor.CommandUnblock: executor.NewBlockOrUnblockExecutor( - topLevelFlags, + printer, + configDir, executor.CommandUnblock, executor.CommandSummaryLookup(executor.CommandUnblock), ), executor.CommandShow: executor.NewShowExecutor( - topLevelFlags, + printer, + configDir, executor.CommandShow, executor.CommandSummaryLookup(executor.CommandShow), ), executor.CommandVersion: executor.NewVersionExecutor( + printer, executor.CommandVersion, executor.CommandSummaryLookup(executor.CommandVersion), binaryVersion, @@ -167,7 +171,8 @@ func run() error { gitCommit, ), executor.CommandWhoami: executor.NewWhoAmIExecutor( - topLevelFlags, + printer, + configDir, executor.CommandWhoami, executor.CommandSummaryLookup(executor.CommandWhoami), ), @@ -175,13 +180,18 @@ func run() error { exe, ok := executorMap[command] if !ok { + err := executor.UnknownCommandError{Command: command} + + printer.PrintFailure(err.Error() + ".") flag.Usage() - return executor.UnknownCommandError{Command: command} + return err } if err := executor.Execute(exe, args); err != nil { - return fmt.Errorf("(%s) %w", command, err) + printer.PrintFailure("(" + command + ") " + err.Error() + ".") + + return err } return nil diff --git a/internal/client/accounts.go b/internal/client/accounts.go index 85a3d39..1f9161c 100644 --- a/internal/client/accounts.go +++ b/internal/client/accounts.go @@ -39,21 +39,27 @@ func (g *Client) GetAccount(accountURI string) (model.Account, error) { return account, nil } -func (g *Client) GetAccountRelationship(accountID string) (model.AccountRelationship, error) { +func (g *Client) GetAccountRelationship(accountID string) (*model.AccountRelationship, error) { path := "/api/v1/accounts/relationships?id=" + accountID url := g.Authentication.Instance + path var relationships []model.AccountRelationship if err := g.sendRequest(http.MethodGet, url, nil, &relationships); err != nil { - return model.AccountRelationship{}, fmt.Errorf("received an error after sending the request to get the account relationship: %w", err) + return nil, fmt.Errorf( + "received an error after sending the request to get the account relationship: %w", + err, + ) } if len(relationships) != 1 { - return model.AccountRelationship{}, fmt.Errorf("unexpected number of account relationships returned: want 1, got %d", len(relationships)) + return nil, fmt.Errorf( + "unexpected number of account relationships returned: want 1, got %d", + len(relationships), + ) } - return relationships[0], nil + return &relationships[0], nil } type FollowAccountForm struct { diff --git a/internal/client/lists.go b/internal/client/lists.go index b46e22a..9d51ce0 100644 --- a/internal/client/lists.go +++ b/internal/client/lists.go @@ -17,7 +17,7 @@ const ( listPath string = "/api/v1/lists" ) -func (g *Client) GetAllLists() (model.Lists, error) { +func (g *Client) GetAllLists() ([]model.List, error) { url := g.Authentication.Instance + listPath var lists []model.List diff --git a/internal/client/preferences.go b/internal/client/preferences.go index a424344..cc8a88f 100644 --- a/internal/client/preferences.go +++ b/internal/client/preferences.go @@ -11,14 +11,14 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/model" ) -func (g *Client) GetUserPreferences() (model.Preferences, error) { +func (g *Client) GetUserPreferences() (*model.Preferences, error) { url := g.Authentication.Instance + "/api/v1/preferences" var preferences model.Preferences if err := g.sendRequest(http.MethodGet, url, nil, &preferences); err != nil { - return model.Preferences{}, fmt.Errorf("received an error after sending the request to get the user preferences: %w", err) + return nil, fmt.Errorf("received an error after sending the request to get the user preferences: %w", err) } - return preferences, nil + return &preferences, nil } diff --git a/internal/executor/accept_or_reject.go b/internal/executor/accept_or_reject.go index 1fae328..7e32c69 100644 --- a/internal/executor/accept_or_reject.go +++ b/internal/executor/accept_or_reject.go @@ -9,23 +9,26 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type AcceptOrRejectExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - resourceType string - accountName string - command string + printer *printer.Printer + configDir string + resourceType string + accountName string + command string } -func NewAcceptOrRejectExecutor(tlf TopLevelFlags, name, summary string) *AcceptOrRejectExecutor { +func NewAcceptOrRejectExecutor(enbasPrinter *printer.Printer, configDir, name, summary string) *AcceptOrRejectExecutor { acceptExe := AcceptOrRejectExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, - command: name, + printer: enbasPrinter, + configDir: configDir, + command: name, } acceptExe.StringVar(&acceptExe.resourceType, flagType, "", "Specify the type of resource to accept or reject") @@ -46,7 +49,7 @@ func (a *AcceptOrRejectExecutor) Execute() error { return UnsupportedTypeError{resourceType: a.resourceType} } - gtsClient, err := client.NewClientFromConfig(a.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(a.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -55,7 +58,7 @@ func (a *AcceptOrRejectExecutor) Execute() error { } func (a *AcceptOrRejectExecutor) acceptOrRejectFollowRequest(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, false, a.accountName, a.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, false, a.accountName, a.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -75,7 +78,7 @@ func (a *AcceptOrRejectExecutor) acceptFollowRequest(gtsClient *client.Client, a return fmt.Errorf("unable to accept the follow request: %w", err) } - fmt.Println("Successfully accepted the follow request.") + a.printer.PrintSuccess("Successfully accepted the follow request.") return nil } @@ -85,7 +88,7 @@ func (a *AcceptOrRejectExecutor) rejectFollowRequest(gtsClient *client.Client, a return fmt.Errorf("unable to reject the follow request: %w", err) } - fmt.Println("Successfully rejected the follow request.") + a.printer.PrintSuccess("Successfully rejected the follow request.") return nil } diff --git a/internal/executor/add.go b/internal/executor/add.go index d48926b..dce233e 100644 --- a/internal/executor/add.go +++ b/internal/executor/add.go @@ -10,12 +10,14 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type AddExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + printer *printer.Printer + configDir string resourceType string toResourceType string listID string @@ -26,13 +28,15 @@ type AddExecutor struct { content string } -func NewAddExecutor(tlf TopLevelFlags, name, summary string) *AddExecutor { +func NewAddExecutor(printer *printer.Printer, configDir, name, summary string) *AddExecutor { emptyArr := make([]string, 0, 3) addExe := AddExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - accountNames: MultiStringFlagValue(emptyArr), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, + accountNames: MultiStringFlagValue(emptyArr), } addExe.StringVar(&addExe.resourceType, flagType, "", "Specify the resource type to add (e.g. account, note)") @@ -67,7 +71,7 @@ func (a *AddExecutor) Execute() error { return UnsupportedTypeError{resourceType: a.toResourceType} } - gtsClient, err := client.NewClientFromConfig(a.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(a.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -124,7 +128,7 @@ func (a *AddExecutor) addAccountsToList(gtsClient *client.Client) error { return fmt.Errorf("unable to add the accounts to the list: %w", err) } - fmt.Println("Successfully added the account(s) to the list.") + a.printer.PrintSuccess("Successfully added the account(s) to the list.") return nil } @@ -150,7 +154,7 @@ func (a *AddExecutor) addNoteToAccount(gtsClient *client.Client) error { return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(a.accountNames)) } - accountID, err := getAccountID(gtsClient, false, a.accountNames[0], a.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, false, a.accountNames[0], a.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -166,7 +170,7 @@ func (a *AddExecutor) addNoteToAccount(gtsClient *client.Client) error { return fmt.Errorf("unable to add the private note to the account: %w", err) } - fmt.Println("Successfully added the private note to the account.") + a.printer.PrintSuccess("Successfully added the private note to the account.") return nil } @@ -196,7 +200,7 @@ func (a *AddExecutor) addStatusToBookmarks(gtsClient *client.Client) error { return fmt.Errorf("unable to add the status to your bookmarks: %w", err) } - fmt.Println("Successfully added the status to your bookmarks.") + a.printer.PrintSuccess("Successfully added the status to your bookmarks.") return nil } @@ -228,7 +232,7 @@ func (a *AddExecutor) addStarToStatus(gtsClient *client.Client) error { return fmt.Errorf("unable to add the %s to the status: %w", a.resourceType, err) } - fmt.Printf("Successfully added a %s to the status.\n", a.resourceType) + a.printer.PrintSuccess("Successfully added a " + a.resourceType + " to the status.") return nil } @@ -238,7 +242,7 @@ func (a *AddExecutor) addBoostToStatus(gtsClient *client.Client) error { return fmt.Errorf("unable to add the boost to the status: %w", err) } - fmt.Println("Successfully added the boost to the status.") + a.printer.PrintSuccess("Successfully added the boost to the status.") return nil } @@ -285,7 +289,7 @@ func (a *AddExecutor) addVoteToPoll(gtsClient *client.Client) error { return fmt.Errorf("unable to add your vote(s) to the poll: %w", err) } - fmt.Println("Successfully added your vote(s) to the poll.") + a.printer.PrintSuccess("Successfully added your vote(s) to the poll.") return nil } diff --git a/internal/executor/block_or_unblock.go b/internal/executor/block_or_unblock.go index e68ae94..c129bb5 100644 --- a/internal/executor/block_or_unblock.go +++ b/internal/executor/block_or_unblock.go @@ -9,23 +9,26 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type BlockOrUnblockExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - resourceType string - accountName string - command string + printer *printer.Printer + configDir string + resourceType string + accountName string + command string } -func NewBlockOrUnblockExecutor(tlf TopLevelFlags, name, summary string) *BlockOrUnblockExecutor { +func NewBlockOrUnblockExecutor(printer *printer.Printer, configDir, name, summary string) *BlockOrUnblockExecutor { blockExe := BlockOrUnblockExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, - command: name, + printer: printer, + configDir: configDir, + command: name, } blockExe.StringVar(&blockExe.resourceType, flagType, "", "Specify the type of resource to block or unblock") @@ -46,7 +49,7 @@ func (b *BlockOrUnblockExecutor) Execute() error { return UnsupportedTypeError{resourceType: b.resourceType} } - gtsClient, err := client.NewClientFromConfig(b.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(b.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -55,7 +58,7 @@ func (b *BlockOrUnblockExecutor) Execute() error { } func (b *BlockOrUnblockExecutor) blockOrUnblockAccount(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, false, b.accountName, b.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, false, b.accountName, b.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -75,7 +78,7 @@ func (b *BlockOrUnblockExecutor) blockAccount(gtsClient *client.Client, accountI return fmt.Errorf("unable to block the account: %w", err) } - fmt.Println("Successfully blocked the account.") + b.printer.PrintSuccess("Successfully blocked the account.") return nil } @@ -85,7 +88,7 @@ func (b *BlockOrUnblockExecutor) unblockAccount(gtsClient *client.Client, accoun return fmt.Errorf("unable to unblock the account: %w", err) } - fmt.Println("Successfully unblocked the account.") + b.printer.PrintSuccess("Successfully unblocked the account.") return nil } diff --git a/internal/executor/create.go b/internal/executor/create.go index 910508d..9aee759 100644 --- a/internal/executor/create.go +++ b/internal/executor/create.go @@ -11,39 +11,42 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type CreateExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - addPoll bool - boostable bool - federated bool - likeable bool + printer *printer.Printer + addPoll bool + boostable bool + federated bool + likeable bool pollAllowsMultipleChoices bool pollHidesVoteCounts bool - replyable bool - sensitive *bool - content string - contentType string - fromFile string - language string - resourceType string - listTitle string - listRepliesPolicy string - spoilerText string - visibility string - pollExpiresIn TimeDurationFlagValue - pollOptions MultiStringFlagValue + replyable bool + sensitive *bool + configDir string + content string + contentType string + fromFile string + language string + resourceType string + listTitle string + listRepliesPolicy string + spoilerText string + visibility string + pollExpiresIn TimeDurationFlagValue + pollOptions MultiStringFlagValue } -func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor { +func NewCreateExecutor(printer *printer.Printer, configDir, name, summary string) *CreateExecutor { createExe := CreateExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + printer: printer, + configDir: configDir, } createExe.BoolVar(&createExe.boostable, flagEnableReposts, true, "Specify if the status can be reposted/boosted by others") @@ -87,7 +90,7 @@ func (c *CreateExecutor) Execute() error { return FlagNotSetError{flagText: flagType} } - gtsClient, err := client.NewClientFromConfig(c.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(c.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -125,8 +128,8 @@ func (c *CreateExecutor) createList(gtsClient *client.Client) error { return fmt.Errorf("unable to create the list: %w", err) } - fmt.Println("Successfully created the following list:") - utilities.Display(list, *c.topLevelFlags.NoColor, c.topLevelFlags.Pager) + c.printer.PrintSuccess("Successfully created the following list:") + c.printer.PrintList(list) return nil } @@ -222,8 +225,8 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { return fmt.Errorf("unable to create the status: %w", err) } - fmt.Println("Successfully created the following status:") - utilities.Display(status, *c.topLevelFlags.NoColor, c.topLevelFlags.Pager) + c.printer.PrintSuccess("Successfully created the following status:") + c.printer.PrintStatus(status) return nil } diff --git a/internal/executor/delete.go b/internal/executor/delete.go index fec798d..7d173c6 100644 --- a/internal/executor/delete.go +++ b/internal/executor/delete.go @@ -9,20 +9,24 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type DeleteExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - resourceType string - listID string + printer *printer.Printer + configDir string + resourceType string + listID string } -func NewDeleteExecutor(tlf TopLevelFlags, name, summary string) *DeleteExecutor { +func NewDeleteExecutor(printer *printer.Printer, configDir, name, summary string) *DeleteExecutor { deleteExe := DeleteExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, } deleteExe.StringVar(&deleteExe.resourceType, flagType, "", "Specify the type of resource to delete") @@ -47,7 +51,7 @@ func (d *DeleteExecutor) Execute() error { return UnsupportedTypeError{resourceType: d.resourceType} } - gtsClient, err := client.NewClientFromConfig(d.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(d.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -64,7 +68,7 @@ func (d *DeleteExecutor) deleteList(gtsClient *client.Client) error { return fmt.Errorf("unable to delete the list: %w", err) } - fmt.Println("The list was successfully deleted.") + d.printer.PrintSuccess("The list was successfully deleted.") return nil } diff --git a/internal/executor/edit.go b/internal/executor/edit.go index 620a3ca..e18e2e8 100644 --- a/internal/executor/edit.go +++ b/internal/executor/edit.go @@ -10,23 +10,26 @@ 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" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type EditExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + printer *printer.Printer + configDir string resourceType string listID string listTitle string listRepliesPolicy string } -func NewEditExecutor(tlf TopLevelFlags, name, summary string) *EditExecutor { +func NewEditExecutor(printer *printer.Printer, configDir, name, summary string) *EditExecutor { editExe := EditExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, } editExe.StringVar(&editExe.resourceType, flagType, "", "Specify the type of resource to update") @@ -53,7 +56,7 @@ func (e *EditExecutor) Execute() error { return UnsupportedTypeError{resourceType: e.resourceType} } - gtsClient, err := client.NewClientFromConfig(e.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(e.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -89,8 +92,8 @@ func (e *EditExecutor) editList(gtsClient *client.Client) error { return fmt.Errorf("unable to update the list: %w", err) } - fmt.Println("Successfully updated the list.") - utilities.Display(updatedList, *e.topLevelFlags.NoColor, e.topLevelFlags.Pager) + e.printer.PrintSuccess("Successfully updated the list.") + e.printer.PrintList(updatedList) return nil } diff --git a/internal/executor/flags.go b/internal/executor/flags.go index 13227db..9faf44d 100644 --- a/internal/executor/flags.go +++ b/internal/executor/flags.go @@ -51,12 +51,6 @@ const ( flagVisibility = "visibility" ) -type TopLevelFlags struct { - ConfigDir string - NoColor *bool - Pager string -} - type MultiStringFlagValue []string func (v *MultiStringFlagValue) String() string { diff --git a/internal/executor/follow_or_unfollow.go b/internal/executor/follow_or_unfollow.go index 9ffbb78..535df04 100644 --- a/internal/executor/follow_or_unfollow.go +++ b/internal/executor/follow_or_unfollow.go @@ -9,25 +9,28 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type FollowOrUnfollowExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - resourceType string - accountName string - showReposts bool - notify bool - action string + printer *printer.Printer + configDir string + resourceType string + accountName string + showReposts bool + notify bool + action string } -func NewFollowOrUnfollowExecutor(tlf TopLevelFlags, name, summary string) *FollowOrUnfollowExecutor { +func NewFollowOrUnfollowExecutor(printer *printer.Printer, configDir, name, summary string) *FollowOrUnfollowExecutor { command := FollowOrUnfollowExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, - action: name, + printer: printer, + configDir: configDir, + action: name, } command.StringVar(&command.resourceType, flagType, "", "Specify the type of resource to follow") @@ -50,7 +53,7 @@ func (f *FollowOrUnfollowExecutor) Execute() error { return UnsupportedTypeError{resourceType: f.resourceType} } - gtsClient, err := client.NewClientFromConfig(f.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(f.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -59,7 +62,7 @@ func (f *FollowOrUnfollowExecutor) Execute() error { } func (f *FollowOrUnfollowExecutor) followOrUnfollowAccount(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, false, f.accountName, f.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, false, f.accountName, f.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -85,7 +88,7 @@ func (f *FollowOrUnfollowExecutor) followAccount(gtsClient *client.Client, accou return fmt.Errorf("unable to follow the account: %w", err) } - fmt.Println("The follow request was sent successfully.") + f.printer.PrintSuccess("Successfully sent the follow request.") return nil } @@ -95,7 +98,7 @@ func (f *FollowOrUnfollowExecutor) unfollowAccount(gtsClient *client.Client, acc return fmt.Errorf("unable to unfollow the account: %w", err) } - fmt.Println("Successfully unfollowed the account.") + f.printer.PrintSuccess("Successfully unfollowed the account.") return nil } diff --git a/internal/executor/login.go b/internal/executor/login.go index 79af662..37d4115 100644 --- a/internal/executor/login.go +++ b/internal/executor/login.go @@ -11,21 +11,25 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type LoginExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - instance string + printer *printer.Printer + configDir string + instance string } -func NewLoginExecutor(tlf TopLevelFlags, name, summary string) *LoginExecutor { +func NewLoginExecutor(printer *printer.Printer, configDir, name, summary string) *LoginExecutor { command := LoginExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, - instance: "", + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, + instance: "", } command.StringVar(&command.instance, flagInstance, "", "Specify the instance that you want to login to.") @@ -66,21 +70,17 @@ func (c *LoginExecutor) Execute() error { utilities.OpenLink(consentPageURL) - consentMessageFormat := ` -You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with -the application's login process. Your browser may have opened the link to the consent page already. If not, please -copy and paste the link below to your browser: + var builder strings.Builder -%s + builder.WriteString("\nYou'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with the application's login process.") + builder.WriteString("\nYour browser may have opened the link to the consent page already. If not, please copy and paste the link below to your browser:") + builder.WriteString("\n\n" + consentPageURL) + builder.WriteString("\n\n" + "Once you have the code please copy and paste it below.") + builder.WriteString("\n" + "Out-of-band token: ") -Once you have the code please copy and paste it below. - -` - - fmt.Printf(consentMessageFormat, consentPageURL) + c.printer.PrintInfo(builder.String()) var code string - fmt.Print("Out-of-band token: ") if _, err := fmt.Scanln(&code); err != nil { return fmt.Errorf("failed to read access code: %w", err) @@ -95,12 +95,12 @@ Once you have the code please copy and paste it below. return fmt.Errorf("unable to verify the credentials: %w", err) } - loginName, err := config.SaveCredentials(c.topLevelFlags.ConfigDir, account.Username, gtsClient.Authentication) + loginName, err := config.SaveCredentials(c.configDir, account.Username, gtsClient.Authentication) if err != nil { return fmt.Errorf("unable to save the authentication details: %w", err) } - fmt.Printf("Successfully logged into %s\n", loginName) + c.printer.PrintSuccess("Successfully logged into " + loginName + ".") return nil } diff --git a/internal/executor/remove.go b/internal/executor/remove.go index 01ecf1f..3814872 100644 --- a/internal/executor/remove.go +++ b/internal/executor/remove.go @@ -9,12 +9,14 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type RemoveExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + printer *printer.Printer + configDir string resourceType string fromResourceType string listID string @@ -22,13 +24,15 @@ type RemoveExecutor struct { accountNames MultiStringFlagValue } -func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor { +func NewRemoveExecutor(printer *printer.Printer, configDir, name, summary string) *RemoveExecutor { emptyArr := make([]string, 0, 3) removeExe := RemoveExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - accountNames: MultiStringFlagValue(emptyArr), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, + accountNames: MultiStringFlagValue(emptyArr), } removeExe.StringVar(&removeExe.resourceType, flagType, "", "Specify the resource type to remove (e.g. account, note)") @@ -59,7 +63,7 @@ func (r *RemoveExecutor) Execute() error { return UnsupportedTypeError{resourceType: r.fromResourceType} } - gtsClient, err := client.NewClientFromConfig(r.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(r.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -107,7 +111,7 @@ func (r *RemoveExecutor) removeAccountsFromList(gtsClient *client.Client) error return fmt.Errorf("unable to remove the accounts from the list: %w", err) } - fmt.Println("Successfully removed the account(s) from the list.") + r.printer.PrintSuccess("Successfully removed the account(s) from the list.") return nil } @@ -133,7 +137,7 @@ func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error { return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(r.accountNames)) } - accountID, err := getAccountID(gtsClient, false, r.accountNames[0], r.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, false, r.accountNames[0], r.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -142,7 +146,7 @@ func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error { return fmt.Errorf("unable to remove the private note from the account: %w", err) } - fmt.Println("Successfully removed the private note from the account.") + r.printer.PrintSuccess("Successfully removed the private note from the account.") return nil } @@ -172,7 +176,7 @@ func (r *RemoveExecutor) removeStatusFromBookmarks(gtsClient *client.Client) err return fmt.Errorf("unable to remove the status from your bookmarks: %w", err) } - fmt.Println("Successfully removed the status from your bookmarks.") + r.printer.PrintSuccess("Successfully removed the status from your bookmarks.") return nil } @@ -204,7 +208,7 @@ func (r *RemoveExecutor) removeStarFromStatus(gtsClient *client.Client) error { return fmt.Errorf("unable to remove the %s from the status: %w", r.resourceType, err) } - fmt.Printf("Successfully removed the %s from the status.\n", r.resourceType) + r.printer.PrintSuccess("Successfully removed the " + r.resourceType + " from the status.") return nil } @@ -214,7 +218,7 @@ func (r *RemoveExecutor) removeBoostFromStatus(gtsClient *client.Client) error { return fmt.Errorf("unable to remove the boost from the status: %w", err) } - fmt.Println("Successfully removed the boost from the status.") + r.printer.PrintSuccess("Successfully removed the boost from the status.") return nil } diff --git a/internal/executor/show.go b/internal/executor/show.go index e9b90d3..03a48e8 100644 --- a/internal/executor/show.go +++ b/internal/executor/show.go @@ -10,16 +10,19 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type ShowExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + + printer *printer.Printer myAccount bool skipAccountRelationship bool showUserPreferences bool showInBrowser bool + configDir string resourceType string accountName string statusID string @@ -30,10 +33,12 @@ type ShowExecutor struct { limit int } -func NewShowExecutor(tlf TopLevelFlags, name, summary string) *ShowExecutor { +func NewShowExecutor(printer *printer.Printer, configDir, name, summary string) *ShowExecutor { showExe := ShowExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, } showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "Set to true to lookup your account") @@ -80,7 +85,7 @@ func (s *ShowExecutor) Execute() error { return UnsupportedTypeError{resourceType: s.resourceType} } - gtsClient, err := client.NewClientFromConfig(s.topLevelFlags.ConfigDir) + gtsClient, err := client.NewClientFromConfig(s.configDir) if err != nil { return fmt.Errorf("unable to create the GoToSocial client: %w", err) } @@ -94,7 +99,7 @@ func (s *ShowExecutor) showInstance(gtsClient *client.Client) error { return fmt.Errorf("unable to retrieve the instance details: %w", err) } - utilities.Display(instance, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintInstance(instance) return nil } @@ -106,7 +111,7 @@ func (s *ShowExecutor) showAccount(gtsClient *client.Client) error { ) if s.myAccount { - account, err = getMyAccount(gtsClient, s.topLevelFlags.ConfigDir) + account, err = getMyAccount(gtsClient, s.configDir) if err != nil { return fmt.Errorf("received an error while getting the account details: %w", err) } @@ -127,26 +132,27 @@ func (s *ShowExecutor) showAccount(gtsClient *client.Client) error { return nil } - utilities.Display(account, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + var ( + relationship *model.AccountRelationship = nil + preferences *model.Preferences = nil + ) if !s.myAccount && !s.skipAccountRelationship { - relationship, err := gtsClient.GetAccountRelationship(account.ID) + relationship, err = gtsClient.GetAccountRelationship(account.ID) if err != nil { return fmt.Errorf("unable to retrieve the relationship to this account: %w", err) } - - utilities.Display(relationship, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) } if s.myAccount && s.showUserPreferences { - preferences, err := gtsClient.GetUserPreferences() + preferences, err = gtsClient.GetUserPreferences() if err != nil { return fmt.Errorf("unable to retrieve the user preferences: %w", err) } - - utilities.Display(preferences, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) } + s.printer.PrintAccount(account, relationship, preferences) + return nil } @@ -166,7 +172,7 @@ func (s *ShowExecutor) showStatus(gtsClient *client.Client) error { return nil } - utilities.Display(status, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintStatus(status) return nil } @@ -210,12 +216,12 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error { } if len(timeline.Statuses) == 0 { - fmt.Println("There are no statuses in this timeline.") + s.printer.PrintInfo("There are no statuses in this timeline.\n") return nil } - utilities.Display(timeline, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintStatusList(timeline) return nil } @@ -244,7 +250,7 @@ func (s *ShowExecutor) showList(gtsClient *client.Client) error { list.Accounts = accountMap } - utilities.Display(list, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintList(list) return nil } @@ -256,18 +262,18 @@ func (s *ShowExecutor) showLists(gtsClient *client.Client) error { } if len(lists) == 0 { - fmt.Println("You have no lists.") + s.printer.PrintInfo("You have no lists.\n") return nil } - utilities.Display(lists, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintLists(lists) return nil } func (s *ShowExecutor) showFollowers(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -278,16 +284,16 @@ func (s *ShowExecutor) showFollowers(gtsClient *client.Client) error { } if len(followers.Accounts) > 0 { - utilities.Display(followers, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintAccountList(followers) } else { - fmt.Println("There are no followers for this account or the list is hidden.") + s.printer.PrintInfo("There are no followers for this account (or the list is hidden).\n") } return nil } func (s *ShowExecutor) showFollowing(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.topLevelFlags.ConfigDir) + accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.configDir) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -298,9 +304,9 @@ func (s *ShowExecutor) showFollowing(gtsClient *client.Client) error { } if len(following.Accounts) > 0 { - utilities.Display(following, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintAccountList(following) } else { - fmt.Println("This account is not following anyone or the list is hidden.") + s.printer.PrintInfo("This account is not following anyone or the list is hidden.\n") } return nil @@ -313,9 +319,9 @@ func (s *ShowExecutor) showBlocked(gtsClient *client.Client) error { } if len(blocked.Accounts) > 0 { - utilities.Display(blocked, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintAccountList(blocked) } else { - fmt.Println("You have no blocked accounts.") + s.printer.PrintInfo("You have no blocked accounts.\n") } return nil @@ -328,9 +334,9 @@ func (s *ShowExecutor) showBookmarks(gtsClient *client.Client) error { } if len(bookmarks.Statuses) > 0 { - utilities.Display(bookmarks, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintStatusList(bookmarks) } else { - fmt.Println("You have no bookmarks.") + s.printer.PrintInfo("You have no bookmarks.\n") } return nil @@ -343,9 +349,9 @@ func (s *ShowExecutor) showLiked(gtsClient *client.Client) error { } if len(liked.Statuses) > 0 { - utilities.Display(liked, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintStatusList(liked) } else { - fmt.Printf("You have no %s statuses.\n", s.resourceType) + s.printer.PrintInfo("You have no " + s.resourceType + " statuses.\n") } return nil @@ -358,9 +364,9 @@ func (s *ShowExecutor) showFollowRequests(gtsClient *client.Client) error { } if len(accounts.Accounts) > 0 { - utilities.Display(accounts, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintAccountList(accounts) } else { - fmt.Println("You have no follow requests.") + s.printer.PrintInfo("You have no follow requests.\n") } return nil @@ -376,7 +382,7 @@ func (s *ShowExecutor) showPoll(gtsClient *client.Client) error { return fmt.Errorf("unable to retrieve the poll: %w", err) } - utilities.Display(poll, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager) + s.printer.PrintPoll(poll) return nil } diff --git a/internal/executor/switch.go b/internal/executor/switch.go index 142be3f..988047d 100644 --- a/internal/executor/switch.go +++ b/internal/executor/switch.go @@ -9,20 +9,23 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type SwitchExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + configDir string toResourceType string accountName string + printer *printer.Printer } -func NewSwitchExecutor(tlf TopLevelFlags, name, summary string) *SwitchExecutor { +func NewSwitchExecutor(printer *printer.Printer, configDir, name, summary string) *SwitchExecutor { switchExe := SwitchExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + printer: printer, + configDir: configDir, } switchExe.StringVar(&switchExe.toResourceType, flagTo, "", "The account to switch to") @@ -51,11 +54,11 @@ func (s *SwitchExecutor) switchToAccount() error { return NoAccountSpecifiedError{} } - if err := config.UpdateCurrentAccount(s.accountName, s.topLevelFlags.ConfigDir); err != nil { + if err := config.UpdateCurrentAccount(s.accountName, s.configDir); err != nil { return fmt.Errorf("unable to switch account to the account: %w", err) } - fmt.Printf("The current account is now set to %q.\n", s.accountName) + s.printer.PrintSuccess("The current account is now set to '" + s.accountName + "'.") return nil } diff --git a/internal/executor/version.go b/internal/executor/version.go index 39c5ae6..4abdf6f 100644 --- a/internal/executor/version.go +++ b/internal/executor/version.go @@ -6,13 +6,13 @@ package executor import ( "flag" - "os" - "strings" - "text/tabwriter" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type VersionExecutor struct { *flag.FlagSet + printer *printer.Printer showFullVersion bool binaryVersion string buildTime string @@ -20,9 +20,19 @@ type VersionExecutor struct { gitCommit string } -func NewVersionExecutor(name, summary, binaryVersion, buildTime, goVersion, gitCommit string) *VersionExecutor { +func NewVersionExecutor( + enbasPrinter *printer.Printer, + name, + summary, + binaryVersion, + buildTime, + goVersion, + gitCommit string, +) *VersionExecutor { command := VersionExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: enbasPrinter, binaryVersion: binaryVersion, buildTime: buildTime, goVersion: goVersion, @@ -38,24 +48,7 @@ func NewVersionExecutor(name, summary, binaryVersion, buildTime, goVersion, gitC } func (v *VersionExecutor) Execute() error { - var builder strings.Builder - - if v.showFullVersion { - builder.WriteString("Enbas\n") - - tableWriter := tabwriter.NewWriter(&builder, 0, 8, 0, '\t', 0) - - tableWriter.Write([]byte(" Version:\t" + v.binaryVersion + "\n")) - tableWriter.Write([]byte(" Git commit:\t" + v.gitCommit + "\n")) - tableWriter.Write([]byte(" Go version:\t" + v.goVersion + "\n")) - tableWriter.Write([]byte(" Build date:\t" + v.buildTime + "\n")) - - tableWriter.Flush() - } else { - builder.WriteString("Enbas " + v.binaryVersion + "\n") - } - - os.Stdout.WriteString(builder.String()) + v.printer.PrintVersion(v.showFullVersion, v.binaryVersion, v.buildTime, v.goVersion, v.gitCommit) return nil } diff --git a/internal/executor/whoami.go b/internal/executor/whoami.go index 8b87b3c..a1673cb 100644 --- a/internal/executor/whoami.go +++ b/internal/executor/whoami.go @@ -9,18 +9,22 @@ import ( "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) type WhoAmIExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags + printer *printer.Printer + configDir string } -func NewWhoAmIExecutor(tlf TopLevelFlags, name, summary string) *WhoAmIExecutor { +func NewWhoAmIExecutor(printer *printer.Printer, configDir, name, summary string) *WhoAmIExecutor { whoExe := WhoAmIExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - topLevelFlags: tlf, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + + printer: printer, + configDir: configDir, } whoExe.Usage = commandUsageFunc(name, summary, whoExe.FlagSet) @@ -29,12 +33,12 @@ func NewWhoAmIExecutor(tlf TopLevelFlags, name, summary string) *WhoAmIExecutor } func (c *WhoAmIExecutor) Execute() error { - config, err := config.NewCredentialsConfigFromFile(c.topLevelFlags.ConfigDir) + config, err := config.NewCredentialsConfigFromFile(c.configDir) if err != nil { return fmt.Errorf("unable to load the credential config: %w", err) } - fmt.Printf("You are logged in as %q.\n", config.CurrentAccount) + c.printer.PrintInfo("You are logged in as '" + config.CurrentAccount + "'.\n") return nil } diff --git a/internal/model/account.go b/internal/model/account.go index ad3fe5b..6a1f520 100644 --- a/internal/model/account.go +++ b/internal/model/account.go @@ -5,10 +5,7 @@ package model import ( - "fmt" "time" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type Account struct { @@ -63,59 +60,6 @@ type Field struct { VerifiedAt string `json:"verified_at"` } -func (a Account) Display(noColor bool) string { - format := ` -%s - -%s - %s - -%s - %s - -%s - %s %d - %s %d - %s %d - -%s - %s - -%s %s - -%s - %s` - - metadata := "" - - for _, field := range a.Fields { - metadata += fmt.Sprintf( - "\n %s: %s", - utilities.FieldFormat(noColor, field.Name), - utilities.ConvertHTMLToText(field.Value), - ) - } - - return fmt.Sprintf( - format, - utilities.FullDisplayNameFormat(noColor, a.DisplayName, a.Acct), - utilities.HeaderFormat(noColor, "ACCOUNT ID:"), - a.ID, - utilities.HeaderFormat(noColor, "JOINED ON:"), - utilities.FormatDate(a.CreatedAt), - 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.ConvertHTMLToText(a.Note), "\n ", 80), - utilities.HeaderFormat(noColor, "METADATA:"), - metadata, - utilities.HeaderFormat(noColor, "ACCOUNT URL:"), - a.URL, - ) -} - type AccountRelationship struct { ID string `json:"id"` PrivateNote string `json:"note"` @@ -133,53 +77,6 @@ type AccountRelationship struct { ShowingReblogs bool `json:"showing_reblogs"` } -func (a AccountRelationship) Display(noColor bool) string { - format := ` -%s - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t - %s: %t` - - privateNoteFormat := ` -%s - %s` - - output := fmt.Sprintf( - format, - 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(noColor, "YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:"), - utilities.WrapLines(a.PrivateNote, "\n ", 80), - ) - } - - return output -} - type AccountListType int const ( @@ -193,36 +90,3 @@ type AccountList struct { Type AccountListType Accounts []Account } - -func (a AccountList) Display(noColor bool) string { - output := "\n" - - switch a.Type { - case AccountListFollowers: - output += utilities.HeaderFormat(noColor, "Followed by:") - case AccountListFollowing: - output += utilities.HeaderFormat(noColor, "Following:") - case AccountListBlockedAccount: - output += utilities.HeaderFormat(noColor, "Blocked accounts:") - case AccountListFollowRequests: - output += utilities.HeaderFormat(noColor, "Accounts that have requested to follow you:") - default: - output += utilities.HeaderFormat(noColor, "Accounts:") - } - - if a.Type == AccountListBlockedAccount { - for i := range a.Accounts { - output += fmt.Sprintf( - "\n • %s (%s)", - a.Accounts[i].Acct, - a.Accounts[i].ID, - ) - } - } else { - for i := range a.Accounts { - output += "\n • " + utilities.FullDisplayNameFormat(noColor, a.Accounts[i].DisplayName, a.Accounts[i].Acct) - } - } - - return output -} diff --git a/internal/model/instance_v2.go b/internal/model/instance_v2.go index 015ee13..c1461d4 100644 --- a/internal/model/instance_v2.go +++ b/internal/model/instance_v2.go @@ -4,12 +4,6 @@ package model -import ( - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" -) - type InstanceV2 struct { AccountDomain string `json:"account_domain"` Configuration InstanceConfiguration `json:"configuration"` @@ -116,48 +110,3 @@ type InstanceV2Usage struct { type InstanceV2Users struct { ActiveMonth int `json:"active_month"` } - -func (i InstanceV2) Display(noColor bool) string { - format := ` -%s - %s - -%s - %s - -%s - %s - -%s - %s - -%s - Running GoToSocial %s - -%s - %s %s - %s %s - %s %s -` - - return fmt.Sprintf( - format, - utilities.HeaderFormat(noColor, "INSTANCE TITLE:"), - i.Title, - utilities.HeaderFormat(noColor, "INSTANCE DESCRIPTION:"), - utilities.WrapLines(i.DescriptionText, "\n ", 80), - utilities.HeaderFormat(noColor, "DOMAIN:"), - i.Domain, - utilities.HeaderFormat(noColor, "TERMS AND CONDITIONS:"), - utilities.WrapLines(i.TermsText, "\n ", 80), - utilities.HeaderFormat(noColor, "VERSION:"), - i.Version, - utilities.HeaderFormat(noColor, "CONTACT:"), - utilities.FieldFormat(noColor, "Name:"), - i.Contact.Account.DisplayName, - utilities.FieldFormat(noColor, "Username:"), - i.Contact.Account.Acct, - utilities.FieldFormat(noColor, "Email:"), - i.Contact.Email, - ) -} diff --git a/internal/model/list.go b/internal/model/list.go index 665d25a..bbd6a30 100644 --- a/internal/model/list.go +++ b/internal/model/list.go @@ -7,8 +7,6 @@ package model import ( "encoding/json" "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type ListRepliesPolicy int @@ -107,53 +105,3 @@ type List struct { Title string `json:"title"` Accounts map[string]string } - -func (l List) Display(noColor bool) string { - format := ` -%s - %s - -%s - %s - -%s - %s - -%s` - - output := fmt.Sprintf( - format, - 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 acct, name := range l.Accounts { - output += "\n • " + utilities.FullDisplayNameFormat(noColor, name, acct) - } - } else { - output += "\n None" - } - - output += "\n" - - return output -} - -type Lists []List - -func (l Lists) Display(noColor bool) string { - output := "\n" + utilities.HeaderFormat(noColor, "LISTS") - - for i := range l { - output += fmt.Sprintf( - "\n • %s (%s)", - l[i].Title, - l[i].ID, - ) - } - - return output -} diff --git a/internal/model/poll.go b/internal/model/poll.go index 94daded..72d174b 100644 --- a/internal/model/poll.go +++ b/internal/model/poll.go @@ -5,13 +5,7 @@ package model import ( - "io" - "math" - "strconv" - "strings" "time" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type Poll struct { @@ -31,93 +25,3 @@ type PollOption struct { Title string `json:"title"` VotesCount int `json:"votes_count"` } - -func (p Poll) Display(noColor bool) string { - var builder strings.Builder - - indent := " " - - builder.WriteString( - utilities.HeaderFormat(noColor, "POLL ID:") + - "\n" + indent + p.ID + - "\n\n" + utilities.HeaderFormat(noColor, "OPTIONS:"), - ) - - displayPollContent(&builder, p, noColor, indent) - - builder.WriteString( - "\n\n" + - utilities.HeaderFormat(noColor, "MULTIPLE CHOICES ALLOWED:") + - "\n" + indent + strconv.FormatBool(p.Multiple) + - "\n\n" + - utilities.HeaderFormat(noColor, "YOU VOTED:") + - "\n" + indent + strconv.FormatBool(p.Voted), - ) - - if len(p.OwnVotes) > 0 { - builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "YOUR VOTES:")) - - for _, vote := range p.OwnVotes { - builder.WriteString("\n" + indent + "[" + strconv.Itoa(vote) + "] " + p.Options[vote].Title) - } - } - - builder.WriteString( - "\n\n" + - utilities.HeaderFormat(noColor, "EXPIRED:") + - "\n" + indent + strconv.FormatBool(p.Expired), - ) - - return builder.String() -} - -func displayPollContent(writer io.StringWriter, poll Poll, noColor bool, indent string) { - for ind, option := range poll.Options { - var percentage int - var calculate float64 - - if poll.VotesCount == 0 { - percentage = 0 - } else { - calculate = float64(option.VotesCount) / float64(poll.VotesCount) - percentage = int(math.Floor(100 * calculate)) - } - - writer.WriteString("\n\n" + indent + "[" + strconv.Itoa(ind) + "] " + option.Title) - drawPollMeter(writer, noColor, calculate, 80, indent) - - writer.WriteString( - "\n" + indent + strconv.Itoa(option.VotesCount) + " votes " + - "(" + strconv.Itoa(percentage) + "%)", - ) - } - - writer.WriteString( - "\n\n" + - indent + utilities.FieldFormat(noColor, "Total votes:") + " " + strconv.Itoa(poll.VotesCount) + - "\n" + indent + utilities.FieldFormat(noColor, "Poll ID:") + " " + poll.ID + - "\n" + indent + utilities.FieldFormat(noColor, "Poll is open until:") + " " + utilities.FormatTime(poll.ExpiredAt), - ) -} - -func drawPollMeter(writer io.StringWriter, noColor bool, calculated float64, limit int, indent string) { - numVoteBlocks := int(math.Floor(float64(limit) * calculated)) - numBackgroundBlocks := limit - numVoteBlocks - blockChar := "\u2501" - voteBlockColor := "\033[32;1m" - backgroundBlockColor := "\033[90m" - - if noColor { - voteBlockColor = "\033[0m" - - if numVoteBlocks == 0 { - numVoteBlocks = 1 - } - } - - writer.WriteString("\n" + indent + voteBlockColor + strings.Repeat(blockChar, numVoteBlocks) + "\033[0m") - - if !noColor { - writer.WriteString(backgroundBlockColor + strings.Repeat(blockChar, numBackgroundBlocks) + "\033[0m") - } -} diff --git a/internal/model/preferences.go b/internal/model/preferences.go index 94bd77e..c20023e 100644 --- a/internal/model/preferences.go +++ b/internal/model/preferences.go @@ -4,12 +4,6 @@ package model -import ( - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" -) - type Preferences struct { PostingDefaultVisibility string `json:"posting:default:visibility"` PostingDefaultSensitive bool `json:"posting:default:sensitive"` @@ -18,19 +12,3 @@ type Preferences struct { ReadingExpandSpoilers bool `json:"reading:expand:spoilers"` ReadingAutoplayGifs bool `json:"reading:autoplay:gifs"` } - -func (p Preferences) Display(noColor bool) string { - format := ` -%s - %s: %s - %s: %s - %s: %t` - - return fmt.Sprintf( - format, - 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, - ) -} diff --git a/internal/model/status.go b/internal/model/status.go index d150765..20c9b62 100644 --- a/internal/model/status.go +++ b/internal/model/status.go @@ -5,11 +5,7 @@ package model import ( - "strconv" - "strings" "time" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) type Status struct { @@ -139,86 +135,7 @@ type MediaDimensions struct { Width int `json:"width"` } -func (s Status) Display(noColor bool) string { - indent := " " - - var builder strings.Builder - - // The account information - builder.WriteString(utilities.FullDisplayNameFormat(noColor, s.Account.DisplayName, s.Account.Acct) + "\n\n") - - // The content of the status. - builder.WriteString(utilities.HeaderFormat(noColor, "CONTENT:")) - builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80)) - - // If a poll exists in a status, write the contents to the builder. - if s.Poll != nil { - displayPollContent(&builder, *s.Poll, noColor, indent) - } - - // The ID of the status - builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "STATUS ID:") + "\n" + indent + s.ID) - - // Status creation time - builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "CREATED AT:") + "\n" + indent + utilities.FormatTime(s.CreatedAt)) - - // Status stats - builder.WriteString( - "\n\n" + - utilities.HeaderFormat(noColor, "STATS:") + - "\n" + indent + utilities.FieldFormat(noColor, "Boosts: ") + strconv.Itoa(s.ReblogsCount) + - "\n" + indent + utilities.FieldFormat(noColor, "Likes: ") + strconv.Itoa(s.FavouritesCount) + - "\n" + indent + utilities.FieldFormat(noColor, "Replies: ") + strconv.Itoa(s.RepliesCount), - ) - - // Status visibility - builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "VISIBILITY:") + "\n" + indent + s.Visibility.String()) - - // Status URL - builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "URL:") + "\n" + indent + s.URL) - - return builder.String() -} - type StatusList struct { Name string Statuses []Status } - -func (s StatusList) Display(noColor bool) string { - var builder strings.Builder - - separator := strings.Repeat("─", 80) - - builder.WriteString(utilities.HeaderFormat(noColor, s.Name) + "\n") - - for _, status := range s.Statuses { - builder.WriteString("\n" + utilities.FullDisplayNameFormat(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.FullDisplayNameFormat(noColor, status.Reblog.Account.DisplayName, status.Reblog.Account.Acct) + "\n") - statusID = status.Reblog.ID - createdAt = status.Reblog.CreatedAt - } - - builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80)) - - if status.Poll != nil { - displayPollContent(&builder, *status.Poll, noColor, "") - } - - builder.WriteString( - "\n\n" + - utilities.FieldFormat(noColor, "Status ID:") + " " + statusID + "\t" + - utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) + - "\n", - ) - - builder.WriteString(separator + "\n") - } - - return builder.String() -} diff --git a/internal/printer/account.go b/internal/printer/account.go new file mode 100644 index 0000000..655ca0d --- /dev/null +++ b/internal/printer/account.go @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "strconv" + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" +) + +func (p Printer) PrintAccount(account model.Account, relationship *model.AccountRelationship, preferences *model.Preferences) { + var builder strings.Builder + + builder.WriteString("\n" + p.fullDisplayNameFormat(account.DisplayName, account.Acct)) + builder.WriteString("\n\n" + p.headerFormat("ACCOUNT ID:")) + builder.WriteString("\n" + account.ID) + builder.WriteString("\n\n" + p.headerFormat("JOINED ON:")) + builder.WriteString("\n" + p.formatDate(account.CreatedAt)) + builder.WriteString("\n\n" + p.headerFormat("STATS:")) + builder.WriteString("\n" + p.fieldFormat("Followers:")) + builder.WriteString(" " + strconv.Itoa(account.FollowersCount)) + builder.WriteString("\n" + p.fieldFormat("Following:")) + builder.WriteString(" " + strconv.Itoa(account.FollowingCount)) + builder.WriteString("\n" + p.fieldFormat("Statuses:")) + builder.WriteString(" " + strconv.Itoa(account.StatusCount)) + builder.WriteString("\n\n" + p.headerFormat("BIOGRAPHY:")) + builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(account.Note), "\n", p.maxTerminalWidth)) + builder.WriteString("\n\n" + p.headerFormat("METADATA:")) + + for _, field := range account.Fields { + builder.WriteString("\n" + p.fieldFormat(field.Name) + ": " + field.Value) + } + + builder.WriteString("\n\n" + p.headerFormat("ACCOUNT URL:")) + builder.WriteString("\n" + account.URL) + + if relationship != nil { + builder.WriteString(p.accountRelationship(relationship)) + } + + if preferences != nil { + builder.WriteString(p.userPreferences(preferences)) + } + + builder.WriteString("\n\n") + + p.print(builder.String()) +} + +func (p Printer) accountRelationship(relationship *model.AccountRelationship) string { + var builder strings.Builder + + builder.WriteString("\n\n" + p.headerFormat("YOUR RELATIONSHIP WITH THIS ACCOUNT:")) + builder.WriteString("\n" + p.fieldFormat("Following:")) + builder.WriteString(" " + strconv.FormatBool(relationship.Following)) + builder.WriteString("\n" + p.fieldFormat("Is following you:")) + builder.WriteString(" " + strconv.FormatBool(relationship.FollowedBy)) + builder.WriteString("\n" + p.fieldFormat("A follow request was sent and is pending:")) + builder.WriteString(" " + strconv.FormatBool(relationship.FollowRequested)) + builder.WriteString("\n" + p.fieldFormat("Received a pending follow request:")) + builder.WriteString(" " + strconv.FormatBool(relationship.FollowRequestedBy)) + builder.WriteString("\n" + p.fieldFormat("Endorsed:")) + builder.WriteString(" " + strconv.FormatBool(relationship.Endorsed)) + builder.WriteString("\n" + p.fieldFormat("Showing Reposts (boosts):")) + builder.WriteString(" " + strconv.FormatBool(relationship.ShowingReblogs)) + builder.WriteString("\n" + p.fieldFormat("Muted:")) + builder.WriteString(" " + strconv.FormatBool(relationship.Muting)) + builder.WriteString("\n" + p.fieldFormat("Notifications muted:")) + builder.WriteString(" " + strconv.FormatBool(relationship.MutingNotifications)) + builder.WriteString("\n" + p.fieldFormat("Blocking:")) + builder.WriteString(" " + strconv.FormatBool(relationship.Blocking)) + builder.WriteString("\n" + p.fieldFormat("Is blocking you:")) + builder.WriteString(" " + strconv.FormatBool(relationship.BlockedBy)) + builder.WriteString("\n" + p.fieldFormat("Blocking account's domain:")) + builder.WriteString(" " + strconv.FormatBool(relationship.DomainBlocking)) + + if relationship.PrivateNote != "" { + builder.WriteString("\n\n" + p.headerFormat("YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:")) + builder.WriteString("\n" + utilities.WrapLines(relationship.PrivateNote, "\n", p.maxTerminalWidth)) + } + + return builder.String() +} + +func (p Printer) userPreferences(preferences *model.Preferences) string { + var builder strings.Builder + + builder.WriteString("\n\n" + p.headerFormat("YOUR PREFERENCES:")) + + builder.WriteString("\n" + p.fieldFormat("Default post language:")) + builder.WriteString(" " + preferences.PostingDefaultLanguage) + + builder.WriteString("\n" + p.fieldFormat("Default post visibility:")) + builder.WriteString(" " + preferences.PostingDefaultVisibility) + + builder.WriteString("\n" + p.fieldFormat("Mark posts as sensitive by default:")) + builder.WriteString(" " + strconv.FormatBool(preferences.PostingDefaultSensitive)) + + return builder.String() +} + +func (p Printer) PrintAccountList(list model.AccountList) { + var builder strings.Builder + + builder.WriteString("\n") + + switch list.Type { + case model.AccountListFollowers: + builder.WriteString(p.headerFormat("Followed by:")) + case model.AccountListFollowing: + builder.WriteString(p.headerFormat("Following:")) + case model.AccountListBlockedAccount: + builder.WriteString(p.headerFormat("Blocked accounts:")) + case model.AccountListFollowRequests: + builder.WriteString(p.headerFormat("Accounts that have requested to follow you:")) + default: + builder.WriteString(p.headerFormat("Accounts:")) + } + + if list.Type == model.AccountListBlockedAccount { + for ind := range list.Accounts { + builder.WriteString("\n" + p.bullet + " " + list.Accounts[ind].Acct + " (" + list.Accounts[ind].ID + ")") + } + } else { + for ind := range list.Accounts { + builder.WriteString("\n" + p.bullet + " " + p.fullDisplayNameFormat(list.Accounts[ind].DisplayName, list.Accounts[ind].Acct)) + } + } + + builder.WriteString("\n") + + p.print(builder.String()) +} diff --git a/internal/printer/instance.go b/internal/printer/instance.go new file mode 100644 index 0000000..ce1b14e --- /dev/null +++ b/internal/printer/instance.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" +) + +func (p Printer) PrintInstance(instance model.InstanceV2) { + var builder strings.Builder + + builder.WriteString("\n" + p.headerFormat("INSTANCE TITLE:")) + builder.WriteString("\n" + instance.Title) + + builder.WriteString("\n\n" + p.headerFormat("INSTANCE DESCRIPTION:")) + builder.WriteString("\n" + utilities.WrapLines(instance.DescriptionText, "\n", p.maxTerminalWidth)) + + builder.WriteString("\n\n" + p.headerFormat("DOMAIN:")) + builder.WriteString("\n" + instance.Domain) + + builder.WriteString("\n\n" + p.headerFormat("TERMS AND CONDITIONS:")) + builder.WriteString("\n" + utilities.WrapLines(instance.TermsText, "\n ", p.maxTerminalWidth)) + + builder.WriteString("\n\n" + p.headerFormat("VERSION:")) + builder.WriteString("\nRunning GoToSocial " + instance.Version) + + builder.WriteString("\n\n" + p.headerFormat("CONTACT:")) + builder.WriteString("\n" + p.fieldFormat("Name:")) + builder.WriteString(" " + instance.Contact.Account.DisplayName) + builder.WriteString("\n" + p.fieldFormat("Username:")) + builder.WriteString(" " + instance.Contact.Account.Acct) + builder.WriteString("\n" + p.fieldFormat("Email:")) + builder.WriteString(" " + instance.Contact.Email) + + builder.WriteString("\n\n") + + p.print(builder.String()) +} diff --git a/internal/printer/list.go b/internal/printer/list.go new file mode 100644 index 0000000..bd16734 --- /dev/null +++ b/internal/printer/list.go @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" +) + +func (p Printer) PrintList(list model.List) { + var builder strings.Builder + + builder.WriteString("\n" + p.headerFormat("LIST TITLE:") + "\n") + builder.WriteString(list.Title + "\n\n") + builder.WriteString(p.headerFormat("LIST ID:") + "\n") + builder.WriteString(list.ID + "\n\n") + builder.WriteString(p.headerFormat("REPLIES POLICY:") + "\n") + builder.WriteString(list.RepliesPolicy.String() + "\n\n") + builder.WriteString(p.headerFormat("ADDED ACCOUNTS:")) + + if len(list.Accounts) > 0 { + for acct, name := range list.Accounts { + builder.WriteString("\n" + p.bullet + " " + p.fullDisplayNameFormat(name, acct)) + } + } else { + builder.WriteString("\n" + "None") + } + + builder.WriteString("\n") + + printToStdout(builder.String()) +} + +func (p Printer) PrintLists(lists []model.List) { + var builder strings.Builder + + builder.WriteString("\n" + p.headerFormat("LISTS")) + + for i := range lists { + builder.WriteString("\n" + p.bullet + " " + lists[i].Title + " (" + lists[i].ID + ")") + } + + builder.WriteString("\n") + + printToStdout(builder.String()) +} diff --git a/internal/printer/poll.go b/internal/printer/poll.go new file mode 100644 index 0000000..b8a34e6 --- /dev/null +++ b/internal/printer/poll.go @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "math" + "strconv" + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" +) + +func (p Printer) PrintPoll(poll model.Poll) { + var builder strings.Builder + + builder.WriteString("\n" + p.headerFormat("POLL ID:")) + builder.WriteString("\n" + poll.ID) + + builder.WriteString("\n\n" + p.headerFormat("OPTIONS:")) + builder.WriteString(p.pollOptions(poll)) + + builder.WriteString("\n\n" + p.headerFormat("MULTIPLE CHOICES ALLOWED:")) + builder.WriteString("\n" + strconv.FormatBool(poll.Multiple)) + + builder.WriteString("\n\n" + p.headerFormat("YOU VOTED:")) + builder.WriteString("\n" + strconv.FormatBool(poll.Voted)) + + if len(poll.OwnVotes) > 0 { + builder.WriteString("\n\n" + p.headerFormat("YOUR VOTES:")) + + for _, vote := range poll.OwnVotes { + builder.WriteString("\n" + "[" + strconv.Itoa(vote) + "] " + poll.Options[vote].Title) + } + } + + builder.WriteString("\n\n" + p.headerFormat("EXPIRED:")) + builder.WriteString("\n" + strconv.FormatBool(poll.Expired)) + builder.WriteString("\n\n") + + p.print(builder.String()) +} + +func (p Printer) pollOptions(poll model.Poll) string { + var builder strings.Builder + + for ind, option := range poll.Options { + var ( + votage float64 + percentage int + ) + + if poll.VotesCount == 0 { + percentage = 0 + } else { + votage = float64(option.VotesCount) / float64(poll.VotesCount) + percentage = int(math.Floor(100 * votage)) + } + + builder.WriteString("\n\n" + "[" + strconv.Itoa(ind) + "] " + option.Title) + builder.WriteString(p.pollMeter(votage)) + builder.WriteString("\n" + strconv.Itoa(option.VotesCount) + " votes " + "(" + strconv.Itoa(percentage) + "%)") + } + + builder.WriteString("\n\n" + p.fieldFormat("Total votes:") + " " + strconv.Itoa(poll.VotesCount)) + builder.WriteString("\n" + p.fieldFormat("Poll ID:") + " " + poll.ID) + builder.WriteString("\n" + p.fieldFormat("Poll is open until:") + " " + p.formatDateTime(poll.ExpiredAt)) + + return builder.String() +} + +func (p Printer) pollMeter(votage float64) string { + numVoteBlocks := int(math.Floor(float64(p.maxTerminalWidth) * votage)) + numBackgroundBlocks := p.maxTerminalWidth - numVoteBlocks + + voteBlockColor := p.theme.boldgreen + backgroundBlockColor := p.theme.grey + + if p.noColor { + voteBlockColor = p.theme.reset + + if numVoteBlocks == 0 { + numVoteBlocks = 1 + } + } + + meter := "\n" + voteBlockColor + strings.Repeat(p.pollMeterSymbol, numVoteBlocks) + p.theme.reset + + if !p.noColor { + meter += backgroundBlockColor + strings.Repeat(p.pollMeterSymbol, numBackgroundBlocks) + p.theme.reset + } + + return meter +} diff --git a/internal/printer/printer.go b/internal/printer/printer.go new file mode 100644 index 0000000..72cc97a --- /dev/null +++ b/internal/printer/printer.go @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "os" + "os/exec" + "regexp" + "strings" + "time" +) + +const ( + minTerminalWidth = 40 +) + +type theme struct { + reset string + boldblue string + boldmagenta string + green string + boldgreen string + grey string + red string + boldred string +} + +type Printer struct { + theme theme + noColor bool + maxTerminalWidth int + pager string + statusSeparator string + bullet string + pollMeterSymbol string + successSymbol string + failureSymbol string + dateFormat string + dateTimeFormat string +} + +func NewPrinter( + noColor bool, + pager string, + maxTerminalWidth int, +) *Printer { + theme := theme{ + reset: "\033[0m", + boldblue: "\033[34;1m", + boldmagenta: "\033[35;1m", + green: "\033[32m", + boldgreen: "\033[32;1m", + grey: "\033[90m", + red: "\033[31m", + boldred: "\033[31;1m", + } + + if maxTerminalWidth < minTerminalWidth { + maxTerminalWidth = minTerminalWidth + } + + return &Printer{ + noColor: noColor, + maxTerminalWidth: maxTerminalWidth, + pager: pager, + statusSeparator: strings.Repeat("\u2501", maxTerminalWidth), + bullet: "\u2022", + pollMeterSymbol: "\u2501", + successSymbol: "\u2714", + failureSymbol: "\u2717", + dateFormat: "02 Jan 2006", + dateTimeFormat: "02 Jan 2006, 15:04 (MST)", + theme: theme, + } +} + +func (p Printer) PrintSuccess(text string) { + success := p.theme.boldgreen + p.successSymbol + p.theme.reset + if p.noColor { + success = p.successSymbol + } + + printToStdout(success + " " + text + "\n") +} + +func (p Printer) PrintFailure(text string) { + failure := p.theme.boldred + p.failureSymbol + p.theme.reset + if p.noColor { + failure = p.failureSymbol + } + + printToStderr(failure + " " + text + "\n") +} + +func (p Printer) PrintInfo(text string) { + printToStdout(text) +} + +func (p Printer) headerFormat(text string) string { + if p.noColor { + return text + } + + return p.theme.boldblue + text + p.theme.reset +} + +func (p Printer) fieldFormat(text string) string { + if p.noColor { + return text + } + + return p.theme.green + text + p.theme.reset +} + +func (p Printer) fullDisplayNameFormat(displayName, acct string) string { + // use this pattern to remove all emoji strings + pattern := regexp.MustCompile(`\s:[A-Za-z0-9_]*:`) + + var builder strings.Builder + + if p.noColor { + builder.WriteString(pattern.ReplaceAllString(displayName, "")) + } else { + builder.WriteString(p.theme.boldmagenta + pattern.ReplaceAllString(displayName, "") + p.theme.reset) + } + + builder.WriteString(" (@" + acct + ")") + + return builder.String() +} + +func (p Printer) formatDate(date time.Time) string { + return date.Local().Format(p.dateFormat) +} + +func (p Printer) formatDateTime(date time.Time) string { + return date.Local().Format(p.dateTimeFormat) +} + +func (p Printer) print(text string) { + if p.pager == "" { + printToStdout(text) + + return + } + + cmdSplit := strings.Split(p.pager, " ") + + pager := new(exec.Cmd) + + if len(cmdSplit) == 1 { + pager = exec.Command(cmdSplit[0]) //nolint:gosec + } else { + pager = exec.Command(cmdSplit[0], cmdSplit[1:]...) //nolint:gosec + } + + pipe, err := pager.StdinPipe() + if err != nil { + printToStdout(text) + + return + } + + pager.Stdout = os.Stdout + pager.Stderr = os.Stderr + + _ = pager.Start() + + defer func() { + _ = pipe.Close() + _ = pager.Wait() + }() + + _, _ = pipe.Write([]byte(text)) +} + +func printToStdout(text string) { + os.Stdout.WriteString(text) +} + +func printToStderr(text string) { + os.Stderr.WriteString(text) +} diff --git a/internal/printer/status.go b/internal/printer/status.go new file mode 100644 index 0000000..93944a5 --- /dev/null +++ b/internal/printer/status.go @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "strconv" + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" +) + +func (p Printer) PrintStatus(status model.Status) { + var builder strings.Builder + + // The account information + builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)) + + // The content of the status. + builder.WriteString("\n\n" + p.headerFormat("CONTENT:")) + builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", p.maxTerminalWidth)) + + // If a poll exists in a status, write the contents to the builder. + if status.Poll != nil { + builder.WriteString(p.pollOptions(*status.Poll)) + } + + // The ID of the status + builder.WriteString("\n\n" + p.headerFormat("STATUS ID:")) + builder.WriteString("\n" + status.ID) + + // Status creation time + builder.WriteString("\n\n" + p.headerFormat("CREATED AT:")) + builder.WriteString("\n" + p.formatDateTime(status.CreatedAt)) + + // Status stats + builder.WriteString("\n\n" + p.headerFormat("STATS:")) + builder.WriteString("\n" + p.fieldFormat("Boosts: ") + strconv.Itoa(status.ReblogsCount)) + builder.WriteString("\n" + p.fieldFormat("Likes: ") + strconv.Itoa(status.FavouritesCount)) + builder.WriteString("\n" + p.fieldFormat("Replies: ") + strconv.Itoa(status.RepliesCount)) + + // Status visibility + builder.WriteString("\n\n" + p.headerFormat("VISIBILITY:")) + builder.WriteString("\n" + status.Visibility.String()) + + // Status URL + builder.WriteString("\n\n" + p.headerFormat("URL:")) + builder.WriteString("\n" + status.URL) + builder.WriteString("\n\n") + + p.print(builder.String()) +} + +func (p Printer) PrintStatusList(list model.StatusList) { + var builder strings.Builder + + builder.WriteString(p.headerFormat(list.Name) + "\n") + + for _, status := range list.Statuses { + builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)) + + statusID := status.ID + createdAt := status.CreatedAt + + if status.Reblog != nil { + builder.WriteString(utilities.WrapLines( + "\n"+ + "reposted this status from "+ + p.fullDisplayNameFormat(status.Reblog.Account.DisplayName, status.Reblog.Account.Acct), + "\n", + p.maxTerminalWidth, + )) + + statusID = status.Reblog.ID + createdAt = status.Reblog.CreatedAt + } + + builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", p.maxTerminalWidth)) + + if status.Poll != nil { + builder.WriteString(p.pollOptions(*status.Poll)) + } + + builder.WriteString( + "\n\n" + + p.fieldFormat("Status ID:") + " " + statusID + "\t" + + p.fieldFormat("Created at:") + " " + p.formatDateTime(createdAt) + + "\n", + ) + + builder.WriteString(p.statusSeparator + "\n") + } + + p.print(builder.String()) +} diff --git a/internal/printer/version.go b/internal/printer/version.go new file mode 100644 index 0000000..695fbb0 --- /dev/null +++ b/internal/printer/version.go @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// SPDX-License-Identifier: GPL-3.0-or-later + +package printer + +import ( + "strings" + "text/tabwriter" +) + +func (p Printer) PrintVersion(showFullVersion bool, binaryVersion, buildTime, goVersion, gitCommit string) { + if !showFullVersion { + printToStdout("Enbas " + binaryVersion + "\n") + + return + } + + var builder strings.Builder + + builder.WriteString(p.headerFormat("Enbas") + "\n\n") + + tableWriter := tabwriter.NewWriter(&builder, 0, 4, 1, ' ', 0) + + _, _ = tableWriter.Write([]byte(p.fieldFormat("Version:") + "\t" + binaryVersion + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Git commit:") + "\t" + gitCommit + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Go version:") + "\t" + goVersion + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Build date:") + "\t" + buildTime + "\n")) + + tableWriter.Flush() + + printToStdout(builder.String()) +} diff --git a/internal/utilities/displayer.go b/internal/utilities/displayer.go deleted file mode 100644 index a595c2a..0000000 --- a/internal/utilities/displayer.go +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dan Anglin -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package utilities - -import ( - "fmt" - "os" - "os/exec" - "strings" -) - -type Displayer interface { - Display(noColor bool) string -} - -func Display(displayer Displayer, noColor bool, pagerCommand string) { - if pagerCommand == "" { - os.Stdout.WriteString(displayer.Display(noColor) + "\n") - - return - } - - split := strings.Split(pagerCommand, " ") - - pager := new(exec.Cmd) - - if len(split) == 1 { - pager = exec.Command(split[0]) //nolint:gosec - } else { - pager = exec.Command(split[0], split[1:]...) //nolint:gosec - } - - pipe, err := pager.StdinPipe() - if err != nil { - os.Stdout.WriteString(displayer.Display(noColor) + "\n") - - return - } - - pager.Stdout = os.Stdout - pager.Stderr = os.Stderr - - _ = pager.Start() - - defer func() { - _ = pipe.Close() - _ = pager.Wait() - }() - - fmt.Fprintln(pipe, displayer.Display(noColor)+"\n") -} diff --git a/internal/utilities/format.go b/internal/utilities/format.go deleted file mode 100644 index 9509e7b..0000000 --- a/internal/utilities/format.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dan Anglin -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package utilities - -import ( - "regexp" - "strings" - "time" -) - -const ( - reset = "\033[0m" - boldblue = "\033[34;1m" - boldmagenta = "\033[35;1m" - green = "\033[32m" -) - -func HeaderFormat(noColor bool, text string) string { - if noColor { - return text - } - - return boldblue + text + reset -} - -func FieldFormat(noColor bool, text string) string { - if noColor { - return text - } - - return green + text + reset -} - -func FullDisplayNameFormat(noColor bool, displayName, acct string) string { - // use this pattern to remove all emoji strings - pattern := regexp.MustCompile(`\s:[A-Za-z0-9_]*:`) - - var builder strings.Builder - - if noColor { - builder.WriteString(pattern.ReplaceAllString(displayName, "")) - } else { - builder.WriteString(boldmagenta + pattern.ReplaceAllString(displayName, "") + reset) - } - - builder.WriteString(" (@" + acct + ")") - - return builder.String() -} - -func FormatDate(date time.Time) string { - return date.Local().Format("02 Jan 2006") -} - -func FormatTime(date time.Time) string { - return date.Local().Format("02 Jan 2006, 15:04 (MST)") -}