Help us shape the future of Go! Take the 2026 Go Developer Survey

app

package
v0.0.0-...-ea410ed Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 39 Imported by: 0

Documentation

Overview

Package app wires the aggregator together: it aggregates the active profile's subscriptions across the provider registry and renders the unified feed into the go-widgets UI scene. It is windowing-agnostic — the same App is driven by the CLI (render to PNG / serve) and by the native window front-end.

Index

Constants

View Source
const PreviewTextStep = 0.1

PreviewTextStep is how much one A−/A+ preview-toolbar tap changes the reader/preview text zoom.

Variables

This section is empty.

Functions

func AccountsToOptions

func AccountsToOptions(base feeds.Options, accts []settings.Account) feeds.Options

AccountsToOptions overlays stored per-provider credentials onto base feeds options, mapping each account's well-known field keys onto the matching Options fields. Absent fields leave the base value untouched, so flags still apply where no account overrides them.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App holds the runtime state: the provider registry, the user's persisted settings (profiles/theme/cache), the active profile's subscriptions, and the UI scene they render into.

func New

func New(cfg Config) *App

New builds an App with a scene themed and populated from the settings.

func (*App) ActivateAfterVault

func (a *App) ActivateAfterVault(onLoaded func())

ActivateAfterVault performs the deferred startup that reads the vault, but keeps the keychain prompt OFF the render thread so the window does not freeze behind it. The blocking keychain read runs on a goroutine; the state changes it enables — reactivating the authenticated providers, importing any missing browser session — are applied on the render thread via post (like every other background result), and onLoaded (also on the render thread) starts the feed load once the providers are live.

When biometric unlock is enabled, the vault read is gated by a LocalAuthentication (Touch ID / device password) prompt run on the same background goroutine. A denial leaves the accounts unauthenticated rather than failing the launch — the reader still opens, just without the sign-ins.

func (*App) AdjustPreviewTextScale

func (a *App) AdjustPreviewTextScale(delta float64)

AdjustPreviewTextScale nudges the reader/preview text zoom by delta (the A−/A+ preview-toolbar pills pass ±PreviewTextStep). The scene clamps it to the supported range; the change persists straight away (a pure-display preference, so no theme re-resolve or feed re-aggregation), and the new size is reported on the status line.

func (*App) ApplyAccounts

func (a *App) ApplyAccounts()

ApplyAccounts snapshots the scene's edited accounts into the settings, persists them, rebuilds the provider registry with the new credentials (Reddit switches to session-cookie authentication) while keeping the same HTTP recorder wired so the Network log keeps updating, then re-aggregates the feed. The front-end calls it after the accounts editor is committed.

func (*App) ApplySceneSettings

func (a *App) ApplySceneSettings()

func (*App) AutoImportSessions

func (a *App) AutoImportSessions()

func (*App) ClearDownloads

func (a *App) ClearDownloads()

ClearDownloads removes finished rows from the download panel.

func (*App) ClearRenderCache

func (a *App) ClearRenderCache()

ClearRenderCache empties the web-preview render cache (freeing its pixels) and reports it on the status line. Wired to the Settings view's "Clear render cache" control.

func (*App) DamageRects

func (a *App) DamageRects() []toolkit.Rect

DamageRects reports which rectangles of the current frame differ from the one before it, in the framebuffer's pixel coordinates, so a damage-aware host (toolkit.Surface.Damage) presents only what changed instead of re-blitting and re-presenting the whole window. During a streaming load that is the difference between re-presenting a small spinner and re-presenting the entire feed on every animation tick — the reader's single biggest load-time cost.

It is exact by construction: rather than enumerate which widgets animate (and risk freezing one it forgot), it diffs the two frame buffers the double- buffered Frame already keeps, via toolkit.DiffRects. Every changed pixel is inside a returned rectangle, so a host can never miss an update.

It must be called right after Frame, on the same (render) goroutine, so the "current" buffer is the frame Frame just produced. A size mismatch — the tick straddling a resize, when the buffers were reallocated — returns nil, which a host reads as "assume everything changed" and presents in full. Nothing changed returns no rectangles.

func (*App) DeferSceneWrites

func (a *App) DeferSceneWrites()

