1package systemviews
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "sync"
9 "time"
10
11 "github.com/bnema/dumber/internal/application/dto"
12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/domain/entity"
14)
15
16type Dependencies struct {
17 DOM DOM
18 Favorites port.SystemviewFavoritesService
19 History port.SystemviewHistoryService
20 Config port.SystemviewConfigService
21 LocationURI string
22}
23
24type App struct {
25 // mu protects rendered route state; actionMu protects worker channels.
26 // The two locks must not be held at the same time.
27 mu sync.Mutex
28 actionMu sync.Mutex
29 deps Dependencies
30 currentRoute Route
31 shellTheme shellTheme
32 historyEntries []*entity.HistoryEntry
33 historyStats *entity.HistoryStats
34 historyAnalytics *entity.HistoryAnalytics
35 historyDomainStats []*entity.DomainStat
36 historyQuery string
37 historyDomainFilter string
38 historyOffset int
39 historyWindowBefore time.Time
40 historyWindowAfter time.Time
41 historyCursorID int64
42 historyHasMore bool
43 historyNotice string
44 historyError string
45 favorites []*entity.Favorite
46 tags []*entity.Tag
47 favoriteTagFilter *entity.TagID
48 favoriteUntaggedFilter bool
49 favoritesNotice string
50 favoritesError string
51 config *dto.SystemviewConfigPayload
52 keybindings port.KeybindingsConfig
53 configNotice string
54 configError string
55 renderedHTML string
56 renderGeneration uint64
57 closed bool
58 actionQueue chan DOMAction
59 actionErrorQueue chan error
60 actionCtx context.Context
61 actionCancel context.CancelFunc
62 actionClosed bool
63 actionWG sync.WaitGroup
64}
65
66const (
67 historyTimelineLimit = 0
68 historySearchLimit = 100
69)
70
71func (a *App) lockAction() {
72 // Lock order: actionMu is never acquired while App.mu is held.
73 a.actionMu.Lock()
74}
75
76func (a *App) unlockAction() {
77 a.actionMu.Unlock()
78}
79
80func (a *App) lockState() {
81 // Lock order: App.mu is never held while acquiring actionMu.
82 a.mu.Lock()
83}
84
85func (a *App) unlockState() {
86 a.mu.Unlock()
87}
88
89func NewApp(deps Dependencies) *App {
90 return &App{deps: deps, currentRoute: RouteUnknown}
91}
92
93func (a *App) Run() error {
94 return a.RunWithContext(context.Background())
95}
96
97func (a *App) RunWithContext(ctx context.Context) error {
98 if a == nil {
99 return errors.New("app is nil")
100 }
101 if ctx == nil {
102 return errors.New("context is nil")
103 }
104
105 a.currentRoute = ParseRoute(a.deps.LocationURI)
106 if a.deps.DOM == nil {
107 return errors.New("DOM not configured")
108 }
109 asyncInitial := a.shouldLoadInitialRouteAsync()
110 if asyncInitial {
111 a.LoadInitialShell(ctx)
112 } else if err := a.LoadInitial(ctx); err != nil {
113 return err
114 }
115
116 if err := a.deps.DOM.Mount(a.renderedHTML); err != nil {
117 a.signalShellError(err)
118 return err
119 }
120 if binder, ok := a.deps.DOM.(DOMActionBinder); ok {
121 if err := a.bindDOMActions(ctx, binder); err != nil {
122 a.signalShellError(err)
123 return err
124 }
125 }
126 // Readiness fires after the initial mount and action binding only:
127 // async history-data completion refreshes content separately and
128 // never gates it.
129 if err := a.signalShellReady(); err != nil {
130 return err
131 }
132 if asyncInitial {
133 a.refreshCurrentRouteAsync(ctx)
134 }
135 return nil
136}
137
138// signalShellReady clears the shell loading state when the adapter
139// supports it. Adapters without a signaler keep legacy behavior.
140func (a *App) signalShellReady() error {
141 signaler, ok := a.deps.DOM.(DOMReadinessSignaler)
142 if !ok {
143 return nil
144 }
145 return signaler.SignalReady()
146}
147
148// signalShellError reports a fatal mount/bind failure to the shell when
149// the adapter supports it. The original error is always returned;
150// signaling is best-effort.
151func (a *App) signalShellError(err error) {
152 if err == nil {
153 return
154 }
155 if signaler, ok := a.deps.DOM.(DOMReadinessSignaler); ok {
156 _ = signaler.SignalError(err.Error())
157 }
158}
159
160func (a *App) shouldLoadInitialRouteAsync() bool {
161 return a != nil && a.currentRoute == RouteHistory && a.deps.History != nil
162}
163
164func (a *App) refreshCurrentRouteAsync(ctx context.Context) {
165 if a == nil {
166 return
167 }
168 a.lockState()
169 snapshot := a.currentHistoryRouteSnapshotLocked()
170 generation := a.renderGeneration
171 closed := a.closed
172 a.unlockState()
173 if closed || snapshot.route != RouteHistory {
174 return
175 }
176
177 a.lockAction()
178 asyncCtx := a.actionCtx
179 if asyncCtx == nil {
180 asyncCtx = ctx
181 }
182 ready := !a.actionClosed && asyncCtx != nil && asyncCtx.Err() == nil
183 if ready {
184 a.actionWG.Add(1)
185 }
186 a.unlockAction()
187 if !ready {
188 return
189 }
190
191 go func() {
192 defer a.actionWG.Done()
193 if asyncCtx.Err() != nil {
194 return
195 }
196 result, err := a.renderHistoryRouteSnapshot(asyncCtx, snapshot)
197
198 a.lockState()
199 if a.closed || asyncCtx.Err() != nil || a.currentRoute != snapshot.route || a.renderGeneration != generation {
200 a.unlockState()
201 return
202 }
203 if err != nil {
204 a.renderRouteError(err)
205 } else {
206 a.commitHistoryRouteResultLocked(result)
207 }
208 a.renderGeneration++
209 mountGeneration := a.renderGeneration
210 html := a.renderedHTML
211 a.unlockState()
212
213 if err := a.mountHTMLIfCurrent(asyncCtx, html, mountGeneration); err != nil {
214 logActionMountError(asyncCtx, err, nil)
215 }
216 }()
217}
218
219func (a *App) bindDOMActions(ctx context.Context, binder DOMActionBinder) error {
220 var (
221 created bool
222 workerCtx context.Context
223 cancel context.CancelFunc
224 queue chan DOMAction
225 errorQueue chan error
226 )
227
228 a.lockAction()
229 if a.actionClosed {
230 a.unlockAction()
231 return errors.New("systemview app is closed")
232 }
233 if a.actionQueue == nil {
234 workerCtx, cancel = context.WithCancel(ctx)
235 queue = make(chan DOMAction, 64)
236 errorQueue = make(chan error, 1)
237 a.actionQueue = queue
238 a.actionErrorQueue = errorQueue
239 a.actionCtx = workerCtx
240 a.actionCancel = cancel
241 created = true
242 }
243 a.unlockAction()
244
245 if err := binder.BindActions(func(action DOMAction) {
246 if !a.enqueueDOMAction(action) && a.actionWorkerActive() {
247 a.enqueueActionError(fmt.Errorf("systemview is busy; dropped action %q", action.Action))
248 }
249 }); err != nil {
250 if created {
251 a.discardPendingActionWorker(workerCtx, cancel, queue, errorQueue)
252 }
253 return err
254 }
255
256 if created && !a.startActionWorkers(workerCtx, queue, errorQueue) {
257 a.discardPendingActionWorker(workerCtx, cancel, queue, errorQueue)
258 a.releaseDOMBindings()
259 return errors.New("systemview action worker unavailable")
260 }
261 return nil
262}
263
264func (a *App) startActionWorkers(ctx context.Context, queue chan DOMAction, errorQueue chan error) bool {
265 if a == nil || ctx == nil || queue == nil || errorQueue == nil {
266 return false
267 }
268 a.lockAction()
269 ready := !a.actionClosed && a.actionCtx == ctx && a.actionQueue == queue && a.actionErrorQueue == errorQueue && ctx.Err() == nil
270 if ready {
271 a.actionWG.Add(2)
272 }
273 a.unlockAction()
274 if !ready {
275 return false
276 }
277 go func() {
278 defer a.actionWG.Done()
279 a.runActionWorker(ctx, queue)
280 }()
281 go func() {
282 defer a.actionWG.Done()
283 a.runActionErrorWorker(ctx, errorQueue)
284 }()
285 return true
286}
287
288func (a *App) discardPendingActionWorker(ctx context.Context, cancel context.CancelFunc, queue chan DOMAction, errorQueue chan error) {
289 if a == nil {
290 return
291 }
292 a.lockAction()
293 if a.actionCtx == ctx && a.actionQueue == queue {
294 a.actionCancel = nil
295 a.actionCtx = nil
296 a.actionQueue = nil
297 a.actionErrorQueue = nil
298 if cancel != nil {
299 cancel()
300 }
301 close(queue)
302 close(errorQueue)
303 }
304 a.unlockAction()
305}
306
307func (a *App) actionWorkerActive() bool {
308 if a == nil {
309 return false
310 }
311 a.lockAction()
312 closed := a.actionClosed
313 queue := a.actionQueue
314 ctx := a.actionCtx
315 a.unlockAction()
316 return !closed && queue != nil && ctx != nil && ctx.Err() == nil
317}
318
319func (a *App) enqueueDOMAction(action DOMAction) bool {
320 if a == nil {
321 return false
322 }
323 a.lockAction()
324 defer a.unlockAction()
325 queue := a.actionQueue
326 ctx := a.actionCtx
327 if a.actionClosed || queue == nil || ctx == nil {
328 return false
329 }
330 select {
331 case <-ctx.Done():
332 return false
333 case queue <- action:
334 return true
335 default:
336 return false
337 }
338}
339
340func (a *App) enqueueActionError(err error) {
341 if a == nil || err == nil {
342 return
343 }
344 a.lockAction()
345 defer a.unlockAction()
346 queue := a.actionErrorQueue
347 ctx := a.actionCtx
348 if a.actionClosed || queue == nil || ctx == nil {
349 return
350 }
351 select {
352 case <-ctx.Done():
353 return
354 case queue <- err:
355 default:
356 }
357}
358
359func (a *App) runActionWorker(ctx context.Context, queue <-chan DOMAction) {
360 for {
361 select {
362 case <-ctx.Done():
363 return
364 case action, ok := <-queue:
365 if !ok || ctx.Err() != nil {
366 return
367 }
368 if err := a.HandleDOMAction(ctx, action); err != nil {
369 a.enqueueActionError(err)
370 }
371 }
372 }
373}
374
375func (a *App) runActionErrorWorker(ctx context.Context, queue <-chan error) {
376 for {
377 select {
378 case <-ctx.Done():
379 return
380 case err, ok := <-queue:
381 if !ok || ctx.Err() != nil {
382 return
383 }
384 a.surfaceActionError(ctx, err)
385 }
386 }
387}
388
389func (a *App) surfaceActionError(ctx context.Context, err error) {
390 if a == nil || err == nil {
391 return
392 }
393 a.lockState()
394 a.renderRouteError(err)
395 a.renderGeneration++
396 generation := a.renderGeneration
397 html := a.renderedHTML
398 a.unlockState()
399 if mountErr := a.mountHTMLIfCurrent(ctx, html, generation); mountErr != nil {
400 logActionMountError(ctx, mountErr, err)
401 }
402}
403
404func (a *App) Close() {
405 if a == nil {
406 return
407 }
408 a.lockState()
409 a.closed = true
410 a.renderGeneration++
411 a.unlockState()
412
413 a.lockAction()
414 cancel := a.actionCancel
415 actionQueue := a.actionQueue
416 actionErrorQueue := a.actionErrorQueue
417 a.actionClosed = true
418 a.actionCancel = nil
419 a.actionCtx = nil
420 a.actionQueue = nil
421 a.actionErrorQueue = nil
422 a.unlockAction()
423
424 if cancel != nil {
425 cancel()
426 }
427 a.actionWG.Wait()
428 if actionQueue != nil {
429 close(actionQueue)
430 }
431 if actionErrorQueue != nil {
432 close(actionErrorQueue)
433 }
434 a.releaseDOMBindings()
435}
436
437func (a *App) releaseDOMBindings() {
438 if a == nil {
439 return
440 }
441 if releaser, ok := a.deps.DOM.(interface{ Release() }); ok {
442 releaser.Release()
443 }
444}
445
446func (a *App) LoadInitialShell(ctx context.Context) {
447 if a == nil {
448 return
449 }
450 a.loadShellTheme(ctx)
451 if a.currentRoute == "" || a.currentRoute == RouteUnknown {
452 a.currentRoute = ParseRoute(a.deps.LocationURI)
453 }
454 switch a.currentRoute {
455 case RouteHistory:
456 a.loadHistoryLoadingRoute()
457 default:
458 a.resetRouteState()
459 a.renderedHTML = renderAppFrame(renderedPage{
460 route: a.currentRoute,
461 title: routeDocumentTitle(a.currentRoute),
462 subtitle: routeSubtitle(a.currentRoute),
463 body: placeholderHTML(a.currentRoute),
464 }, a.shellTheme)
465 }
466}
467
468func (a *App) LoadInitial(ctx context.Context) error {
469 if a == nil {
470 return errors.New("app is nil")
471 }
472 a.loadShellTheme(ctx)
473 if a.currentRoute == "" || a.currentRoute == RouteUnknown {
474 a.currentRoute = ParseRoute(a.deps.LocationURI)
475 }
476
477 if err := a.loadCurrentRoute(ctx); err != nil {
478 a.renderRouteError(err)
479 }
480 return nil
481}
482
483func (a *App) loadCurrentRoute(ctx context.Context) error {
484 switch a.currentRoute {
485 case RouteHistory:
486 return a.loadHistoryRoute(ctx)
487 case RouteFavorites:
488 return a.loadFavoritesRoute(ctx)
489 case RouteConfig:
490 return a.loadConfigRoute(ctx)
491 default:
492 a.resetRouteState()
493 a.renderedHTML = renderAppFrame(renderedPage{
494 route: a.currentRoute,
495 title: routeDocumentTitle(a.currentRoute),
496 subtitle: string(a.currentRoute),
497 body: placeholderHTML(a.currentRoute),
498 }, a.shellTheme)
499 return nil
500 }
501}
502
503func (a *App) renderRouteError(err error) {
504 a.resetRouteState()
505 message := "unknown error"
506 if err != nil {
507 message = err.Error()
508 }
509 a.renderedHTML = renderAppFrame(renderedPage{
510 route: a.currentRoute,
511 title: routeDocumentTitle(a.currentRoute) + " — Error",
512 subtitle: routeSubtitle(a.currentRoute),
513 body: errorStateHTML(message),
514 }, a.shellTheme)
515}
516
517func routeDocumentTitle(route Route) string {
518 switch route {
519 case RouteHistory:
520 return "History"
521 case RouteFavorites:
522 return "Favorites"
523 case RouteConfig:
524 return "Config"
525 default:
526 return "Dumber System View"
527 }
528}
529
530func routeSubtitle(route Route) string {
531 switch route {
532 case RouteHistory:
533 return "Recent visits"
534 case RouteFavorites:
535 return "Saved bookmarks"
536 case RouteConfig:
537 return "Browser settings"
538 default:
539 return string(route)
540 }
541}
542
543func (a *App) loadHistoryLoadingRoute() {
544 a.favorites = nil
545 a.tags = nil
546 a.historyEntries = nil
547 a.historyStats = nil
548 a.historyAnalytics = nil
549 a.historyDomainStats = nil
550 a.historyNotice = ""
551 a.historyError = ""
552 data := historyRenderData{
553 Query: a.historyQuery,
554 DomainFilter: a.historyDomainFilter,
555 Offset: a.historyOffset,
556 Limit: historyTimelineLimit,
557 WindowBefore: a.historyWindowBefore,
558 WindowAfter: a.historyWindowAfter,
559 HasMore: a.historyHasMore,
560 Notice: a.historyNotice,
561 Error: a.historyError,
562 Loading: true,
563 }
564 title := historyDocumentTitle(data)
565 a.renderedHTML = renderAppFrame(renderedPage{
566 route: RouteHistory,
567 title: title,
568 subtitle: "History",
569 subtitleDetail: historyTitleDetail(data),
570 body: historyHTML(data),
571 }, a.shellTheme)
572}
573
574func (a *App) loadHistoryRoute(ctx context.Context) error {
575 if a.deps.History == nil {
576 a.resetRouteState()
577 a.renderedHTML = renderAppFrame(renderedPage{
578 route: a.currentRoute,
579 title: routeDocumentTitle(RouteHistory),
580 subtitle: routeDocumentTitle(RouteHistory),
581 body: placeholderHTML(a.currentRoute),
582 }, a.shellTheme)
583 return nil
584 }
585
586 result, err := a.renderHistoryRouteSnapshot(ctx, a.currentHistoryRouteSnapshotLocked())
587 if err != nil {
588 return err
589 }
590 a.commitHistoryRouteResultLocked(result)
591 return nil
592}
593
594type historyRouteSnapshot struct {
595 route Route
596 history port.SystemviewHistoryService
597 shellTheme shellTheme
598 query string
599 domainFilter string
600 offset int
601 windowBefore time.Time
602 windowAfter time.Time
603 cursorID int64
604 notice string
605 error string
606}
607
608type historyRouteResult struct {
609 entries []*entity.HistoryEntry
610 stats *entity.HistoryStats
611 domains []*entity.DomainStat
612 windowBefore time.Time
613 windowAfter time.Time
614 cursorID int64
615 hasMore bool
616 html string
617}
618
619func (a *App) currentHistoryRouteSnapshotLocked() historyRouteSnapshot {
620 return historyRouteSnapshot{
621 route: a.currentRoute,
622 history: a.deps.History,
623 shellTheme: a.shellTheme,
624 query: a.historyQuery,
625 domainFilter: a.historyDomainFilter,
626 offset: a.historyOffset,
627 windowBefore: a.historyWindowBefore,
628 windowAfter: a.historyWindowAfter,
629 cursorID: a.historyCursorID,
630 notice: a.historyNotice,
631 error: a.historyError,
632 }
633}
634
635func (a *App) renderHistoryRouteSnapshot(ctx context.Context, snapshot historyRouteSnapshot) (historyRouteResult, error) {
636 if snapshot.history == nil {
637 return historyRouteResult{
638 html: renderAppFrame(renderedPage{
639 route: snapshot.route,
640 title: routeDocumentTitle(RouteHistory),
641 subtitle: routeDocumentTitle(RouteHistory),
642 body: placeholderHTML(snapshot.route),
643 }, snapshot.shellTheme),
644 }, nil
645 }
646
647 window, entries, err := loadHistoryWindowSnapshot(ctx, snapshot)
648 if err != nil {
649 return historyRouteResult{}, err
650 }
651 stats, err := snapshot.history.Stats(ctx)
652 if err != nil {
653 return historyRouteResult{}, fmt.Errorf("failed to load history stats: %w", err)
654 }
655 domains, domainsErr := snapshot.history.DomainStats(ctx, 10)
656 if domainsErr != nil {
657 domains = nil
658 }
659 result := historyRouteResult{
660 entries: entries,
661 stats: stats,
662 domains: domains,
663 windowBefore: snapshot.windowBefore,
664 windowAfter: snapshot.windowAfter,
665 }
666 if window != nil {
667 result.windowBefore = window.Before
668 result.windowAfter, result.cursorID, result.hasMore = historyWindowCursor(window)
669 }
670 data := historyRenderData{
671 Entries: entries,
672 Stats: stats,
673 Analytics: nil,
674 Domains: domains,
675 Query: snapshot.query,
676 DomainFilter: snapshot.domainFilter,
677 Offset: snapshot.offset,
678 Limit: historyTimelineLimit,
679 WindowBefore: result.windowBefore,
680 WindowAfter: result.windowAfter,
681 HasMore: result.hasMore,
682 Notice: snapshot.notice,
683 Error: snapshot.error,
684 }
685 title := historyDocumentTitle(data)
686 result.html = renderAppFrame(renderedPage{
687 route: RouteHistory,
688 title: title,
689 subtitle: "History",
690 subtitleDetail: historyTitleDetail(data),
691 body: historyHTML(data),
692 }, snapshot.shellTheme)
693 return result, nil
694}
695
696func historyWindowCursor(window *entity.HistoryWindow) (time.Time, int64, bool) {
697 if window == nil || !window.HasMore {
698 return time.Time{}, 0, false
699 }
700 cursorTime := window.CursorLastVisited
701 if cursorTime.IsZero() {
702 cursorTime = window.After
703 }
704 if cursorTime.IsZero() || window.CursorID <= 0 {
705 return time.Time{}, 0, false
706 }
707 return cursorTime, window.CursorID, true
708}
709
710func loadHistoryWindowSnapshot(ctx context.Context, snapshot historyRouteSnapshot) (*entity.HistoryWindow, []*entity.HistoryEntry, error) {
711 query := strings.TrimSpace(snapshot.query)
712 domain := strings.TrimSpace(snapshot.domainFilter)
713 if query != "" {
714 entries, err := snapshot.history.Search(ctx, query, historySearchLimit)
715 return nil, entries, err
716 }
717 window, err := snapshot.history.TimelineWindow(ctx, snapshot.windowAfter, snapshot.cursorID, domain)
718 if err != nil {
719 return nil, nil, err
720 }
721 if window == nil {
722 return nil, nil, nil
723 }
724 return window, window.Entries, nil
725}
726
727func (a *App) commitHistoryRouteResultLocked(result historyRouteResult) {
728 a.favorites = nil
729 a.tags = nil
730 a.historyEntries = result.entries
731 a.historyStats = result.stats
732 a.historyAnalytics = nil
733 a.historyDomainStats = result.domains
734 a.historyWindowBefore = result.windowBefore
735 a.historyWindowAfter = result.windowAfter
736 a.historyCursorID = result.cursorID
737 a.historyHasMore = result.hasMore
738 a.renderedHTML = result.html
739}
740
741func (a *App) loadFavoritesRoute(ctx context.Context) error {
742 if a.deps.Favorites == nil {
743 a.resetRouteState()
744 a.renderedHTML = renderAppFrame(renderedPage{
745 route: a.currentRoute,
746 title: routeDocumentTitle(RouteFavorites),
747 subtitle: "Saved bookmarks",
748 body: placeholderHTML(a.currentRoute),
749 }, a.shellTheme)
750 return nil
751 }
752
753 favorites, err := a.deps.Favorites.List(ctx)
754 if err != nil {
755 return err
756 }
757 tags, err := a.deps.Favorites.ListTags(ctx)
758 if err != nil {
759 return err
760 }
761
762 a.historyEntries = nil
763 favorites = filterFavorites(favorites, a.favoriteTagFilter, a.favoriteUntaggedFilter)
764 a.favorites = favorites
765 a.tags = tags
766 data := favoritesRenderData{
767 Favorites: favorites,
768 Tags: tags,
769 TagFilter: a.favoriteTagFilter,
770 UntaggedFilter: a.favoriteUntaggedFilter,
771 Notice: a.favoritesNotice,
772 Error: a.favoritesError,
773 }
774 a.renderedHTML = renderAppFrame(renderedPage{
775 route: RouteFavorites,
776 title: favoritesDocumentTitle(data),
777 subtitle: "Saved bookmarks",
778 body: favoritesHTML(data),
779 }, a.shellTheme)
780 return nil
781}
782
783func (a *App) loadConfigRoute(ctx context.Context) error {
784 if a.deps.Config == nil {
785 a.resetRouteState()
786 a.renderedHTML = renderAppFrame(renderedPage{
787 route: a.currentRoute,
788 title: routeDocumentTitle(RouteConfig),
789 subtitle: "Browser settings",
790 body: placeholderHTML(a.currentRoute),
791 }, a.shellTheme)
792 return nil
793 }
794
795 config, err := a.deps.Config.Current(ctx)
796 if err != nil {
797 return err
798 }
799 keybindings, err := a.deps.Config.GetKeybindings(ctx)
800 if err != nil {
801 return err
802 }
803
804 a.historyEntries = nil
805 a.favorites = nil
806 a.tags = nil
807 a.config = &config
808 a.keybindings = keybindings
809 a.renderedHTML = renderAppFrame(renderedPage{
810 route: RouteConfig,
811 title: "Config — Dumber",
812 subtitle: "Browser settings",
813 body: configHTML(configRenderData{
814 Config: config,
815 Keybindings: keybindings,
816 Notice: a.configNotice,
817 Error: a.configError,
818 }),
819 }, a.shellTheme)
820 return nil
821}
822
823func (a *App) loadShellTheme(ctx context.Context) {
824 if a == nil {
825 return
826 }
827
828 a.shellTheme = shellTheme{}
829 if a.deps.Config == nil {
830 return
831 }
832
833 config, err := a.deps.Config.Current(ctx)
834 if err == nil {
835 appearance := config.Appearance
836 if config.ResolvedAppearance != nil {
837 appearance = *config.ResolvedAppearance
838 }
839 a.shellTheme = resolveShellTheme(appearance)
840 }
841}
842
843func (a *App) resetRouteState() {
844 a.historyEntries = nil
845 a.historyStats = nil
846 a.historyAnalytics = nil
847 a.historyDomainStats = nil
848 a.historyQuery = ""
849 a.historyDomainFilter = ""
850 a.historyOffset = 0
851 a.historyWindowBefore = time.Time{}
852 a.historyWindowAfter = time.Time{}
853 a.historyCursorID = 0
854 a.historyHasMore = false
855 a.historyNotice = ""
856 a.historyError = ""
857 a.favorites = nil
858 a.tags = nil
859 a.favoriteTagFilter = nil
860 a.favoriteUntaggedFilter = false
861 a.favoritesNotice = ""
862 a.favoritesError = ""
863 a.config = nil
864 a.keybindings = port.KeybindingsConfig{}
865 a.configNotice = ""
866 a.configError = ""
867}
868
869func (a *App) CurrentRoute() Route {
870 if a == nil {
871 return RouteUnknown
872 }
873 return a.currentRoute
874}