github.com/wtfutil/wtf@v0.43.0/app/wtf_app.go (about) 1 package app 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "time" 8 9 _ "github.com/gdamore/tcell/terminfo/extended" 10 "github.com/gdamore/tcell/v2" 11 "github.com/olebedev/config" 12 "github.com/radovskyb/watcher" 13 "github.com/rivo/tview" 14 "github.com/wtfutil/wtf/cfg" 15 "github.com/wtfutil/wtf/support" 16 "github.com/wtfutil/wtf/utils" 17 "github.com/wtfutil/wtf/wtf" 18 ) 19 20 // WtfApp is the container for a collection of widgets that are all constructed from a single 21 // configuration file and displayed together 22 type WtfApp struct { 23 TViewApp *tview.Application 24 25 config *config.Config 26 configFilePath string 27 display *Display 28 focusTracker FocusTracker 29 ghUser *support.GitHubUser 30 pages *tview.Pages 31 validator *ModuleValidator 32 widgets []wtf.Wtfable 33 34 // The redrawChan channel is used to allow modules to signal back to the main loop that 35 // the screen needs to be explicitly redrawn, instead of waiting for tcell to redraw 36 // on a user event, because something has visually changed 37 redrawChan chan bool 38 } 39 40 // NewWtfApp creates and returns an instance of WtfApp 41 func NewWtfApp(tviewApp *tview.Application, config *config.Config, configFilePath string) *WtfApp { 42 wtfApp := &WtfApp{ 43 TViewApp: tviewApp, 44 45 config: config, 46 configFilePath: configFilePath, 47 pages: tview.NewPages(), 48 49 redrawChan: make(chan bool, 1), 50 } 51 52 wtfApp.TViewApp.SetBeforeDrawFunc(func(s tcell.Screen) bool { 53 s.Clear() 54 return false 55 }) 56 57 wtfApp.widgets = MakeWidgets(wtfApp.TViewApp, wtfApp.pages, wtfApp.config, wtfApp.redrawChan) 58 if len(wtfApp.widgets) == 0 { 59 fmt.Println("No modules were defined. Make sure you have at least one properly defined widget") 60 os.Exit(1) 61 } 62 63 wtfApp.display = NewDisplay(wtfApp.widgets, wtfApp.config) 64 wtfApp.focusTracker = NewFocusTracker(wtfApp.TViewApp, wtfApp.widgets, wtfApp.config) 65 wtfApp.validator = NewModuleValidator() 66 67 githubAPIKey := readGitHubAPIKey(wtfApp.config) 68 wtfApp.ghUser = support.NewGitHubUser(githubAPIKey) 69 70 wtfApp.pages.AddPage("grid", wtfApp.display.Grid, true, true) 71 72 wtfApp.validator.Validate(wtfApp.widgets) 73 74 firstWidget := wtfApp.widgets[0] 75 wtfApp.pages.Box.SetBackgroundColor( 76 wtf.ColorFor( 77 firstWidget.CommonSettings().Colors.WidgetTheme.Background, 78 ), 79 ) 80 81 wtfApp.TViewApp.SetInputCapture(wtfApp.keyboardIntercept) 82 wtfApp.TViewApp.SetRoot(wtfApp.pages, true) 83 84 // Create a watcher to handle calls to redraw the screen 85 go handleRedraws(wtfApp.TViewApp, wtfApp.redrawChan) 86 87 return wtfApp 88 } 89 90 func handleRedraws(tviewApp *tview.Application, redrawChan chan bool) { 91 if redrawChan == nil { 92 return 93 } 94 95 for { 96 data := <-redrawChan 97 98 if data { 99 tviewApp.Draw() 100 } 101 } 102 } 103 104 /* -------------------- Exported Functions -------------------- */ 105 106 // Exit quits the app 107 func (wtfApp *WtfApp) Exit() { 108 wtfApp.Stop() 109 wtfApp.TViewApp.Stop() 110 wtfApp.DisplayExitMessage() 111 os.Exit(0) 112 } 113 114 // Execute starts the underlying tview app 115 func (wtfApp *WtfApp) Execute() error { 116 if err := wtfApp.TViewApp.Run(); err != nil { 117 return err 118 } 119 120 return nil 121 } 122 123 // Start initializes the app 124 func (wtfApp *WtfApp) Start() { 125 go wtfApp.scheduleWidgets() 126 go wtfApp.watchForConfigChanges() 127 128 // FIXME: This should be moved to the AppManager 129 go func() { _ = wtfApp.ghUser.Load() }() 130 } 131 132 // Stop kills all the currently-running widgets in this app 133 func (wtfApp *WtfApp) Stop() { 134 wtfApp.stopAllWidgets() 135 close(wtfApp.redrawChan) 136 } 137 138 /* -------------------- Unexported Functions -------------------- */ 139 140 func (wtfApp *WtfApp) stopAllWidgets() { 141 for _, widget := range wtfApp.widgets { 142 widget.Stop() 143 } 144 } 145 146 func (wtfApp *WtfApp) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { 147 // These keys are global keys used by the app. Widgets should not implement these keys 148 switch event.Key() { 149 case tcell.KeyCtrlC: 150 wtfApp.Stop() 151 wtfApp.TViewApp.Stop() 152 wtfApp.DisplayExitMessage() 153 case tcell.KeyCtrlR: 154 wtfApp.refreshAllWidgets() 155 return nil 156 case tcell.KeyCtrlSpace: 157 // FIXME: This can't reside in the app, the app doesn't know about 158 // the AppManager. The AppManager needs to catch this one 159 fmt.Println("Next app") 160 return nil 161 case tcell.KeyTab: 162 wtfApp.focusTracker.Next() 163 case tcell.KeyBacktab: 164 wtfApp.focusTracker.Prev() 165 return nil 166 case tcell.KeyEsc: 167 wtfApp.focusTracker.None() 168 } 169 170 // Checks to see if any widget has been assigned the pressed key as its focus key 171 if wtfApp.focusTracker.FocusOn(string(event.Rune())) { 172 return nil 173 } 174 175 // If no specific widget has focus, then allow the key presses to fall through to the app 176 if !wtfApp.focusTracker.IsFocused { 177 switch string(event.Rune()) { 178 case "q": 179 wtfApp.Exit() 180 case "/": 181 return nil 182 default: 183 } 184 } 185 186 return event 187 } 188 189 func (wtfApp *WtfApp) refreshAllWidgets() { 190 for _, widget := range wtfApp.widgets { 191 go widget.Refresh() 192 } 193 } 194 195 func (wtfApp *WtfApp) scheduleWidgets() { 196 for _, widget := range wtfApp.widgets { 197 go Schedule(widget) 198 } 199 } 200 201 func (wtfApp *WtfApp) watchForConfigChanges() { 202 watch := watcher.New() 203 204 // Notify write events 205 watch.FilterOps(watcher.Write) 206 207 go func() { 208 for { 209 select { 210 case <-watch.Event: 211 wtfApp.Stop() 212 213 config := cfg.LoadWtfConfigFile(wtfApp.configFilePath) 214 newApp := NewWtfApp(wtfApp.TViewApp, config, wtfApp.configFilePath) 215 openURLUtil := utils.ToStrs(config.UList("wtf.openUrlUtil", []interface{}{})) 216 utils.Init(config.UString("wtf.openFileUtil", "open"), openURLUtil) 217 218 newApp.Start() 219 case err := <-watch.Error: 220 if err == watcher.ErrWatchedFileDeleted { 221 // Usually happens because the watcher looks for the file as the OS is updating it 222 continue 223 } 224 log.Fatalln(err) 225 case <-watch.Closed: 226 return 227 } 228 } 229 }() 230 231 // Watch config file for changes. 232 absPath, _ := utils.ExpandHomeDir(wtfApp.configFilePath) 233 if err := watch.Add(absPath); err != nil { 234 log.Fatalln(err) 235 } 236 237 // Start the watching process - it'll check for changes every 100ms. 238 if err := watch.Start(time.Millisecond * 100); err != nil { 239 log.Fatalln(err) 240 } 241 }