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