fix: add a new internal printer #25

Manually merged
dananglin merged 1 commit from the-internal-printer into main 2024-06-17 19:12:06 +01:00
33 changed files with 938 additions and 808 deletions
Showing only changes of commit ccdd8b6530 - Show all commits

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -51,12 +51,6 @@ const (
flagVisibility = "visibility"
)
type TopLevelFlags struct {
ConfigDir string
NoColor *bool
Pager string
}
type MultiStringFlagValue []string
func (v *MultiStringFlagValue) String() string {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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,
)
}

View file

@ -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()
}

137
internal/printer/account.go Normal file
View file

@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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())
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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())
}

49
internal/printer/list.go Normal file
View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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())
}

95
internal/printer/poll.go Normal file
View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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
}

185
internal/printer/printer.go Normal file
View file

@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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)
}

View file

@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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())
}

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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())
}

View file

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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")
}

View file

@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
//
// 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)")
}