From 09cd13a2f7032cf740b37f6a81139a1eb814bd16 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Mon, 17 Jun 2024 00:27:56 +0100 Subject: [PATCH] checkpoint: printer now prints statuses and status lists --- internal/executor/show.go | 8 ++-- internal/model/status.go | 83 --------------------------------- internal/printer/account.go | 2 +- internal/printer/poll.go | 62 +++++++++++++++++++++++++ internal/printer/printer.go | 44 +++++++++++++++++- internal/printer/status.go | 93 +++++++++++++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 89 deletions(-) create mode 100644 internal/printer/poll.go create mode 100644 internal/printer/status.go diff --git a/internal/executor/show.go b/internal/executor/show.go index 97e1bbf..668f58c 100644 --- a/internal/executor/show.go +++ b/internal/executor/show.go @@ -171,7 +171,7 @@ func (s *ShowExecutor) showStatus(gtsClient *client.Client) error { return nil } - utilities.Display(status, false, "") + s.printer.PrintStatus(status) return nil } @@ -220,7 +220,7 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error { return nil } - utilities.Display(timeline, false, "") + s.printer.PrintStatusList(timeline) return nil } @@ -333,7 +333,7 @@ func (s *ShowExecutor) showBookmarks(gtsClient *client.Client) error { } if len(bookmarks.Statuses) > 0 { - utilities.Display(bookmarks, false, "") + s.printer.PrintStatusList(bookmarks) } else { s.printer.PrintInfo("You have no bookmarks.\n") } @@ -348,7 +348,7 @@ func (s *ShowExecutor) showLiked(gtsClient *client.Client) error { } if len(liked.Statuses) > 0 { - utilities.Display(liked, false, "") + s.printer.PrintStatusList(liked) } else { s.printer.PrintInfo("You have no " + s.resourceType + " statuses.\n") } 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 index 31fe418..e6c5b5e 100644 --- a/internal/printer/account.go +++ b/internal/printer/account.go @@ -102,7 +102,7 @@ func (p Printer) PrintAccountRelationship(relationship model.AccountRelationship builder.WriteString("\n" + utilities.WrapLines(relationship.PrivateNote, "\n", p.maxTerminalWidth)) } - builder.WriteString("\n") + builder.WriteString("\n\n") printToStdout(builder.String()) } diff --git a/internal/printer/poll.go b/internal/printer/poll.go new file mode 100644 index 0000000..72b9442 --- /dev/null +++ b/internal/printer/poll.go @@ -0,0 +1,62 @@ +package printer + +import ( + "math" + "strconv" + "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" +) + +func (p Printer) pollContent(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:") + " " + utilities.FormatTime(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 index a487461..c466fc9 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -6,6 +6,7 @@ package printer import ( "os" + "os/exec" "regexp" "strings" "time" @@ -16,6 +17,8 @@ type theme struct { boldblue string boldmagenta string green string + boldgreen string + grey string } type Printer struct { @@ -42,13 +45,15 @@ func NewPrinter( boldblue: "\033[34;1m", boldmagenta: "\033[35;1m", green: "\033[32m", + boldgreen: "\033[32;1m", + grey: "\033[90m", } return &Printer{ noColor: noColor, maxTerminalWidth: maxTerminalWidth, pager: pager, - statusSeparator: strings.Repeat("─", maxTerminalWidth), + statusSeparator: strings.Repeat("\u2501", maxTerminalWidth), bullet: "\u2022", pollMeterSymbol: "\u2501", successSymbol: "\u2714", @@ -117,6 +122,43 @@ 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) } diff --git a/internal/printer/status.go b/internal/printer/status.go new file mode 100644 index 0000000..437fdaa --- /dev/null +++ b/internal/printer/status.go @@ -0,0 +1,93 @@ +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.pollContent(*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.pollContent(*status.Poll)) + } + + builder.WriteString( + "\n\n" + + p.fieldFormat("Status ID:") + " " + statusID + "\t" + + p.fieldFormat("Created at:") + " " + utilities.FormatTime(createdAt) + + "\n", + ) + + builder.WriteString(p.statusSeparator + "\n") + } + + p.print(builder.String()) +}