From bcd2ce5d7c24058b9b7c515942f4af6ca2695516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=9E=E3=83=AA=E3=82=A6=E3=82=B9?= Date: Sun, 4 Apr 2021 18:49:40 -0500 Subject: [PATCH] Implemented `whois` command and user profiles --- .gitignore | 1 + README.md | 6 ++++ mast/cmd.go | 16 +++++++-- mast/timeline.go | 24 +++++++++++-- tui/profile.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ tui/tui.go | 61 +++++++++++++++++++++++++------ 6 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 tui/profile.go diff --git a/.gitignore b/.gitignore index f1fc23f..25600d3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ .DS_Store /gomphotherium +error.log diff --git a/README.md b/README.md index b2adad6..5c3b4e0 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,9 @@ Leave **Command** mode (while in **Command** mode) `hashtag`*` tag [local]`* \ Switch between timelines +`whois`*` user`* \ +Switch to user profile and timeline + `t`*` content ...`* \ `toot`*` content ...`* \ Publish a new public toot @@ -193,6 +196,9 @@ the *local* instance `hashtag lol` \ Switch to the hashtag timeline and search for the hashtag *#lol* globally +`whois mrus@merveilles.town` \ +Switch to the profile of *mrus@merveilles.town* and list his toots + `t Hello World!` \ Publish a new public toot that says *Hello World!* diff --git a/mast/cmd.go b/mast/cmd.go index b9f89f0..abc36e5 100644 --- a/mast/cmd.go +++ b/mast/cmd.go @@ -16,6 +16,7 @@ const ( CodeOk CmdReturnCode = 0 CodeNotOk = 1 CodeCommandNotFound = 2 + CodeUserNotFound = 3 CodeQuit = -1 CodeHelp = -2 @@ -32,6 +33,8 @@ func CmdAvailable() ([]string) { "notifications", "hashtag", + "whois", + "t", "toot", @@ -70,8 +73,6 @@ func CmdAvailable() ([]string) { "open", "share", - "whois", - // "search", "help", @@ -152,6 +153,17 @@ func CmdProcessor(timeline *Timeline, input string) (CmdReturnCode) { timeline.Switch(TimelineHashtag, &timelineOptions) return CodeOk case "whois": + accounts, err := timeline.SearchUser(args, 1) + if err != nil || len(accounts) < 1 { + // TODO: pass info back to caller + return CodeUserNotFound + } + + timelineOptions := TimelineOptions{ + User: *accounts[0], + } + + timeline.Switch(TimelineUser, &timelineOptions) return CodeOk case "t", "toot": return CmdToot(timeline, args, -1, VisibilityPublic) diff --git a/mast/timeline.go b/mast/timeline.go index 747b344..07c1719 100644 --- a/mast/timeline.go +++ b/mast/timeline.go @@ -13,12 +13,13 @@ const ( TimelinePublic = 2 TimelineNotifications = 3 TimelineHashtag = 4 - TimelineEnd = 5 + TimelineUser = 5 ) type TimelineOptions struct { IsLocal bool Hashtag string + User mastodon.Account } type Timeline struct { @@ -48,7 +49,9 @@ func NewTimeline(mastodonClient *mastodon.Client) Timeline { } func (timeline *Timeline) Switch(timelineType TimelineType, options *TimelineOptions) { - if timeline.timelineType != timelineType { + if timeline.timelineType != timelineType || + timelineType == TimelineHashtag || + timelineType == TimelineUser { timeline.timelineType = timelineType if options != nil { timeline.timelineOptions = *options @@ -63,6 +66,10 @@ func (timeline *Timeline) GetCurrentType() (TimelineType) { return timeline.timelineType } +func (timeline *Timeline) GetCurrentOptions() (TimelineOptions) { + return timeline.timelineOptions +} + func (timeline *Timeline) Load() (error) { var statuses []*mastodon.Status var err error @@ -102,6 +109,13 @@ func (timeline *Timeline) Load() (error) { timeline.timelineOptions.IsLocal, nil, ) + case TimelineUser: + statuses, err = + timeline.client.GetAccountStatuses( + context.Background(), + timeline.timelineOptions.User.ID, + nil, + ) } if err != nil { @@ -185,3 +199,9 @@ func (timeline *Timeline) Fav( } return timeline.client.Unfavourite(context.Background(), id) } + +func (timeline *Timeline) SearchUser( + query string, + limit int64) ([]*mastodon.Account, error) { + return timeline.client.AccountsSearch(context.Background(), query, limit) +} diff --git a/tui/profile.go b/tui/profile.go new file mode 100644 index 0000000..04bed99 --- /dev/null +++ b/tui/profile.go @@ -0,0 +1,93 @@ +package tui + +import ( + "fmt" + "math" + // "time" + // "context" + + "github.com/mattn/go-runewidth" + "github.com/grokify/html-strip-tags-go" + "html" + + // "image/color" + // "github.com/eliukblau/pixterm/pkg/ansimage" + + "github.com/mattn/go-mastodon" + // "github.com/mrusme/gomphotherium/mast" +) + +func RenderProfile( + profile *mastodon.Account, + width int, + showImages bool) (string, error) { + var output string = "" + var err error = nil + + account := profile.Acct + if account == "" { + account = profile.Username + } + + bot := "" + if profile.Bot == true { + bot = "\xF0\x9F\xA4\x96" + } + + output = fmt.Sprintf("%s[blue]%s[-] [grey]%s[-] [red]%s[-]\n%s\n\n", + output, + profile.DisplayName, + account, + bot, + runewidth.Truncate( + html.UnescapeString(strip.StripTags(profile.Note)), + width, + "...", + ), + ) + + halfwidth := int(math.Floor(float64(width))) + + fieldsNumber := len(profile.Fields) + if fieldsNumber > 4 { + fieldsNumber = 4 + } + + for i := 0; i < fieldsNumber; i++ { + field := profile.Fields[i] + output = fmt.Sprintf("%s[grey]%s:[-] [magenta]%s[-]\n", + output, + runewidth.Truncate( + field.Name, + halfwidth, + "...", + ), + runewidth.Truncate( + html.UnescapeString(strip.StripTags(field.Value)), + halfwidth, + "...", + ), + ) + } + + for i := fieldsNumber; i < 4; i++ { + output = fmt.Sprintf("%s\n", output) + } + + output = fmt.Sprintf("%s[blue]%d[-] [grey]toots[-] ", + output, + profile.StatusesCount, + ) + + output = fmt.Sprintf("%s[blue]%d[-] [grey]followers[-] ", + output, + profile.FollowersCount, + ) + + output = fmt.Sprintf("%s[blue]%d[-] [grey]following[-] ", + output, + profile.FollowingCount, + ) + + return output, err +} diff --git a/tui/tui.go b/tui/tui.go index 49a2960..e4beaa5 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -27,6 +27,7 @@ type TUICore struct { Client *mastodon.Client App *tview.Application CmdLine *tview.InputField + Profile *tview.TextView Stream *tview.TextView Grid *tview.Grid @@ -77,6 +78,19 @@ func TUI(tuiCore TUICore) { switch retCode { case mast.CodeOk: + if tuiCore.Timeline.GetCurrentType() == mast.TimelineUser && + tuiCore.RenderedTimelineType != mast.TimelineUser { + tuiCore.Grid. + RemoveItem(tuiCore.Stream). + AddItem(tuiCore.Profile, 0, 0, 1, 1, 0, 0, false). + AddItem(tuiCore.Stream, 1, 0, 1, 1, 0, 0, false) + } else if tuiCore.RenderedTimelineType == mast.TimelineUser && + tuiCore.Timeline.GetCurrentType() != mast.TimelineUser { + tuiCore.Grid. + RemoveItem(tuiCore.Profile). + RemoveItem(tuiCore.Stream). + AddItem(tuiCore.Stream, 0, 0, 2, 1, 0, 0, false) + } tuiCore.UpdateTimeline(true) case mast.CodeHelp: tuiCore.ShowHelp() @@ -86,17 +100,23 @@ func TUI(tuiCore TUICore) { } }) + tuiCore.Profile = tview.NewTextView(). + SetDynamicColors(true). + SetRegions(true). + SetWrap(true). + SetScrollable(false) + tuiCore.Stream = tview.NewTextView(). SetDynamicColors(true). SetRegions(true). SetWrap(true) tuiCore.Grid = tview.NewGrid(). - SetRows(0, 1). + SetRows(8, 0, 1). SetColumns(0). SetBorders(true). - AddItem(tuiCore.Stream, 0, 0, 1, 1, 0, 0, false). - AddItem(tuiCore.CmdLine, 1, 0, 1, 1, 0, 0, true) + AddItem(tuiCore.Stream, 0, 0, 2, 1, 0, 0, false). + AddItem(tuiCore.CmdLine, 2, 0, 1, 1, 0, 0, false) tuiCore.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { @@ -186,6 +206,15 @@ func (tuiCore *TUICore) UpdateTimeline(scrollToEnd bool) bool { return false } + currentTimelineType := tuiCore.Timeline.GetCurrentType() + if tuiCore.RenderedTimelineType != currentTimelineType || + currentTimelineType == mast.TimelineHashtag || + currentTimelineType == mast.TimelineUser { + tuiCore.Stream.Clear() + tuiCore.RenderedTimelineType = currentTimelineType + tuiCore.Timeline.LastRenderedIndex = -1 + } + output, err := RenderTimeline( &tuiCore.Timeline, w, @@ -197,19 +226,31 @@ func (tuiCore *TUICore) UpdateTimeline(scrollToEnd bool) bool { return false } - currentTimelineType := tuiCore.Timeline.GetCurrentType() - if tuiCore.RenderedTimelineType != currentTimelineType || - currentTimelineType == mast.TimelineHashtag { - tuiCore.Stream.Clear() - tuiCore.RenderedTimelineType = currentTimelineType - } - fmt.Fprint(tuiCore.Stream, tview.TranslateANSI(output)) if scrollToEnd == true { tuiCore.Stream.ScrollToEnd() } + if currentTimelineType == mast.TimelineUser { + tuiCore.Profile.Clear() + + options := tuiCore.Timeline.GetCurrentOptions() + + profileOutput, err := RenderProfile( + &options.User, + w, + tuiCore.Options.ShowImages, + ) + + if err != nil { + // TODO: Display errors somewhere + return false + } + + fmt.Fprint(tuiCore.Profile, tview.TranslateANSI(profileOutput)) + } + return true }