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