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  }