DeferSceneWrites routes view-model→scene mutations through a render-thread queue instead of applying them inline. The native-window front-end enables it so the background streaming-aggregation goroutine never mutates the scene the render thread is drawing (Frame drains the queue on the render thread). It must be called before the present loop and the aggregation goroutine start, so deferScene is set before any concurrent reader exists.

func (*App) DeleteProfile

func (a *App) DeleteProfile(i int)

DeleteProfile removes the profile at i from the settings editor, re-syncs the active-profile observable with the scene's re-clamped selection, and persists + re-aggregates. Front-ends route the settings editor's delete affordance here.

func (*App) Frame

func (a *App) Frame() (buf []byte, changed bool)

Frame returns the current framebuffer, redrawing into the back buffer only when the scene's damage sequence has advanced since the last call. While the scene is animating (a refresh is streaming in), it advances the animation clock by the real time elapsed since the last advance, so the moving indicator keeps a steady real-time speed however often the present loop repaints it; when nothing is loading the scene is idle and the damage gate skips the redraw entirely.

func (*App) ImportFollows

func (a *App) ImportFollows(ctx context.Context, kind source.Kind) (int, error)

ImportFollows pulls every account/subreddit/feed the connected provider for kind follows — via its source.FollowImporter capability — and adds any not-yet-subscribed ones to the active profile, then persists, re-aggregates, and reports on the status line. Each returned source.Subscription carries its own source+channel, so a provider that follows several channel forms (Reddit's r/ subreddits and u/ redditors) imports them all in one action. It requires a connected/authenticated provider; without one it prompts the user to connect an account. The network call runs on the caller's goroutine (the front-end launches it off the render thread) and the scene mutations are applied through a.post so they land on the render thread. The returned count is meaningful only on the inline (CLI/test) path.

func (*App) ImportRedditSessionFromFirefox

func (a *App) ImportRedditSessionFromFirefox() (bool, error)

ImportRedditSessionFromFirefox lifts the user's logged-in reddit_session cookie out of their Firefox profile and installs it as the Reddit account's session cookie: it writes the value into the accounts editor buffer (so the editor shows it), then commits through ApplyAccounts — persisting the account, rebuilding the provider registry so authenticated reads take effect immediately, and re-aggregating. It reports whether a cookie was imported and surfaces success/failure on the status line. A failure (Firefox not installed, no profile, or not logged into Reddit) leaves the existing credentials untouched.

func (*App) ImportRedditSubscriptions

func (a *App) ImportRedditSubscriptions(ctx context.Context) (int, error)

ImportRedditSubscriptions pulls every subreddit the connected Reddit account follows and adds any not-yet-subscribed ones as r/<name> subscriptions on the active profile, then persists, re-aggregates, and reports on the status line. It requires a connected (session-cookie) Reddit provider; without one it prompts the user to connect an account. It runs the network call on the caller's goroutine (the front-end launches it off the render thread) and applies the scene mutations through a.post so they land on the render thread. The returned count is meaningful only on the inline (CLI/test) path.

func (*App) ImportSessionFromFirefox

func (a *App) ImportSessionFromFirefox() (bool, error)

ImportSessionFromFirefox lifts the logged-in session cookie of the account currently selected in the editor (Instagram, TikTok or X) out of the user's Firefox profile and installs it as that provider's "session" field, then commits through ApplyAccounts so the authenticated home/following timeline takes effect immediately. Reddit uses its own richer flow (ImportRedditSessionFromFirefox); an unsupported selection is a no-op. It reports whether a cookie was imported and surfaces success/failure on the status line.

func (*App) Items

func (a *App) Items() []source.Item

Items returns the currently loaded feed.

func (*App) LaunchRedditSignIn

func (a *App) LaunchRedditSignIn() error

LaunchRedditSignIn opens Reddit's login page in the browser configured by the SignInBrowser setting, so the user can sign in there and (with Firefox) import the resulting session cookie. It surfaces progress/failure on the status line.

func (*App) LoadActiveTab

func (a *App) LoadActiveTab(ctx context.Context) []error

LoadActiveTab loads the tab the sidebar currently shows: a single subscription when one is selected (only that source is fetched — the per-tab lazy model), or a full streaming aggregate for the "All" filter. It backs both the initial load and pull-to-refresh, and always re-fetches (unlike the lazy first-visit load).

func (*App) LoadGroups

func (a *App) LoadGroups()

LoadGroups asynchronously fetches the configured Usenet server's full group list into the browse view (served from the provider cache after the first fetch). The front-end calls it when opening the browser.

func (*App) LoadMore

func (a *App) LoadMore(ctx context.Context) []error

LoadMore fetches the next page of every subscription that still has a live cursor and appends the new, deduplicated items to the shown feed. It is the infinite-scroll seam the scene's OnReachBottom trigger drives. It is a no-op when infinite scroll is off, when a load-more is already in flight (the guard coalesces the burst of at-bottom wheel events), or when no source has an unexhausted cursor. Returns the collected per-subscription failures.

func (*App) MarkSubSeen

func (a *App) MarkSubSeen(index int)

MarkSubSeen records a subscription's current high-water marker as seen and persists it, dropping its unseen count to zero — without changing which filter is active. ViewSub uses it when a group is opened; the sidebar context menu's "mark as read" uses it in place, leaving the current view untouched. A bad index or a marker-less sub (e.g. AllFilter) is a no-op.

func (*App) NeedsPresent

func (a *App) NeedsPresent() bool

NeedsPresent reports whether the next Frame would do anything worth blitting: a background write is queued, or the last frame was still animating or waiting on a debounce. A native present loop that can ask this before it repaints will stop re-blitting an idle, unchanged window every tick. It is safe from the loop's own goroutine (both flags are atomic). It is deliberately conservative about staying dark — appearance changes are only noticed inside Frame, so a gated loop keeps a slow heartbeat rather than trusting this alone.

func (*App) OpenDefaultTab

func (a *App) OpenDefaultTab()

OpenDefaultTab selects the first source as the active tab so a launch loads only that source (the per-tab lazy model) rather than aggregating everything. A no-op with no subscriptions, leaving the "All" filter in place.

func (*App) OpenSelected

func (a *App) OpenSelected()

OpenSelected opens the currently previewed post in the full reading view (the keyboard equivalent of the pane's Open button). A no-op when nothing is selected.

func (*App) PersistSettings

func (a *App) PersistSettings()

PersistSettings snapshots the scene's settings and writes them through the store, WITHOUT re-aggregating the feed. It is the persistence path for pure-display preference changes such as editing the sidebar's virtual folders (which regroup the sidebar but never change what the feed fetches).

func (*App) PrefetchImages

func (a *App) PrefetchImages()

PrefetchImages enqueues every shown Usenet image post for parallel background download, so their thumbnails stream into the feed cards and preview pane. Safe to call repeatedly (it de-duplicates); must run on the UI thread.

func (*App) PrefetchMedia

func (a *App) PrefetchMedia()

PrefetchMedia downloads the remote thumbnails the shown feed is missing and hands each decoded image to the scene. Runs on the UI thread (posted after a refresh), the same thread that drains SetThumb, so the HasThumb reads inside MediaPrefetch are race-free; the downloads themselves are asynchronous.

func (*App) PresentImmediate

func (a *App) PresentImmediate() bool

PresentImmediate reports that a background scene write is queued, so the next present must not be throttled — the new content has to reach the screen at once. A gated present loop checks it to override its spinner throttle. Safe from the loop's goroutine (the flag is atomic).

func (*App) PresentThrottle

func (a *App) PresentThrottle() bool

PresentThrottle reports that the only thing animating is the indeterminate loading spinner, whose clock is real-time, so a gated present loop may redraw it below its tick rate to spare the CPU a full-window blit on every tick. It is false the moment anything else needs the full cadence (a queued write, a playing GIF, a web-preview debounce) or nothing is animating at all. Safe from the loop's goroutine (the flag is atomic).

func (*App) PreviewGroup

func (a *App) PreviewGroup(base string)

PreviewGroup previews a Usenet multipart post (a group card, which is not a single Item): it synthesises a preview item and reconstructs the post's image into the pane.

func (*App) PullRefresh

func (a *App) PullRefresh(ctx context.Context) []error

PullRefresh is the pull-to-refresh seam the scene's OnPullRefresh trigger drives. When the feed is filtered to a single subscription (the sidebar has one selected), it re-fetches ONLY that subscription — the one the user is looking at — instead of re-aggregating every source. With no single subscription selected ("All"), it falls back to a full streaming refresh.

func (*App) ReconstructGroup

func (a *App) ReconstructGroup(base string)

ReconstructGroup triggers reconstruction of the Usenet post group with the given release base. It is non-blocking: the default hook runs on a goroutine so the UI keeps presenting the loading indicator; tests substitute a synchronous hook.

func (*App) Refresh

func (a *App) Refresh(ctx context.Context) []error

Refresh aggregates the active profile's subscriptions (concurrently, newest-first) and loads the merged items into the scene. It splits the failures: every provider that needs sign-in/configuration becomes a clickable in-feed prompt (de-duplicated, in subscription order), while any remaining non-auth failure is shown in the status line.

func (*App) RefreshGroups

func (a *App) RefreshGroups()

RefreshGroups asynchronously re-fetches the group list, bypassing the provider cache so newly-carried groups appear. The browser's Refresh control calls it.

func (*App) RefreshStreaming

func (a *App) RefreshStreaming(ctx context.Context) []error

RefreshStreaming aggregates the active profile's subscriptions incrementally, updating the scene as each source returns instead of waiting for the slowest. It marks every source pending and sets the scene loading, then on each completed source refreshes the merged feed, clears that source's pending marker, updates the progress counters, and re-derives the auth prompts and status line (in stable subscription order, independent of completion order). When the last source lands it turns loading off. The window front-end uses it so the feed fills in live with a moving indicator; the CLI keeps the blocking [Refresh]. It returns the collected failures (subscription order).

func (*App) RenderCacheStats

func (a *App) RenderCacheStats() (pages, bytes int)

RenderCacheStats reports how many pages the render cache holds and their total pixel bytes (shown as the Settings caption).

func (*App) RenderPNG

func (a *App) RenderPNG() ([]byte, error)

RenderPNG draws the current scene and encodes it as PNG.

func (*App) RunSearch

func (a *App) RunSearch()

RunSearch reads the search view's current query + tab on the render thread, marks the view loading, and kicks off the async search through the (test- substitutable) searchFetch seam. A blank query just reports a friendly hint instead of hitting the network.

func (*App) RunSearchFetch

func (a *App) RunSearchFetch(ctx context.Context, query string, posts bool)

RunSearchFetch performs the active search and delivers the results to the search view on the render thread (via a.post): on the posts tab a Reddit keyword post-search; otherwise channel discovery across EVERY provider that implements source.Searcher (Reddit subreddits, X accounts/hashtags, Instagram accounts/tags, ...), merged into one list — each source.ChannelResult carries its own platform, so a mixed list stays unambiguous. The default searchFetch runs this on its own goroutine; tests call it directly.

func (*App) ScanBrowseGroup

func (a *App) ScanBrowseGroup()

ScanBrowseGroup kicks off a sampled content-mix scan for the newsgroup under the browser's keyboard selection, unless one is already cached. A no-op when the selected row isn't a group. Called after browse-tree navigation.

func (*App) Scene

func (a *App) Scene() *ui.Scene

Scene exposes the scene so front-ends can dispatch input to it.

func (*App) SelectAdjacent

func (a *App) SelectAdjacent(dir int)

SelectAdjacent moves the feed selection one card up (dir<0) or down (dir>0) through the virtual.CardList and previews the newly selected post — the keyboard equivalent of clicking it, so its image is fetched and it scrolls into view. The preview itself is driven by the feed select hook (installed in New), which the CardList fires on the cursor move; the web-page render is debounced there so holding an arrow key does not render every item passed over.

func (*App) SelectPreview

func (a *App) SelectPreview(it source.Item)

SelectPreview shows it in the right preview pane. For a Usenet item that carries an image and has not been fetched yet, it kicks off an async reconstruct → decode → SetThumb so the picture appears in the pane.

func (*App) SelectProfile

func (a *App) SelectProfile(i int)

SelectProfile switches the active profile: it updates the scene (which re-derives the sidebar subscriptions), keeps the view-model's Profile observable in step, and re-aggregates the newly-active profile's feed. The window/CLI front-ends route a profile-tab click here instead of poking the scene, so the active profile flows through the app rather than the view.

func (*App) SetBiometricUnlock

func (a *App) SetBiometricUnlock(v bool)

ApplySceneSettings snapshots the scene's edited settings, persists them (when a store is configured), reselects the active profile's subscriptions and theme, and triggers a re-aggregate. Front-ends call it after any profile switch or settings-view edit. SetBiometricUnlock toggles biometric unlock: it records the preference on the scene, then persists and re-applies. Secrets are always stored UNGATED (the SecAccessControl user-presence flag needs a keychain entitlement AMFI rejects on a self-signed build); the preference instead gates the vault READ behind the LAContext prompt in ActivateAfterVault, so the change takes effect on the next launch's read.

func (*App) SetCommentFetchHook

func (a *App) SetCommentFetchHook(f func(it source.Item))

SetCommentFetchHook overrides the async comment fetch (tests use a synchronous variant for determinism).

func (*App) SetCookieFinder

func (a *App) SetCookieFinder(f cookieImporter)

SetCookieFinder overrides the browser-cookie importer (tests inject a fake).

func (*App) SetDownloadSync

func (a *App) SetDownloadSync()

SetDownloadSync makes the file-download manager reconstruct inline instead of in a goroutine — a determinism seam for tests (mirrors SetPreviewFetchHook).

func (*App) SetGroupStatsFetchHook

func (a *App) SetGroupStatsFetchHook(f func(name string))

SetGroupStatsFetchHook overrides the async group scan (tests use a synchronous variant for determinism).

func (*App) SetLoadGroupsHook

func (a *App) SetLoadGroupsHook(f func(force bool))

SetLoadGroupsHook overrides the asynchronous group-list fetch trigger (tests use a synchronous variant for determinism).

func (*App) SetLoadMoreHook

func (a *App) SetLoadMoreHook(f func())

SetLoadMoreHook overrides the infinite-scroll (OnReachBottom) trigger's async launch (tests use a synchronous variant for determinism).

func (*App) SetMediaFetchHook

func (a *App) SetMediaFetchHook(f func(reqs []ui.MediaRequest))

SetMediaFetchHook overrides the async thumbnail fetch (tests use a synchronous, network-free variant for determinism).

func (*App) SetMediaSync

func (a *App) SetMediaSync()

SetMediaSync makes PrefetchMedia block until every thumbnail has been fetched. The one-shot CLI paths need it: they render (or serialise) the feed the moment Refresh returns, so an asynchronous fetch would always lose the race and emit placeholder-only images. The interactive window keeps the async default, where thumbnails filling in a frame or two later is exactly the wanted behaviour.

func (*App) SetPrefetchHook

func (a *App) SetPrefetchHook(f func(url string, width int))

SetPrefetchHook overrides the background prefetch trigger (tests use a synchronous variant so a warmed neighbour is populated deterministically).

func (*App) SetPreviewFetchHook

func (a *App) SetPreviewFetchHook(f func(id string, parts []usenet.ReconstructPart))

SetPreviewFetchHook overrides the async image fetch (tests use a synchronous variant for determinism).

func (*App) SetPullRefreshHook

func (a *App) SetPullRefreshHook(f func())

SetPullRefreshHook overrides the pull-to-refresh (OnPullRefresh) trigger's async launch (tests use a synchronous variant for determinism).

func (*App) SetReconstructHook

func (a *App) SetReconstructHook(f func(base string))

SetReconstructHook overrides the asynchronous reconstruction trigger (tests use a synchronous variant for determinism).

func (*App) SetRefreshHook

func (a *App) SetRefreshHook(f func())

SetRefreshHook overrides the asynchronous re-aggregate trigger (tests use a synchronous variant for determinism).

func (*App) SetRegistryBuilder

func (a *App) SetRegistryBuilder(f func(feeds.Options) *source.Registry)

SetRegistryBuilder overrides how ApplyAccounts rebuilds the provider registry (a seam so tests inject a fake registry instead of real providers).

func (*App) SetSearchFetchHook

func (a *App) SetSearchFetchHook(f func(query string, posts bool))

SetSearchFetchHook overrides the async Reddit search (tests use a synchronous variant for determinism).

func (*App) SetSystemAppearance

func (a *App) SetSystemAppearance(dark bool, accent color.RGBA, hasAccent bool, fontTTF []byte)

SetSystemAppearance applies look-and-feel harvested from the host UI: the effective dark/light mode (honoured only when the user's theme is "system"), the accent colour, and — when fontTTF parses — the system font. Called by the native window back-end at launch and whenever the system appearance changes.

func (*App) SetTheme

func (a *App) SetTheme(osName string, dark bool)

SetTheme reselects the palette (e.g. on a system light/dark change).

func (*App) SetURLOpener

func (a *App) SetURLOpener(f func(browser, url string) error)

SetURLOpener installs the browser opener used by browser sign-in flows: f launches a URL in the named browser (default|firefox|chrome|safari|edge). The window front-end wires it to the platform opener; tests inject a fake.

func (*App) SetWebFetchHook

func (a *App) SetWebFetchHook(f func(target string, width int, created int64))

SetWebFetchHook overrides the async page render (tests use a synchronous variant for determinism).

func (*App) SetWebRenderer

func (a *App) SetWebRenderer(r webrender.Renderer)

SetWebRenderer overrides the page renderer (tests inject a fake that returns a canned image without touching the network).

func (*App) SubscribeChannel

func (a *App) SubscribeChannel(src source.Kind, channel string)

SubscribeChannel adds a channel-discovery result — its handle already carrying the platform form (r/<name>, @account, #tag) — as a subscription on the active profile (dedup + normalization handled by the scene), then persists and re-aggregates. A no-op (already subscribed / no active profile) skips the re-aggregate.

func (*App) SubscribeGroup

func (a *App) SubscribeGroup(group string)

SubscribeGroup adds a usenet:<group> subscription to the active profile (when not already present), then persists and re-aggregates so the group appears in the sidebar and feed. A click in the browser routes here.

func (*App) SubscribePostSearch

func (a *App) SubscribePostSearch(query string)

SubscribePostSearch saves a post keyword search as a live "search:<query>" subscription on the active profile so fresh matches keep flowing into the aggregated feed, then persists and re-aggregates. A blank query, an existing subscription, or no active profile is a no-op.

func (*App) ToggleDownload

func (a *App) ToggleDownload(base string)

ToggleDownload queues (or cancels) the download of the complete Usenet post with the given release base. Parts + the cache dir are captured here on the UI thread so the worker never reads the scene.

func (*App) UnsubscribeGroup

func (a *App) UnsubscribeGroup(group string)

UnsubscribeGroup removes the usenet:<group> subscription from the active profile (when present), then persists and re-aggregates so it leaves the sidebar and feed. Clicking a subscribed group's ✓ in the browser routes here.

func (*App) VM

func (a *App) VM() *viewmodel.ViewModel

VM exposes the view-model so front-ends can execute its commands and read its observables. The core state flow (items, loading, search, mode, status, auth prompts) is driven through it and reflected onto the scene by bindScene.

func (*App) ViewSub

func (a *App) ViewSub(index int)

ViewSub selects the sidebar subscription (a group filter) and marks it seen: its current high-water marker is recorded + persisted so its unseen count drops to zero. AllFilter (or any marker-less sub) just switches the filter.

type Config

type Config struct {
	Registry *source.Registry
	// Settings supplies the profiles/theme/cache. When nil, a single "Home"
	// profile is synthesized from Subscriptions (the CLI PNG/JSON/serve paths).
	Settings *settings.Settings
	// Store persists settings edits/profile switches. Optional (nil = no disk).
	Store *settings.Store
	// Recorder, when set, feeds the scene's Network-log view. It should be the
	// same recorder the provider registry logs into.
	Recorder *httplog.Recorder
	// Subscriptions seeds the synthesized profile when Settings is nil.
	Subscriptions []source.Subscription
	// Options is the base (flag-derived) feeds configuration. The window path
	// rebuilds the registry from Options + Settings.Accounts + Recorder whenever
	// accounts change; the CLI paths leave it zero and pass a prebuilt Registry.
	Options       feeds.Options
	Width, Height int
	OS            string // ui.OSMac | ui.OSLinux | ui.OSWindows
	Dark          bool
}

Config configures a new App.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL