github.com/wtfutil/wtf@v0.43.0/modules/textfile/widget.go (about)

     1  package textfile
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/alecthomas/chroma/formatters"
    12  	"github.com/alecthomas/chroma/lexers"
    13  	"github.com/alecthomas/chroma/styles"
    14  	"github.com/radovskyb/watcher"
    15  	"github.com/rivo/tview"
    16  	"github.com/wtfutil/wtf/utils"
    17  	"github.com/wtfutil/wtf/view"
    18  )
    19  
    20  const (
    21  	pollingIntervalms = 100
    22  )
    23  
    24  type Widget struct {
    25  	view.MultiSourceWidget
    26  	view.TextWidget
    27  
    28  	settings *Settings
    29  }
    30  
    31  // NewWidget creates a new instance of a widget
    32  func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
    33  	widget := Widget{
    34  		MultiSourceWidget: view.NewMultiSourceWidget(settings.Common, "filePath", "filePaths"),
    35  		TextWidget:        view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),
    36  
    37  		settings: settings,
    38  	}
    39  
    40  	// Don't use a timer for this widget, watch for filesystem changes instead
    41  	widget.settings.RefreshInterval = 0
    42  
    43  	widget.initializeKeyboardControls()
    44  
    45  	widget.SetDisplayFunction(widget.Refresh)
    46  	widget.View.SetWordWrap(true)
    47  	widget.View.SetWrap(settings.wrapText)
    48  
    49  	go widget.watchForFileChanges()
    50  
    51  	return &widget
    52  }
    53  
    54  /* -------------------- Exported Functions -------------------- */
    55  
    56  // Refresh is only called once on start-up. Its job is to display the
    57  // text files that first time. After that, the watcher takes over
    58  func (widget *Widget) Refresh() {
    59  	widget.Redraw(widget.content)
    60  }
    61  
    62  /* -------------------- Unexported Functions -------------------- */
    63  
    64  func (widget *Widget) content() (string, string, bool) {
    65  	title := fmt.Sprintf(
    66  		"[%s]%s[white]",
    67  		widget.settings.Colors.TextTheme.Title,
    68  		widget.CurrentSource(),
    69  	)
    70  
    71  	_, _, width, _ := widget.View.GetRect()
    72  	text := widget.settings.PaginationMarker(len(widget.Sources), widget.Idx, width) + "\n"
    73  
    74  	if widget.settings.format {
    75  		text += widget.formattedText()
    76  	} else {
    77  		text += widget.plainText()
    78  	}
    79  
    80  	return title, text, widget.settings.wrapText
    81  }
    82  
    83  func (widget *Widget) formattedText() string {
    84  	filePath, _ := utils.ExpandHomeDir(widget.CurrentSource())
    85  
    86  	file, err := os.Open(filepath.Clean(filePath))
    87  	if err != nil {
    88  		return err.Error()
    89  	}
    90  	defer func() { _ = file.Close() }()
    91  
    92  	lexer := lexers.Match(filePath)
    93  	if lexer == nil {
    94  		lexer = lexers.Fallback
    95  	}
    96  
    97  	style := styles.Get(widget.settings.formatStyle)
    98  	if style == nil {
    99  		style = styles.Fallback
   100  	}
   101  	formatter := formatters.Get("terminal256")
   102  	if formatter == nil {
   103  		formatter = formatters.Fallback
   104  	}
   105  
   106  	contents, _ := io.ReadAll(file)
   107  	str := string(contents)
   108  	str = tview.Escape(str)
   109  	iterator, _ := lexer.Tokenise(nil, str)
   110  
   111  	var buf bytes.Buffer
   112  	err = formatter.Format(&buf, style, iterator)
   113  	if err != nil {
   114  		return err.Error()
   115  	}
   116  
   117  	return tview.TranslateANSI(buf.String())
   118  }
   119  
   120  func (widget *Widget) plainText() string {
   121  	filePath, _ := utils.ExpandHomeDir(filepath.Clean(widget.CurrentSource()))
   122  
   123  	text, err := os.ReadFile(filepath.Clean(filePath))
   124  	if err != nil {
   125  		return err.Error()
   126  	}
   127  	return tview.Escape(string(text))
   128  }
   129  
   130  func (widget *Widget) watchForFileChanges() {
   131  	watch := watcher.New()
   132  	watch.FilterOps(watcher.Write)
   133  
   134  	go func() {
   135  		for {
   136  			select {
   137  			case <-watch.Event:
   138  				widget.Refresh()
   139  			case err := <-watch.Error:
   140  				fmt.Println(err)
   141  				os.Exit(1)
   142  			case <-watch.Closed:
   143  				return
   144  			case quit := <-widget.QuitChan():
   145  				if quit {
   146  					return
   147  				}
   148  			}
   149  		}
   150  	}()
   151  
   152  	// Watch each textfile for changes
   153  	for _, source := range widget.Sources {
   154  		fullPath, err := utils.ExpandHomeDir(source)
   155  		if err == nil {
   156  			e := watch.Add(fullPath)
   157  			if e != nil {
   158  				fmt.Println(e)
   159  				os.Exit(1)
   160  			}
   161  		}
   162  	}
   163  
   164  	// Start the watching process - it'll check for changes every pollingIntervalms.
   165  	if err := watch.Start(time.Millisecond * pollingIntervalms); err != nil {
   166  		fmt.Println(err)
   167  		os.Exit(1)
   168  	}
   169  }