1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
package internal
import (
"fmt"
"math"
"os"
"os/exec"
"runtime"
"strings"
log "github.com/sirupsen/logrus"
"github.com/walles/moor/internal/linemetadata"
"github.com/walles/moor/internal/reader"
)
// Dump the reader lines into a read-only temp file and return the absolute file
// name.
func dumpToTempFile(reader *reader.ReaderImpl) (string, error) {
tempFile, err := os.CreateTemp("", "moor-contents-")
if err != nil {
return "", err
}
defer func() {
err = tempFile.Close()
if err != nil {
log.Warn("Failed to close temp file: ", err)
}
}()
log.Debug("Dumping contents into: ", tempFile.Name())
lines := reader.GetLines(linemetadata.Index{}, math.MaxInt)
for _, line := range lines.Lines {
toWrite := line.Plain()
_, err := tempFile.WriteString(toWrite + "\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
// moor 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
// moor 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 = reader.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:]...)
if runtime.GOOS == "windows" {
// Don't touch command.Stdin on Windows:
// https://github.com/walles/moor/issues/281#issuecomment-2953384726
} else {
// 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()
}
|