mirror of
https://github.com/walles/moar.git
synced 2024-11-22 21:50:43 +03:00
Use fallback if $EDITOR is not set
Fixes #232. Note that vim as a pretty crappy fallback since it's so hard to use, but I'm going with vim anyway for compatibility with less.
This commit is contained in:
parent
eacddda29e
commit
eafc0e2250
182
m/editor.go
Normal file
182
m/editor.go
Normal file
@ -0,0 +1,182 @@
|
||||
package m
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/walles/moar/m/linenumbers"
|
||||
)
|
||||
|
||||
// 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 pickAnEditor() (string, string, error) {
|
||||
// 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 != "" {
|
||||
return editor, editorEnv, nil
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
"vim", // This is a sucky default, but let's have it for compatibility with less
|
||||
"nano",
|
||||
"vi",
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
fullPath, err := exec.LookPath(candidate)
|
||||
log.Trace("Problem finding ", candidate, ": ", err)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = errUnlessExecutable(fullPath)
|
||||
log.Trace("Problem with executability of ", fullPath, ": ", err)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return candidate, "fallback list", nil
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("No editor found, tried: $VISUAL, $EDITOR, %s", strings.Join(candidates, ", "))
|
||||
}
|
||||
|
||||
func handleEditingRequest(p *Pager) {
|
||||
editor, editorEnv, err := pickAnEditor()
|
||||
if err != nil {
|
||||
log.Warn("Failed to find an editor: ", err)
|
||||
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 from {} not executable: {}", editorEnv, 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()
|
||||
}
|
@ -1,15 +1,7 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@ -77,145 +69,6 @@ func (m PagerModeViewing) onKey(keyCode twin.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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user