From bfa06a58c534b497276218d29374b7a3d6e7c644 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 23 Nov 2025 02:35:49 +0100 Subject: [PATCH 1/6] Switched from RasmusLindroth/go-mastodon to blacklight/go-mastodon The new fork has the required ContentType field in the Toot struct to support Markdown posts from Pleroma/Akkoma instances. --- api/feed.go | 2 +- api/item.go | 2 +- api/poll.go | 2 +- api/status.go | 2 +- api/stream.go | 2 +- api/tags.go | 2 +- api/types.go | 2 +- api/user.go | 2 +- auth/add.go | 2 +- feed/feed.go | 2 +- go.mod | 3 ++- go.sum | 2 ++ ui/commands.go | 2 +- ui/composeview.go | 2 +- ui/feed.go | 2 +- ui/input.go | 2 +- ui/item.go | 2 +- ui/item_list.go | 2 +- ui/item_status.go | 2 +- ui/item_tag.go | 2 +- ui/media.go | 2 +- ui/pollview.go | 2 +- ui/preferenceview.go | 2 +- ui/tutview.go | 2 +- ui/view.go | 2 +- ui/voteview.go | 2 +- util/util.go | 2 +- 27 files changed, 29 insertions(+), 26 deletions(-) diff --git a/api/feed.go b/api/feed.go index 79f8eca..d18c061 100644 --- a/api/feed.go +++ b/api/feed.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/config" ) diff --git a/api/item.go b/api/item.go index 7e31720..562d21e 100644 --- a/api/item.go +++ b/api/item.go @@ -4,7 +4,7 @@ import ( "strings" "sync" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" "golang.org/x/exp/slices" diff --git a/api/poll.go b/api/poll.go index 4e394d5..2f1c260 100644 --- a/api/poll.go +++ b/api/poll.go @@ -3,7 +3,7 @@ package api import ( "context" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" ) func (ac *AccountClient) Vote(poll *mastodon.Poll, choices ...int) (*mastodon.Poll, error) { diff --git a/api/status.go b/api/status.go index 7ca8515..b53357c 100644 --- a/api/status.go +++ b/api/status.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/util" ) diff --git a/api/stream.go b/api/stream.go index 01e2ecd..f3a71ed 100644 --- a/api/stream.go +++ b/api/stream.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" ) type MastodonType uint diff --git a/api/tags.go b/api/tags.go index b8fab99..412c1a6 100644 --- a/api/tags.go +++ b/api/tags.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" ) func (ac *AccountClient) FollowTag(tag string) error { diff --git a/api/types.go b/api/types.go index a969ff8..42c59ce 100644 --- a/api/types.go +++ b/api/types.go @@ -1,6 +1,6 @@ package api -import "github.com/RasmusLindroth/go-mastodon" +import "github.com/blacklight/go-mastodon" type RequestData struct { MinID mastodon.ID diff --git a/api/user.go b/api/user.go index 744901e..dca5358 100644 --- a/api/user.go +++ b/api/user.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" ) func (ac *AccountClient) GetUserByID(id mastodon.ID) (Item, error) { diff --git a/auth/add.go b/auth/add.go index 66ba46b..3556f32 100644 --- a/auth/add.go +++ b/auth/add.go @@ -8,7 +8,7 @@ import ( "os" "strings" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/util" ) diff --git a/feed/feed.go b/feed/feed.go index 6565b91..06c0661 100644 --- a/feed/feed.go +++ b/feed/feed.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "golang.org/x/exp/slices" diff --git a/go.mod b/go.mod index a47ce2f..7ef40b1 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/RasmusLindroth/tut go 1.18 require ( - github.com/RasmusLindroth/go-mastodon v0.0.21 github.com/adrg/xdg v0.4.0 github.com/atotto/clipboard v0.1.4 + github.com/blacklight/go-mastodon v0.0.23 github.com/gdamore/tcell/v2 v2.5.4 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 github.com/gobwas/glob v0.2.3 @@ -21,6 +21,7 @@ require ( ) require ( + github.com/RasmusLindroth/go-mastodon v0.0.21 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect diff --git a/go.sum b/go.sum index e7b9a45..cd83df6 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/blacklight/go-mastodon v0.0.23 h1:gU2FMkXAQYz505SXCe4t3N/b1HwsobEEb9VpAossqgQ= +github.com/blacklight/go-mastodon v0.0.23/go.mod h1:xXQxOPtM05cbeq/p5eBPFjawo6520+wdamVa7MxLQwo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/ui/commands.go b/ui/commands.go index e975224..e078dd5 100644 --- a/ui/commands.go +++ b/ui/commands.go @@ -5,7 +5,7 @@ import ( "os" "strconv" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" diff --git a/ui/composeview.go b/ui/composeview.go index f35de89..b3a55a1 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" diff --git a/ui/feed.go b/ui/feed.go index 30ae6e8..6c4e80d 100644 --- a/ui/feed.go +++ b/ui/feed.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/feed" diff --git a/ui/input.go b/ui/input.go index 39a3fbc..1d1effb 100644 --- a/ui/input.go +++ b/ui/input.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" diff --git a/ui/item.go b/ui/item.go index d7ba959..eac862c 100644 --- a/ui/item.go +++ b/ui/item.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/icza/gox/timex" diff --git a/ui/item_list.go b/ui/item_list.go index c2a9911..77a7358 100644 --- a/ui/item_list.go +++ b/ui/item_list.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/rivo/tview" ) diff --git a/ui/item_status.go b/ui/item_status.go index 3c93905..a7310d5 100644 --- a/ui/item_status.go +++ b/ui/item_status.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/config" "github.com/RasmusLindroth/tut/util" diff --git a/ui/item_tag.go b/ui/item_tag.go index f0acde6..c9fb204 100644 --- a/ui/item_tag.go +++ b/ui/item_tag.go @@ -5,7 +5,7 @@ import ( "strconv" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/rivo/tview" ) diff --git a/ui/media.go b/ui/media.go index c97813c..9159581 100644 --- a/ui/media.go +++ b/ui/media.go @@ -8,7 +8,7 @@ import ( "os/exec" "path/filepath" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/atotto/clipboard" ) diff --git a/ui/pollview.go b/ui/pollview.go index 9825ca7..ec0b6df 100644 --- a/ui/pollview.go +++ b/ui/pollview.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) diff --git a/ui/preferenceview.go b/ui/preferenceview.go index 747334a..7812f96 100644 --- a/ui/preferenceview.go +++ b/ui/preferenceview.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) diff --git a/ui/tutview.go b/ui/tutview.go index 5494c8c..cbd7239 100644 --- a/ui/tutview.go +++ b/ui/tutview.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" "github.com/RasmusLindroth/tut/auth" "github.com/RasmusLindroth/tut/config" diff --git a/ui/view.go b/ui/view.go index 92519fd..86127d9 100644 --- a/ui/view.go +++ b/ui/view.go @@ -3,7 +3,7 @@ package ui import ( "log" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/RasmusLindroth/tut/api" ) diff --git a/ui/voteview.go b/ui/voteview.go index 599db67..29db282 100644 --- a/ui/voteview.go +++ b/ui/voteview.go @@ -3,7 +3,7 @@ package ui import ( "fmt" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/rivo/tview" ) diff --git a/util/util.go b/util/util.go index 738a313..0981fb5 100644 --- a/util/util.go +++ b/util/util.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/RasmusLindroth/go-mastodon" + "github.com/blacklight/go-mastodon" "github.com/adrg/xdg" "github.com/microcosm-cc/bluemonday" "github.com/rivo/tview" From 65db4c723b59873a0c640ec5918c858a7accc332 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 8 Dec 2025 19:46:27 +0100 Subject: [PATCH 2/6] Implement support for custom content types (e.g. Markdown) in posts --- config/config.go | 3 + config/toml.go | 3 + config/toml_default.go | 4 + go.mod | 5 +- go.sum | 4 +- ui/composeview.go | 161 ++++++++++++++++++++++++++++++++++++++--- ui/input.go | 4 + 7 files changed, 172 insertions(+), 12 deletions(-) diff --git a/config/config.go b/config/config.go index 8413160..f31e7ba 100644 --- a/config/config.go +++ b/config/config.go @@ -198,6 +198,7 @@ type General struct { ShowBoostedUser bool DynamicTimelineName bool CommandsInNewPane bool + DefaultContentType string } type Style struct { @@ -501,6 +502,7 @@ type Input struct { ComposeVisibility Key ComposeLanguage Key ComposePoll Key + ComposeFormat Key MediaDelete Key MediaEditDesc Key @@ -1345,6 +1347,7 @@ func parseInput(cfg InputTOML) Input { ic.ComposeToggleContentWarning = inputOrDef("compose-toggle-content-warning", cfg.ComposeToggleContentWarning, def.ComposeToggleContentWarning, false) ic.ComposeVisibility = inputOrDef("compose-visibility", cfg.ComposeVisibility, def.ComposeVisibility, false) ic.ComposeLanguage = inputOrDef("compose-language", cfg.ComposeLanguage, def.ComposeLanguage, false) + ic.ComposeFormat = inputOrDef("compose-format", cfg.ComposeFormat, def.ComposeFormat, false) ic.ComposePoll = inputOrDef("compose-poll", cfg.ComposePoll, def.ComposePoll, false) ic.MediaDelete = inputOrDef("media-delete", cfg.MediaDelete, def.MediaDelete, false) diff --git a/config/toml.go b/config/toml.go index 6270978..fbcc447 100644 --- a/config/toml.go +++ b/config/toml.go @@ -8,6 +8,7 @@ type ConfigTOML struct { OpenCustom OpenCustomTOML `toml:"open-custom"` NotificationConfig NotificationsTOML `toml:"desktop-notification"` Input InputTOML `toml:"input"` + DefaultContentType string `toml:"default-content-type"` // e.g. "text/plain" or "text/markdown" } type GeneralTOML struct { @@ -38,6 +39,7 @@ type GeneralTOML struct { ShowBoostedUser *bool `toml:"show-boosted-user"` DynamicTimelineName *bool `toml:"dynamic-timeline-name"` CommandsInNewPane *bool `toml:"commands-in-new-pane"` + DefaultContentType string `toml:"default-content-type"` } type TimelineTOML struct { @@ -231,6 +233,7 @@ type InputTOML struct { ComposeVisibility *KeyHintTOML `toml:"compose-visibility"` ComposeLanguage *KeyHintTOML `toml:"compose-language"` ComposePoll *KeyHintTOML `toml:"compose-poll"` + ComposeFormat *KeyHintTOML `toml:"compose-format"` MediaDelete *KeyHintTOML `toml:"media-delete"` MediaEditDesc *KeyHintTOML `toml:"media-edit-desc"` diff --git a/config/toml_default.go b/config/toml_default.go index bdaa5fc..64bbb02 100644 --- a/config/toml_default.go +++ b/config/toml_default.go @@ -365,6 +365,10 @@ var ConfigDefault = ConfigTOML{ Hint: sp("P[O]ll"), Keys: &[]string{"o", "O"}, }, + ComposeFormat: &KeyHintTOML{ + Hint: sp("[F]ormat"), + Keys: &[]string{"f", "F"}, + }, MediaDelete: &KeyHintTOML{ Hint: sp("[D]elete"), Keys: &[]string{"d", "D"}, diff --git a/go.mod b/go.mod index 7ef40b1..73582aa 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.18 require ( github.com/adrg/xdg v0.4.0 github.com/atotto/clipboard v0.1.4 - github.com/blacklight/go-mastodon v0.0.23 + // TODO Replace this with github.com/RasmusLindroth/go-mastodon again and the + // appropriate tag once https://github.com/RasmusLindroth/go-mastodon/pull/2 + // is merged + github.com/blacklight/go-mastodon v0.0.27 github.com/gdamore/tcell/v2 v2.5.4 github.com/gen2brain/beeep v0.0.0-20220909211152-5a9ec94374f6 github.com/gobwas/glob v0.2.3 diff --git a/go.sum b/go.sum index cd83df6..4a762c1 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/blacklight/go-mastodon v0.0.23 h1:gU2FMkXAQYz505SXCe4t3N/b1HwsobEEb9VpAossqgQ= -github.com/blacklight/go-mastodon v0.0.23/go.mod h1:xXQxOPtM05cbeq/p5eBPFjawo6520+wdamVa7MxLQwo= +github.com/blacklight/go-mastodon v0.0.27 h1:LKvrQjSW+9yfWW1cq3iujUubB1o+wnyjYuAsuPC9MHc= +github.com/blacklight/go-mastodon v0.0.27/go.mod h1:XbpBOtPLP6H1PtLdUHq2cBLzekcJiD/TvxyeiVqpwDE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/ui/composeview.go b/ui/composeview.go index b3a55a1..30a229e 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "time" @@ -30,6 +31,7 @@ type msgToot struct { QuoteIncluded bool Visibility string Language string + ContentType string } type ComposeView struct { @@ -44,6 +46,7 @@ type ComposeView struct { controls *tview.Flex visibility *tview.DropDown lang *tview.DropDown + format *tview.DropDown media *MediaList msg *msgToot } @@ -73,8 +76,37 @@ func NewComposeView(tv *TutView) *ComposeView { info: NewTextView(tv.tut.Config), visibility: NewDropDown(tv.tut.Config), lang: NewDropDown(tv.tut.Config), + format: NewDropDown(tv.tut.Config), media: NewMediaList(tv), + // initialize the message early to avoid nil deref in callbacks + msg: &msgToot{}, + } + + // set format options and handler + cv.format.SetLabel("Format: ") + cv.format.SetOptions([]string{"Text (text/plain)", "Markdown (text/markdown)"}, func(text string, index int) { + switch index { + case 1: + cv.msg.ContentType = "text/markdown" + default: + cv.msg.ContentType = "text/plain" + } + cv.UpdateContent() + }) + + // set default based on config (if present) otherwise default to text/plain + if tv.tut.Config != nil && tv.tut.Config.General.DefaultContentType != "" { + cv.msg.ContentType = tv.tut.Config.General.DefaultContentType + if cv.msg.ContentType == "text/markdown" { + cv.format.SetCurrentOption(1) + } else { + cv.format.SetCurrentOption(0) + } + } else { + cv.msg.ContentType = "text/plain" + cv.format.SetCurrentOption(0) } + cv.content.SetDynamicColors(true) cv.View = newComposeUI(cv) return cv @@ -93,6 +125,7 @@ func newComposeUI(cv *ComposeView) *tview.Flex { AddItem(tview.NewFlex().SetDirection(tview.FlexRow). AddItem(cv.visibility, 1, 0, false). AddItem(cv.lang, 1, 0, false). + AddItem(cv.format, 1, 0, false). AddItem(cv.info, 5, 0, false). AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false). AddItem(cv.input.View, 1, 0, false). @@ -172,6 +205,7 @@ func (cv *ComposeView) SetControls(ctrl ComposeControls) { items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeMediaFocus, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposePoll, true)) items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeLanguage, true)) + items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeFormat, true)) if cv.msg.Reply != nil { items = append(items, NewControl(cv.tutView.tut.Config, cv.tutView.tut.Config.Input.ComposeIncludeQuote, true)) } @@ -237,6 +271,42 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) msg.ID = source.ID msg.Text = source.Text msg.CWText = source.SpoilerText + + // update the dropdown selection to reflect the msg.ContentType: + if cv.msg.ContentType == "text/markdown" { + cv.format.SetCurrentOption(1) + } else { + cv.format.SetCurrentOption(0) + } + + // Attempt to preserve ContentType from the fetched source if it exists. + // Some client libraries expose Source.ContentType; others don't. + // Use reflection so this code compiles regardless of whether the client type contains the field. + preserved := "" + v := reflect.ValueOf(source) + if v.IsValid() { + // if pointer, get element + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.IsValid() && v.Kind() == reflect.Struct { + f := v.FieldByName("ContentType") + if f.IsValid() && f.Kind() == reflect.String { + preserved = f.String() + } + } + } + if preserved != "" { + msg.ContentType = preserved + } else { + // fall back to configured default or html + if cv.tutView.tut.Config != nil && cv.tutView.tut.Config.General.DefaultContentType != "" { + msg.ContentType = cv.tutView.tut.Config.General.DefaultContentType + } else { + msg.ContentType = "text/plain" + } + } + for _, mid := range edit.MediaAttachments { msg.MediaIDs = append(msg.MediaIDs, mid.ID) } @@ -345,10 +415,27 @@ func (cv *ComposeView) ToggleCW() { } func (cv *ComposeView) UpdateContent() { - cv.info.SetText(fmt.Sprintf("Chars left: %d\nCW: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, cv.tutView.PollView.HasPoll())) - normal := config.ColorMark(cv.tutView.tut.Config.Style.Text) - subtleColor := config.ColorMark(cv.tutView.tut.Config.Style.Subtle) - warningColor := config.ColorMark(cv.tutView.tut.Config.Style.WarningText) + // Safely determine whether there is a poll (PollView may be nil during init) + hasPoll := false + if cv != nil && cv.tutView != nil && cv.tutView.PollView != nil { + // PollView.HasPoll might still access internal fields, but only call it when PollView is set + hasPoll = cv.tutView.PollView.HasPoll() + } + + // Safely retrieve colors (config may not be fully initialized during early init) + var normal, subtleColor, warningColor string + if cv != nil && cv.tutView != nil && cv.tutView.tut != nil && cv.tutView.tut.Config != nil { + normal = config.ColorMark(cv.tutView.tut.Config.Style.Text) + subtleColor = config.ColorMark(cv.tutView.tut.Config.Style.Subtle) + warningColor = config.ColorMark(cv.tutView.tut.Config.Style.WarningText) + } else { + normal, subtleColor, warningColor = "", "", "" + } + + // Update info text (safe to call even if PollView is nil) + if cv.info != nil { + cv.info.SetText(fmt.Sprintf("Chars left: %d\nCW: %t\nHas poll: %t\n", cv.msgLength(), cv.msg.Sensitive, hasPoll)) + } var outputHead string var output string @@ -370,7 +457,7 @@ func (cv *ComposeView) UpdateContent() { } if !cv.tutView.tut.Config.General.UseInternalEditor { - if cv.msg.Sensitive && cv.msg.CWText != "" { + if cv.msg.Sensitive && cv.tutView.tut != nil && cv.msg.CWText != "" { outputHead += subtleColor + "Content warning\n\n" + normal outputHead += tview.Escape(cv.msg.CWText) outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal @@ -380,7 +467,9 @@ func (cv *ComposeView) UpdateContent() { output = strings.TrimSpace(outputHead) } - cv.content.SetText(output) + if cv.content != nil { + cv.content.SetText(output) + } } func (cv *ComposeView) IncludeQuote() { @@ -514,21 +603,75 @@ func (cv *ComposeView) FocusLang() { cv.tutView.tut.App.QueueEvent(ev) } +func (cv *ComposeView) formatInput(event *tcell.EventKey) *tcell.EventKey { + if cv.tutView.tut.Config.Input.GlobalDown.Match(event.Key(), event.Rune()) { + return tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone) + } + if cv.tutView.tut.Config.Input.GlobalUp.Match(event.Key(), event.Rune()) { + return tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone) + } + if cv.tutView.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) || + cv.tutView.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) { + cv.exitFormat() + return nil + } + return event +} + +func (cv *ComposeView) exitFormat() { + cv.tutView.tut.App.SetInputCapture(cv.tutView.Input) + cv.tutView.tut.App.SetFocus(cv.content) +} + +func (cv *ComposeView) FocusFormat() { + cv.tutView.tut.App.SetInputCapture(cv.formatInput) + cv.tutView.tut.App.SetFocus(cv.format) + ev := tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone) + cv.tutView.tut.App.QueueEvent(ev) +} + func (cv *ComposeView) Post() { toot := cv.msg + sendText := strings.TrimSpace(toot.Text) + sendCW := strings.TrimSpace(toot.CWText) send := mastodon.Toot{ - Status: strings.TrimSpace(toot.Text), + Status: sendText, + InReplyToID: "", + QuoteID: nil, + MediaIDs: []mastodon.ID{}, + Sensitive: toot.Sensitive, + SpoilerText: sendCW, + Visibility: toot.Visibility, + ContentType: toot.ContentType, + Poll: nil, + ScheduledAt: nil, + Language: toot.Language, } if toot.Reply != nil { - send.InReplyToID = toot.Reply.ID + id := mastodon.ID(toot.Reply.ID) + send.InReplyToID = id } if toot.Edit != nil && toot.Edit.InReplyToID != nil { - send.InReplyToID = mastodon.ID(toot.Edit.InReplyToID.(string)) + var id = mastodon.ID(toot.Edit.InReplyToID.(string)) + send.InReplyToID = id } if toot.Sensitive { send.Sensitive = true send.SpoilerText = toot.CWText } + if len(toot.MediaIDs) > 0 { + // convert []mastodon.ID (if your msg uses same type) into send.MediaIDs + send.MediaIDs = make([]mastodon.ID, 0, len(toot.MediaIDs)) + for _, mid := range toot.MediaIDs { + send.MediaIDs = append(send.MediaIDs, mid) + } + } + if toot.ContentType != "" { + send.ContentType = toot.ContentType + } else { + // fallback default + send.ContentType = "text/plain" + } if cv.HasMedia() { attachments := cv.media.Files diff --git a/ui/input.go b/ui/input.go index 1d1effb..4ba4985 100644 --- a/ui/input.go +++ b/ui/input.go @@ -946,6 +946,10 @@ func (tv *TutView) InputComposeView(event *tcell.EventKey) *tcell.EventKey { tv.ComposeView.FocusLang() return nil } + if tv.tut.Config.Input.ComposeFormat.Match(event.Key(), event.Rune()) { + tv.ComposeView.FocusFormat() + return nil + } if tv.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) || tv.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) { tv.ModalView.Run( From c3d5b19da164f8534770c8eac02d0922f5e27029 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 Dec 2025 00:02:03 +0100 Subject: [PATCH 3/6] Better handling of the content type input lifecycle in compose view --- ui/composeview.go | 85 +++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/ui/composeview.go b/ui/composeview.go index 30a229e..29204e9 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -63,6 +63,10 @@ var visibilitiesStr = []string{ mastodon.VisibilityFollowersOnly, mastodon.VisibilityDirectMessage, } +var contentTypes = []string{ + "text/plain", + "text/markdown", +} func NewComposeView(tv *TutView) *ComposeView { cv := &ComposeView{ @@ -82,25 +86,19 @@ func NewComposeView(tv *TutView) *ComposeView { msg: &msgToot{}, } - // set format options and handler - cv.format.SetLabel("Format: ") - cv.format.SetOptions([]string{"Text (text/plain)", "Markdown (text/markdown)"}, func(text string, index int) { - switch index { - case 1: - cv.msg.ContentType = "text/markdown" - default: - cv.msg.ContentType = "text/plain" - } - cv.UpdateContent() - }) - // set default based on config (if present) otherwise default to text/plain if tv.tut.Config != nil && tv.tut.Config.General.DefaultContentType != "" { cv.msg.ContentType = tv.tut.Config.General.DefaultContentType - if cv.msg.ContentType == "text/markdown" { - cv.format.SetCurrentOption(1) - } else { - cv.format.SetCurrentOption(0) + var ctIndex int = -1 + for i, ct := range contentTypes { + if cv.msg.ContentType == ct { + ctIndex = i + break + } + } + + if ctIndex >= 0 { + cv.format.SetCurrentOption(ctIndex) } } else { cv.msg.ContentType = "text/plain" @@ -147,6 +145,7 @@ func newComposeUI(cv *ComposeView) *tview.Flex { AddItem(tview.NewFlex().SetDirection(tview.FlexRow). AddItem(cv.visibility, 1, 0, false). AddItem(cv.lang, 1, 0, false). + AddItem(cv.format, 1, 0, false). AddItem(cv.info, 5, 0, false). AddItem(cv.media.View, 0, 1, false), 0, 1, false), 0, 1, false). AddItem(cv.input.View, 1, 0, false). @@ -272,17 +271,10 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) msg.Text = source.Text msg.CWText = source.SpoilerText - // update the dropdown selection to reflect the msg.ContentType: - if cv.msg.ContentType == "text/markdown" { - cv.format.SetCurrentOption(1) - } else { - cv.format.SetCurrentOption(0) - } - // Attempt to preserve ContentType from the fetched source if it exists. // Some client libraries expose Source.ContentType; others don't. // Use reflection so this code compiles regardless of whether the client type contains the field. - preserved := "" + preservedContentType := "" v := reflect.ValueOf(source) if v.IsValid() { // if pointer, get element @@ -292,14 +284,14 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) if v.IsValid() && v.Kind() == reflect.Struct { f := v.FieldByName("ContentType") if f.IsValid() && f.Kind() == reflect.String { - preserved = f.String() + preservedContentType = f.String() } } } - if preserved != "" { - msg.ContentType = preserved + if preservedContentType != "" { + msg.ContentType = preservedContentType } else { - // fall back to configured default or html + // fall back to configured default or plain text if cv.tutView.tut.Config != nil && cv.tutView.tut.Config.General.DefaultContentType != "" { msg.ContentType = cv.tutView.tut.Config.General.DefaultContentType } else { @@ -321,6 +313,13 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) } cv.msg = msg + } else { + // Set the ContentType to the default for new toots + if cv.tutView.tut.Config != nil && cv.tutView.tut.Config.General.DefaultContentType != "" { + cv.msg.ContentType = cv.tutView.tut.Config.General.DefaultContentType + } else { + cv.msg.ContentType = "text/plain" + } } if cv.tutView.tut.Config.General.QuoteReply && edit == nil { @@ -338,6 +337,18 @@ func (cv *ComposeView) SetStatus(reply *mastodon.Status, edit *mastodon.Status) cv.visibility.SetCurrentOption(index) cv.visibility.SetInputCapture(cv.visibilityInput) + cv.format.SetLabel("Format: ") + index = 0 + for i, ct := range contentTypes { + if cv.msg.ContentType == ct { + index = i + break + } + } + cv.format.SetOptions(contentTypes, cv.formatSelected) + cv.format.SetCurrentOption(index) + cv.format.SetInputCapture(cv.formatInput) + cv.lang.SetLabel("Lang: ") langStrs := []string{} for i, l := range util.Languages { @@ -546,6 +557,7 @@ func (cv *ComposeView) visibilityInput(event *tcell.EventKey) *tcell.EventKey { if cv.tutView.tut.Config.Input.GlobalExit.Match(event.Key(), event.Rune()) || cv.tutView.tut.Config.Input.GlobalBack.Match(event.Key(), event.Rune()) { cv.exitVisibility() + cv.exitFormat() return nil } return event @@ -561,6 +573,18 @@ func (cv *ComposeView) visibilitySelected(s string, index int) { cv.exitVisibility() } +func (cv *ComposeView) exitFormat() { + cv.tutView.tut.App.SetInputCapture(cv.tutView.Input) + cv.tutView.tut.App.SetFocus(cv.content) +} + +func (cv *ComposeView) formatSelected(s string, index int) { + if index >= 0 && index < len(contentTypes) { + cv.msg.ContentType = contentTypes[index] + } + cv.exitFormat() +} + func (cv *ComposeView) FocusVisibility() { cv.tutView.tut.App.SetInputCapture(cv.visibilityInput) cv.tutView.tut.App.SetFocus(cv.visibility) @@ -618,11 +642,6 @@ func (cv *ComposeView) formatInput(event *tcell.EventKey) *tcell.EventKey { return event } -func (cv *ComposeView) exitFormat() { - cv.tutView.tut.App.SetInputCapture(cv.tutView.Input) - cv.tutView.tut.App.SetFocus(cv.content) -} - func (cv *ComposeView) FocusFormat() { cv.tutView.tut.App.SetInputCapture(cv.formatInput) cv.tutView.tut.App.SetFocus(cv.format) From 3bf3ad77a5bf95988fa5bb1666ec77cc950eca60 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 Dec 2025 00:41:39 +0100 Subject: [PATCH 4/6] Add default content type configuration Updated config.example.toml to include a default content type option for toots, allowing users to set a preferred type (e.g. text/plain or text/markdown) for supported backends. --- config.example.toml | 4 ++++ config/toml.go | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config.example.toml b/config.example.toml index fe9e2f4..7e497f2 100644 --- a/config.example.toml +++ b/config.example.toml @@ -129,6 +129,10 @@ leader-key="" # default=1000 leader-timeout=1000 +# If the backend supports it (e.g. Pleroma, Akkoma and Friendica), you can set +# the default content type for toots here (usually text/plain or text/markdown). +default-content-type="text/plain" + # [[general.timelines]] # Timelines adds panes of feeds. You can customize the number of feeds, what # they should show and the key to activate them. diff --git a/config/toml.go b/config/toml.go index fbcc447..16a3972 100644 --- a/config/toml.go +++ b/config/toml.go @@ -8,7 +8,6 @@ type ConfigTOML struct { OpenCustom OpenCustomTOML `toml:"open-custom"` NotificationConfig NotificationsTOML `toml:"desktop-notification"` Input InputTOML `toml:"input"` - DefaultContentType string `toml:"default-content-type"` // e.g. "text/plain" or "text/markdown" } type GeneralTOML struct { From 40d48d2de301d4045c4d3a4d4dc68c44ee99eb2a Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 Dec 2025 02:12:03 +0100 Subject: [PATCH 5/6] Ensure proper initialization of DefaultContentType in config --- config/config.go | 1 + config/toml.go | 2 +- config/toml_default.go | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index f31e7ba..a779ae9 100644 --- a/config/config.go +++ b/config/config.go @@ -842,6 +842,7 @@ func parseGeneral(cfg GeneralTOML) General { general.ShowBoostedUser = NilDefaultBool(cfg.ShowBoostedUser, def.ShowBoostedUser) general.DynamicTimelineName = NilDefaultBool(cfg.DynamicTimelineName, def.DynamicTimelineName) general.CommandsInNewPane = NilDefaultBool(cfg.CommandsInNewPane, def.CommandsInNewPane) + general.DefaultContentType = NilDefaultString(cfg.DefaultContentType, def.DefaultContentType) lp := NilDefaultString(cfg.ListPlacement, def.ListPlacement) switch lp { diff --git a/config/toml.go b/config/toml.go index 16a3972..28b67e4 100644 --- a/config/toml.go +++ b/config/toml.go @@ -38,7 +38,7 @@ type GeneralTOML struct { ShowBoostedUser *bool `toml:"show-boosted-user"` DynamicTimelineName *bool `toml:"dynamic-timeline-name"` CommandsInNewPane *bool `toml:"commands-in-new-pane"` - DefaultContentType string `toml:"default-content-type"` + DefaultContentType *string `toml:"default-content-type"` } type TimelineTOML struct { diff --git a/config/toml_default.go b/config/toml_default.go index 64bbb02..1f5a00e 100644 --- a/config/toml_default.go +++ b/config/toml_default.go @@ -43,6 +43,7 @@ var ConfigDefault = ConfigTOML{ TerminalTitle: ip(0), LeaderKey: sp(""), LeaderTimeout: ip64(1000), + DefaultContentType: sp("text/plain"), NotificationsToHide: &[]string{}, Timelines: &[]TimelineTOML{ { From 5566cf12ab2acc0f1480bbc7e6dff1f576fe3126 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 Dec 2025 02:19:29 +0100 Subject: [PATCH 6/6] Removed duplicate initialization of ContentType in NewComposeView --- ui/composeview.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/ui/composeview.go b/ui/composeview.go index 29204e9..7ddd509 100644 --- a/ui/composeview.go +++ b/ui/composeview.go @@ -86,25 +86,6 @@ func NewComposeView(tv *TutView) *ComposeView { msg: &msgToot{}, } - // set default based on config (if present) otherwise default to text/plain - if tv.tut.Config != nil && tv.tut.Config.General.DefaultContentType != "" { - cv.msg.ContentType = tv.tut.Config.General.DefaultContentType - var ctIndex int = -1 - for i, ct := range contentTypes { - if cv.msg.ContentType == ct { - ctIndex = i - break - } - } - - if ctIndex >= 0 { - cv.format.SetCurrentOption(ctIndex) - } - } else { - cv.msg.ContentType = "text/plain" - cv.format.SetCurrentOption(0) - } - cv.content.SetDynamicColors(true) cv.View = newComposeUI(cv) return cv