1
1
mirror of https://github.com/walles/moar.git synced 2024-09-11 20:17:13 +03:00
moar/m/pagermode-viewing.go
2024-06-29 08:36:13 +02:00

329 lines
8.2 KiB
Go

package m
import (
"fmt"
"math"
"os"
"os/exec"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
"github.com/walles/moar/m/linenumbers"
"github.com/walles/moar/twin"
)
type PagerModeViewing struct {
pager *Pager
}
func (m PagerModeViewing) drawFooter(statusText string, spinner string) {
helpText := "Press 'ESC' / 'q' to exit, '/' to search, '?' for help"
if m.pager.isShowingHelp {
helpText = "Press 'ESC' / 'q' to exit help, '/' to search"
}
if m.pager.ShowStatusBar {
m.pager.setFooter(statusText + spinner + " " + helpText)
}
}
func (m PagerModeViewing) onKey(keyCode twin.KeyCode) {
p := m.pager
switch keyCode {
case twin.KeyEscape:
p.Quit()
case twin.KeyUp:
// Clipping is done in _Redraw()
p.scrollPosition = p.scrollPosition.PreviousLine(1)
p.handleScrolledUp()
case twin.KeyDown, twin.KeyEnter:
// Clipping is done in _Redraw()
p.scrollPosition = p.scrollPosition.NextLine(1)
p.handleScrolledDown()
case twin.KeyRight:
p.moveRight(p.SideScrollAmount)
case twin.KeyLeft:
p.moveRight(-p.SideScrollAmount)
case twin.KeyAltRight:
p.moveRight(1)
case twin.KeyAltLeft:
p.moveRight(-1)
case twin.KeyHome:
p.scrollPosition = newScrollPosition("Pager scroll position")
p.handleScrolledUp()
case twin.KeyEnd:
p.scrollToEnd()
case twin.KeyPgUp:
p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight())
p.handleScrolledUp()
case twin.KeyPgDown:
p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight())
p.handleScrolledDown()
default:
log.Debugf("Unhandled key event %v", keyCode)
}
}
// Dump the reader lines into a read-only temp file and return the absolute file
// name.
func dumpToTempFile(reader *Reader) (string, error) {
tempFile, err := os.CreateTemp("", "moar-contents-")
if err != nil {
return "", err
}
defer tempFile.Close()
log.Debug("Dumping contents into: ", tempFile.Name())
lines, _ := reader.GetLines(linenumbers.LineNumber{}, math.MaxInt)
for _, line := range lines.lines {
_, err := tempFile.WriteString(line.raw + "\n")
if err != nil {
return "", err
}
}
// Ref: https://pkg.go.dev/os#Chmod
err = os.Chmod(tempFile.Name(), 0400)
if err != nil {
// Doesn't matter that much, but if it fails we should at least log it
log.Debug("Failed to make temp file ", tempFile.Name(), " read-only: ", err)
}
return tempFile.Name(), nil
}
// Check that the editor is executable
func errUnlessExecutable(file string) error {
stat, err := os.Stat(file)
if err != nil {
return fmt.Errorf("Failed to stat %s: %w", file, err)
}
if runtime.GOOS == "windows" && strings.HasSuffix(strings.ToLower(file), ".exe") {
log.Debug(".exe file on Windows, assuming executable: ", file)
return nil
}
if stat.Mode()&0111 != 0 {
// Note that this check isn't perfect, it could still be executable but
// not by us. Corner case, let's just fail later in that case.
return nil
}
return fmt.Errorf("Not executable: %s", file)
}
func handleEditingRequest(p *Pager) {
// Get an editor setting from either VISUAL or EDITOR
editorEnv := "VISUAL"
editor := strings.TrimSpace(os.Getenv(editorEnv))
if editor == "" {
editorEnv := "EDITOR"
editor = strings.TrimSpace(os.Getenv(editorEnv))
}
if editor == "" {
// FIXME: Show a message in the status bar instead? Nothing wrong with
// moar here.
log.Warn("Neither $VISUAL nor $EDITOR are set, can't launch any editor")
return
}
// Tyre kicking check that we can find the editor either in the PATH or as
// an absolute path
firstWord := strings.Fields(editor)[0]
editorPath, err := exec.LookPath(firstWord)
if err != nil {
// FIXME: Show a message in the status bar instead? Nothing wrong with
// moar here.
log.Warn("Failed to find editor "+firstWord+" from $"+editorEnv+": ", err)
return
}
// Check that the editor is executable
err = errUnlessExecutable(editorPath)
if err != nil {
// FIXME: Show a message in the status bar instead? Nothing wrong with
// moar here.
log.Warn("Editor not executable: {}", err)
return
}
canOpenFile := p.reader.fileName != nil
if p.reader.fileName != nil {
// Verify that the file exists and is readable
err = tryOpen(*p.reader.fileName)
if err != nil {
canOpenFile = false
log.Info("File to edit is not readable: ", err)
}
}
var fileToEdit string
if canOpenFile {
fileToEdit = *p.reader.fileName
} else {
// NOTE: Let's not wait for the stream to finish, just dump whatever we
// have and open the editor on that. The user just asked for it, if they
// wanted to wait, they should have done that themselves.
// Create a temp file based on reader contents
fileToEdit, err = dumpToTempFile(p.reader)
if err != nil {
log.Warn("Failed to create temp file to edit: ", err)
return
}
}
p.AfterExit = func() error {
// NOTE: If you do any changes here, make sure they work with both "nano"
// and "code -w" (VSCode).
commandWithArgs := strings.Fields(editor)
commandWithArgs = append(commandWithArgs, fileToEdit)
log.Info("'v' pressed, launching editor: ", commandWithArgs)
command := exec.Command(commandWithArgs[0], commandWithArgs[1:]...)
// Since os.Stdin might come from a pipe, we can't trust that. Instead,
// we tell the editor to read from os.Stdout, which points to the
// terminal as well.
//
// Tested on macOS and Linux, works like a charm.
command.Stdin = os.Stdout // <- YES, WE SHOULD ASSIGN STDOUT TO STDIN
command.Stdout = os.Stdout
command.Stderr = os.Stderr
err := command.Run()
if err == nil {
log.Info("Editor exited successfully: ", commandWithArgs)
}
return err
}
p.Quit()
}
func (m PagerModeViewing) onRune(char rune) {
p := m.pager
switch char {
case 'q':
p.Quit()
case 'v':
handleEditingRequest(p)
case '?':
if !p.isShowingHelp {
p.preHelpState = &_PreHelpState{
reader: p.reader,
scrollPosition: p.scrollPosition,
leftColumnZeroBased: p.leftColumnZeroBased,
targetLineNumber: p.TargetLineNumber,
}
p.reader = _HelpReader
p.scrollPosition = newScrollPosition("Pager scroll position")
p.leftColumnZeroBased = 0
p.TargetLineNumber = nil
p.isShowingHelp = true
}
case '=':
p.ShowStatusBar = !p.ShowStatusBar
// '\x10' = CTRL-p, should scroll up one line.
// Ref: https://github.com/walles/moar/issues/107#issuecomment-1328354080
case 'k', 'y', '\x10':
// Clipping is done in _Redraw()
p.scrollPosition = p.scrollPosition.PreviousLine(1)
p.handleScrolledUp()
// '\x0e' = CTRL-n, should scroll down one line.
// Ref: https://github.com/walles/moar/issues/107#issuecomment-1328354080
case 'j', 'e', '\x0e':
// Clipping is done in _Redraw()
p.scrollPosition = p.scrollPosition.NextLine(1)
p.handleScrolledDown()
case 'l':
// vim right
p.moveRight(p.SideScrollAmount)
case 'h':
// vim left
p.moveRight(-p.SideScrollAmount)
case '<':
p.scrollPosition = newScrollPosition("Pager scroll position")
p.handleScrolledUp()
case '>', 'G':
p.scrollToEnd()
case 'f', ' ':
p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight())
p.handleScrolledDown()
case 'b':
p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight())
p.handleScrolledUp()
// '\x15' = CTRL-u, should work like just 'u'.
// Ref: https://github.com/walles/moar/issues/90
case 'u', '\x15':
p.scrollPosition = p.scrollPosition.PreviousLine(p.visibleHeight() / 2)
p.handleScrolledUp()
// '\x04' = CTRL-d, should work like just 'd'.
// Ref: https://github.com/walles/moar/issues/90
case 'd', '\x04':
p.scrollPosition = p.scrollPosition.NextLine(p.visibleHeight() / 2)
p.handleScrolledDown()
case '/':
p.mode = PagerModeSearch{pager: p}
p.TargetLineNumber = nil
p.searchString = ""
p.searchPattern = nil
case 'g':
p.mode = &PagerModeGotoLine{pager: p}
p.TargetLineNumber = nil
// Should match the pagermode-not-found.go previous-search-hit bindings
case 'n':
p.scrollToNextSearchHit()
// Should match the pagermode-not-found.go next-search-hit bindings
case 'p', 'N':
p.scrollToPreviousSearchHit()
case 'm':
p.mode = PagerModeMark{pager: p}
p.TargetLineNumber = nil
case '\'':
p.mode = PagerModeJumpToMark{pager: p}
p.TargetLineNumber = nil
case 'w':
p.WrapLongLines = !p.WrapLongLines
default:
log.Debugf("Unhandled rune keypress '%s'/0x%08x", string(char), int32(char))
}
}