mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-18 14:55:38 -04:00
Merge pull request #3273 from JoeKar/fix/save-atomically
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
save: Perform write process safe
This commit is contained in:
commit
98356765c1
@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/buffer"
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func shouldContinue() bool {
|
func shouldContinue() bool {
|
||||||
@ -39,7 +40,16 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Cleaning default settings")
|
fmt.Println("Cleaning default settings")
|
||||||
config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
|
||||||
|
settingsFile := filepath.Join(config.ConfigDir, "settings.json")
|
||||||
|
err := config.WriteSettings(settingsFile)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("Error writing settings.json file: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// detect unused options
|
// detect unused options
|
||||||
var unusedOptions []string
|
var unusedOptions []string
|
||||||
@ -67,16 +77,20 @@ func CleanConfig() {
|
|||||||
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json"))
|
fmt.Printf("These options will be removed from %s\n", settingsFile)
|
||||||
|
|
||||||
if shouldContinue() {
|
if shouldContinue() {
|
||||||
for _, s := range unusedOptions {
|
for _, s := range unusedOptions {
|
||||||
delete(config.GlobalSettings, s)
|
delete(config.GlobalSettings, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
err := config.OverwriteSettings(settingsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error writing settings.json file: " + err.Error())
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("Error overwriting settings.json file: " + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Removed unused options")
|
fmt.Println("Removed unused options")
|
||||||
@ -85,12 +99,13 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detect incorrectly formatted buffer/ files
|
// detect incorrectly formatted buffer/ files
|
||||||
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers"))
|
buffersPath := filepath.Join(config.ConfigDir, "buffers")
|
||||||
|
files, err := os.ReadDir(buffersPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var badFiles []string
|
var badFiles []string
|
||||||
var buffer buffer.SerializedBuffer
|
var buffer buffer.SerializedBuffer
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
|
fname := filepath.Join(buffersPath, f.Name())
|
||||||
file, e := os.Open(fname)
|
file, e := os.Open(fname)
|
||||||
|
|
||||||
if e == nil {
|
if e == nil {
|
||||||
@ -105,9 +120,9 @@ func CleanConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(badFiles) > 0 {
|
if len(badFiles) > 0 {
|
||||||
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers"))
|
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), buffersPath)
|
||||||
fmt.Println("These files store cursor and undo history.")
|
fmt.Println("These files store cursor and undo history.")
|
||||||
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers"))
|
fmt.Printf("Removing badly formatted files in %s\n", buffersPath)
|
||||||
|
|
||||||
if shouldContinue() {
|
if shouldContinue() {
|
||||||
removed := 0
|
removed := 0
|
||||||
|
@ -18,7 +18,7 @@ func (NullWriter) Write(data []byte) (n int, err error) {
|
|||||||
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
||||||
func InitLog() {
|
func InitLog() {
|
||||||
if util.Debug == "ON" {
|
if util.Debug == "ON" {
|
||||||
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error opening file: %v", err)
|
log.Fatalf("error opening file: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
@ -99,7 +99,7 @@ func InitFlags() {
|
|||||||
fmt.Println("Version:", util.Version)
|
fmt.Println("Version:", util.Version)
|
||||||
fmt.Println("Commit hash:", util.CommitHash)
|
fmt.Println("Commit hash:", util.CommitHash)
|
||||||
fmt.Println("Compiled on", util.CompileDate)
|
fmt.Println("Compiled on", util.CompileDate)
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flagOptions {
|
if *flagOptions {
|
||||||
@ -115,7 +115,7 @@ func InitFlags() {
|
|||||||
fmt.Printf("-%s value\n", k)
|
fmt.Printf("-%s value\n", k)
|
||||||
fmt.Printf(" \tDefault value: '%v'\n", v)
|
fmt.Printf(" \tDefault value: '%v'\n", v)
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.Debug == "OFF" && *flagDebug {
|
if util.Debug == "OFF" && *flagDebug {
|
||||||
@ -136,7 +136,7 @@ func DoPluginFlags() {
|
|||||||
CleanConfig()
|
CleanConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
|
|||||||
// Option 2
|
// Option 2
|
||||||
// The input is not a terminal, so something is being piped in
|
// The input is not a terminal, so something is being piped in
|
||||||
// and we should read from stdin
|
// and we should read from stdin
|
||||||
input, err = ioutil.ReadAll(os.Stdin)
|
input, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error reading from stdin: ", err)
|
screen.TermMessage("Error reading from stdin: ", err)
|
||||||
input = []byte{}
|
input = []byte{}
|
||||||
@ -223,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
|
|||||||
return buffers
|
return buffers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkBackup(name string) error {
|
||||||
|
target := filepath.Join(config.ConfigDir, name)
|
||||||
|
backup := util.AppendBackupSuffix(target)
|
||||||
|
if info, err := os.Stat(backup); err == nil {
|
||||||
|
input, err := os.ReadFile(backup)
|
||||||
|
if err == nil {
|
||||||
|
t := info.ModTime()
|
||||||
|
msg := fmt.Sprintf(buffer.BackupMsg, target, t.Format("Mon Jan _2 at 15:04, 2006"), backup)
|
||||||
|
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
||||||
|
|
||||||
|
if choice%3 == 0 {
|
||||||
|
// recover
|
||||||
|
err := os.WriteFile(target, input, util.FileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Remove(backup)
|
||||||
|
} else if choice%3 == 1 {
|
||||||
|
// delete
|
||||||
|
return os.Remove(backup)
|
||||||
|
} else if choice%3 == 2 {
|
||||||
|
// abort
|
||||||
|
return errors.New("Aborted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exit(rc int) {
|
||||||
|
for _, b := range buffer.OpenBuffers {
|
||||||
|
if !b.Modified() {
|
||||||
|
b.Fini()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if screen.Screen != nil {
|
||||||
|
screen.Screen.Fini()
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if util.Stdout.Len() > 0 {
|
if util.Stdout.Len() > 0 {
|
||||||
fmt.Fprint(os.Stdout, util.Stdout.String())
|
fmt.Fprint(os.Stdout, util.Stdout.String())
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -256,6 +299,12 @@ func main() {
|
|||||||
config.InitRuntimeFiles(true)
|
config.InitRuntimeFiles(true)
|
||||||
config.InitPlugins()
|
config.InitPlugins()
|
||||||
|
|
||||||
|
err = checkBackup("settings.json")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
err = config.ReadSettings()
|
err = config.ReadSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
@ -288,7 +337,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
fmt.Println("Fatal: Micro could not initialize a Screen.")
|
fmt.Println("Fatal: Micro could not initialize a Screen.")
|
||||||
os.Exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
|
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
|
||||||
clipErr := clipboard.Initialize(m)
|
clipErr := clipboard.Initialize(m)
|
||||||
@ -307,7 +356,7 @@ func main() {
|
|||||||
for _, b := range buffer.OpenBuffers {
|
for _, b := range buffer.OpenBuffers {
|
||||||
b.Backup()
|
b.Backup()
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -316,6 +365,12 @@ func main() {
|
|||||||
screen.TermMessage(err)
|
screen.TermMessage(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = checkBackup("bindings.json")
|
||||||
|
if err != nil {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
action.InitBindings()
|
action.InitBindings()
|
||||||
action.InitCommands()
|
action.InitCommands()
|
||||||
|
|
||||||
@ -434,24 +489,12 @@ func DoEvent() {
|
|||||||
}
|
}
|
||||||
case f := <-timerChan:
|
case f := <-timerChan:
|
||||||
f()
|
f()
|
||||||
|
case b := <-buffer.BackupCompleteChan:
|
||||||
|
b.RequestedBackup = false
|
||||||
case <-sighup:
|
case <-sighup:
|
||||||
for _, b := range buffer.OpenBuffers {
|
exit(0)
|
||||||
if !b.Modified() {
|
|
||||||
b.Fini()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
case <-util.Sigterm:
|
case <-util.Sigterm:
|
||||||
for _, b := range buffer.OpenBuffers {
|
exit(0)
|
||||||
if !b.Modified() {
|
|
||||||
b.Fini()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if screen.Screen != nil {
|
|
||||||
screen.Screen.Fini()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, ok := event.(*tcell.EventError); ok {
|
if e, ok := event.(*tcell.EventError); ok {
|
||||||
@ -459,16 +502,7 @@ func DoEvent() {
|
|||||||
|
|
||||||
if e.Err() == io.EOF {
|
if e.Err() == io.EOF {
|
||||||
// shutdown due to terminal closing/becoming inaccessible
|
// shutdown due to terminal closing/becoming inaccessible
|
||||||
for _, b := range buffer.OpenBuffers {
|
exit(0)
|
||||||
if !b.Modified() {
|
|
||||||
b.Fini()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if screen.Screen != nil {
|
|
||||||
screen.Screen.Fini()
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1003,6 +1003,9 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool {
|
|||||||
h.completeAction(action)
|
h.completeAction(action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
InfoBar.YNPrompt(
|
InfoBar.YNPrompt(
|
||||||
@ -1039,7 +1042,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
InfoBar.Error(err)
|
||||||
} else {
|
} else {
|
||||||
h.Buf.Path = filename
|
|
||||||
h.Buf.SetName(filename)
|
h.Buf.SetName(filename)
|
||||||
InfoBar.Message("Saved " + filename)
|
InfoBar.Message("Saved " + filename)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
@ -1065,7 +1067,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
|
|||||||
InfoBar.Error(err)
|
InfoBar.Error(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
h.Buf.Path = filename
|
|
||||||
h.Buf.SetName(filename)
|
h.Buf.SetName(filename)
|
||||||
InfoBar.Message("Saved " + filename)
|
InfoBar.Message("Saved " + filename)
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/micro-editor/json5"
|
"github.com/micro-editor/json5"
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/micro-editor/tcell/v2"
|
"github.com/micro-editor/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
|
|||||||
"terminal": TermMapEvent,
|
"terminal": TermMapEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeFile(name string, txt []byte) error {
|
||||||
|
return util.SafeWrite(name, txt, false)
|
||||||
|
}
|
||||||
|
|
||||||
func createBindingsIfNotExist(fname string) {
|
func createBindingsIfNotExist(fname string) {
|
||||||
if _, e := os.Stat(fname); os.IsNotExist(e) {
|
if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
|
||||||
ioutil.WriteFile(fname, []byte("{}"), 0644)
|
writeFile(fname, []byte("{}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +42,7 @@ func InitBindings() {
|
|||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
|
|
||||||
if _, e := os.Stat(filename); e == nil {
|
if _, e := os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
||||||
return
|
return
|
||||||
@ -265,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
|||||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
if _, e = os.Stat(filename); e == nil {
|
if _, e = os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -304,7 +309,8 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
|||||||
BindKey(k, v, Binder["buffer"])
|
BindKey(k, v, Binder["buffer"])
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||||
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
return true, writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return false, e
|
return false, e
|
||||||
}
|
}
|
||||||
@ -317,7 +323,7 @@ func UnbindKey(k string) error {
|
|||||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||||
createBindingsIfNotExist(filename)
|
createBindingsIfNotExist(filename)
|
||||||
if _, e = os.Stat(filename); e == nil {
|
if _, e = os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Error reading bindings.json file: " + err.Error())
|
return errors.New("Error reading bindings.json file: " + err.Error())
|
||||||
}
|
}
|
||||||
@ -354,7 +360,8 @@ func UnbindKey(k string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||||
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
return writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@ -658,7 +658,16 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
|||||||
delete(b.LocalSettings, option)
|
delete(b.LocalSettings, option)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
err = errors.Unwrap(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalOption(option, value string) error {
|
func SetGlobalOption(option, value string) error {
|
||||||
@ -783,7 +792,11 @@ func (h *BufPane) BindCmd(args []string) {
|
|||||||
|
|
||||||
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
|
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
} else {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,7 +809,11 @@ func (h *BufPane) UnbindCmd(args []string) {
|
|||||||
|
|
||||||
err := UnbindKey(parseKeyArg(args[0]))
|
err := UnbindKey(parseKeyArg(args[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
InfoBar.Error(err)
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
|
screen.TermMessage(err)
|
||||||
|
} else {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -890,7 +907,10 @@ func (h *BufPane) SaveCmd(args []string) {
|
|||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
h.Save()
|
h.Save()
|
||||||
} else {
|
} else {
|
||||||
h.Buf.SaveAs(args[0])
|
err := h.Buf.SaveAs(args[0])
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ package buffer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -109,15 +109,15 @@ func FileComplete(b *Buffer) ([]string, []string) {
|
|||||||
sep := string(os.PathSeparator)
|
sep := string(os.PathSeparator)
|
||||||
dirs := strings.Split(input, sep)
|
dirs := strings.Split(input, sep)
|
||||||
|
|
||||||
var files []os.FileInfo
|
var files []fs.DirEntry
|
||||||
var err error
|
var err error
|
||||||
if len(dirs) > 1 {
|
if len(dirs) > 1 {
|
||||||
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||||
|
|
||||||
directories, _ = util.ReplaceHome(directories)
|
directories, _ = util.ReplaceHome(directories)
|
||||||
files, err = ioutil.ReadDir(directories)
|
files, err = os.ReadDir(directories)
|
||||||
} else {
|
} else {
|
||||||
files, err = ioutil.ReadDir(".")
|
files, err = os.ReadDir(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupMsg = `A backup was detected for this file. This likely means that micro
|
const BackupMsg = `A backup was detected for:
|
||||||
crashed while editing this file, or another instance of micro is currently
|
|
||||||
editing this file.
|
|
||||||
|
|
||||||
The backup was created on %s, and the file is
|
%s
|
||||||
|
|
||||||
|
This likely means that micro crashed while editing this file,
|
||||||
|
or another instance of micro is currently editing this file,
|
||||||
|
or an error occurred while saving this file so it may be corrupted.
|
||||||
|
|
||||||
|
The backup was created on %s and its path is:
|
||||||
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
@ -30,90 +32,80 @@ The backup was created on %s, and the file is
|
|||||||
|
|
||||||
Options: [r]ecover, [i]gnore, [a]bort: `
|
Options: [r]ecover, [i]gnore, [a]bort: `
|
||||||
|
|
||||||
var backupRequestChan chan *Buffer
|
const backupSeconds = 8
|
||||||
|
|
||||||
func backupThread() {
|
var BackupCompleteChan chan *Buffer
|
||||||
for {
|
|
||||||
time.Sleep(time.Second * 8)
|
|
||||||
|
|
||||||
for len(backupRequestChan) > 0 {
|
|
||||||
b := <-backupRequestChan
|
|
||||||
bfini := atomic.LoadInt32(&(b.fini)) != 0
|
|
||||||
if !bfini {
|
|
||||||
b.Backup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
backupRequestChan = make(chan *Buffer, 10)
|
BackupCompleteChan = make(chan *Buffer, 10)
|
||||||
|
|
||||||
go backupThread()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Buffer) RequestBackup() {
|
func (b *Buffer) RequestBackup() {
|
||||||
if !b.requestedBackup {
|
if !b.RequestedBackup {
|
||||||
select {
|
select {
|
||||||
case backupRequestChan <- b:
|
case backupRequestChan <- b:
|
||||||
default:
|
default:
|
||||||
// channel is full
|
// channel is full
|
||||||
}
|
}
|
||||||
b.requestedBackup = true
|
b.RequestedBackup = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup saves the current buffer to ConfigDir/backups
|
func (b *Buffer) backupDir() string {
|
||||||
|
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
|
||||||
|
if backupdir == "" || err != nil {
|
||||||
|
backupdir = filepath.Join(config.ConfigDir, "backups")
|
||||||
|
}
|
||||||
|
return backupdir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) keepBackup() bool {
|
||||||
|
return b.forceKeepBackup || b.Settings["permbackup"].(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup saves the current buffer to the backups directory
|
||||||
func (b *Buffer) Backup() error {
|
func (b *Buffer) Backup() error {
|
||||||
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
|
backupdir := b.backupDir()
|
||||||
if backupdir == "" || err != nil {
|
if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) {
|
||||||
backupdir = filepath.Join(config.ConfigDir, "backups")
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
|
|
||||||
os.Mkdir(backupdir, os.ModePerm)
|
os.Mkdir(backupdir, os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath))
|
name := util.DetermineEscapePath(backupdir, b.AbsPath)
|
||||||
|
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
|
||||||
err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
|
_, err = b.overwriteFile(name)
|
||||||
if len(b.lines) == 0 {
|
if err == nil {
|
||||||
return
|
BackupCompleteChan <- b
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// end of line
|
tmp := util.AppendBackupSuffix(name)
|
||||||
eol := []byte{'\n'}
|
_, err := b.overwriteFile(tmp)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Rename(tmp, name)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// write lines
|
BackupCompleteChan <- b
|
||||||
if _, e = file.Write(b.lines[0].data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range b.lines[1:] {
|
|
||||||
if _, e = file.Write(eol); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, e = file.Write(l.data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
b.requestedBackup = false
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveBackup removes any backup file associated with this buffer
|
// RemoveBackup removes any backup file associated with this buffer
|
||||||
func (b *Buffer) RemoveBackup() {
|
func (b *Buffer) RemoveBackup() {
|
||||||
if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||||
os.Remove(f)
|
os.Remove(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,13 +113,13 @@ func (b *Buffer) RemoveBackup() {
|
|||||||
// Returns true if a backup was applied
|
// Returns true if a backup was applied
|
||||||
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
|
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
|
||||||
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
||||||
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
|
||||||
if info, err := os.Stat(backupfile); err == nil {
|
if info, err := os.Stat(backupfile); err == nil {
|
||||||
backup, err := os.Open(backupfile)
|
backup, err := os.Open(backupfile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer backup.Close()
|
defer backup.Close()
|
||||||
t := info.ModTime()
|
t := info.ModTime()
|
||||||
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
|
msg := fmt.Sprintf(BackupMsg, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile)
|
||||||
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
|
||||||
|
|
||||||
if choice%3 == 0 {
|
if choice%3 == 0 {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -25,13 +25,12 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"github.com/zyedidia/micro/v2/pkg/highlight"
|
"github.com/zyedidia/micro/v2/pkg/highlight"
|
||||||
|
"golang.org/x/text/encoding"
|
||||||
"golang.org/x/text/encoding/htmlindex"
|
"golang.org/x/text/encoding/htmlindex"
|
||||||
"golang.org/x/text/encoding/unicode"
|
"golang.org/x/text/encoding/unicode"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
const backupTime = 8000
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// OpenBuffers is a list of the currently open buffers
|
// OpenBuffers is a list of the currently open buffers
|
||||||
OpenBuffers []*Buffer
|
OpenBuffers []*Buffer
|
||||||
@ -89,6 +88,8 @@ type SharedBuffer struct {
|
|||||||
// LocalSettings customized by the user for this buffer only
|
// LocalSettings customized by the user for this buffer only
|
||||||
LocalSettings map[string]bool
|
LocalSettings map[string]bool
|
||||||
|
|
||||||
|
encoding encoding.Encoding
|
||||||
|
|
||||||
Suggestions []string
|
Suggestions []string
|
||||||
Completions []string
|
Completions []string
|
||||||
CurSuggestion int
|
CurSuggestion int
|
||||||
@ -101,7 +102,8 @@ type SharedBuffer struct {
|
|||||||
diffLock sync.RWMutex
|
diffLock sync.RWMutex
|
||||||
diff map[int]DiffStatus
|
diff map[int]DiffStatus
|
||||||
|
|
||||||
requestedBackup bool
|
RequestedBackup bool
|
||||||
|
forceKeepBackup bool
|
||||||
|
|
||||||
// ReloadDisabled allows the user to disable reloads if they
|
// ReloadDisabled allows the user to disable reloads if they
|
||||||
// are viewing a file that is constantly changing
|
// are viewing a file that is constantly changing
|
||||||
@ -237,17 +239,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
|
|
||||||
readonly := os.IsPermission(err)
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
fileInfo, serr := os.Stat(filename)
|
fileInfo, serr := os.Stat(filename)
|
||||||
if serr != nil && !os.IsNotExist(serr) {
|
if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
|
||||||
return nil, serr
|
return nil, serr
|
||||||
}
|
}
|
||||||
if serr == nil && fileInfo.IsDir() {
|
if serr == nil && fileInfo.IsDir() {
|
||||||
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
|
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
|
||||||
}
|
}
|
||||||
|
if serr == nil && !fileInfo.Mode().IsRegular() {
|
||||||
|
return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
|
||||||
|
readonly := errors.Is(err, fs.ErrPermission)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -255,7 +260,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf *Buffer
|
var buf *Buffer
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
// File does not exist -- create an empty buffer with that name
|
// File does not exist -- create an empty buffer with that name
|
||||||
buf = NewBufferFromString("", filename, btype)
|
buf = NewBufferFromString("", filename, btype)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -335,9 +340,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
}
|
}
|
||||||
config.UpdatePathGlobLocals(b.Settings, absPath)
|
config.UpdatePathGlobLocals(b.Settings, absPath)
|
||||||
|
|
||||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
enc = unicode.UTF8
|
b.encoding = unicode.UTF8
|
||||||
b.Settings["encoding"] = "utf-8"
|
b.Settings["encoding"] = "utf-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +353,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
return NewBufferFromString("", "", btype)
|
return NewBufferFromString("", "", btype)
|
||||||
}
|
}
|
||||||
if !hasBackup {
|
if !hasBackup {
|
||||||
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
|
reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
|
||||||
|
|
||||||
var ff FileFormat = FFAuto
|
var ff FileFormat = FFAuto
|
||||||
|
|
||||||
@ -389,7 +394,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
|
|||||||
// we know the filetype now, so update per-filetype settings
|
// we know the filetype now, so update per-filetype settings
|
||||||
config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string))
|
config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string))
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) {
|
||||||
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
|
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,7 +545,7 @@ func (b *Buffer) ReOpen() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
|
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
|
||||||
data, err := ioutil.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
txt := string(data)
|
txt := string(data)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,18 +5,19 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
"golang.org/x/text/encoding/htmlindex"
|
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,74 +25,178 @@ import (
|
|||||||
// because hashing is too slow
|
// because hashing is too slow
|
||||||
const LargeFileThreshold = 50000
|
const LargeFileThreshold = 50000
|
||||||
|
|
||||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
type wrappedFile struct {
|
||||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
writeCloser io.WriteCloser
|
||||||
// closed afterwards.
|
withSudo bool
|
||||||
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
|
screenb bool
|
||||||
|
cmd *exec.Cmd
|
||||||
|
sigChan chan os.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
type saveResponse struct {
|
||||||
|
size int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type saveRequest struct {
|
||||||
|
buf *Buffer
|
||||||
|
path string
|
||||||
|
withSudo bool
|
||||||
|
newFile bool
|
||||||
|
saveResponseChan chan saveResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveRequestChan chan saveRequest
|
||||||
|
var backupRequestChan chan *Buffer
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
saveRequestChan = make(chan saveRequest, 10)
|
||||||
|
backupRequestChan = make(chan *Buffer, 10)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
duration := backupSeconds * float64(time.Second)
|
||||||
|
backupTicker := time.NewTicker(time.Duration(duration))
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sr := <-saveRequestChan:
|
||||||
|
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
|
||||||
|
sr.saveResponseChan <- saveResponse{size, err}
|
||||||
|
case <-backupTicker.C:
|
||||||
|
for len(backupRequestChan) > 0 {
|
||||||
|
b := <-backupRequestChan
|
||||||
|
bfini := atomic.LoadInt32(&(b.fini)) != 0
|
||||||
|
if !bfini {
|
||||||
|
b.Backup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFile(name string, withSudo bool) (wrappedFile, error) {
|
||||||
|
var err error
|
||||||
var writeCloser io.WriteCloser
|
var writeCloser io.WriteCloser
|
||||||
var screenb bool
|
var screenb bool
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
var c chan os.Signal
|
var sigChan chan os.Signal
|
||||||
|
|
||||||
if withSudo {
|
if withSudo {
|
||||||
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
||||||
|
writeCloser, err = cmd.StdinPipe()
|
||||||
if writeCloser, err = cmd.StdinPipe(); err != nil {
|
if err != nil {
|
||||||
return
|
return wrappedFile{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c = make(chan os.Signal, 1)
|
sigChan = make(chan os.Signal, 1)
|
||||||
signal.Reset(os.Interrupt)
|
signal.Reset(os.Interrupt)
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(sigChan, os.Interrupt)
|
||||||
|
|
||||||
screenb = screen.TempFini()
|
screenb = screen.TempFini()
|
||||||
// need to start the process now, otherwise when we flush the file
|
// need to start the process now, otherwise when we flush the file
|
||||||
// contents to its stdin it might hang because the kernel's pipe size
|
// contents to its stdin it might hang because the kernel's pipe size
|
||||||
// is too small to handle the full file contents all at once
|
// is too small to handle the full file contents all at once
|
||||||
if err = cmd.Start(); err != nil {
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
screen.TempStart(screenb)
|
screen.TempStart(screenb)
|
||||||
|
|
||||||
signal.Notify(util.Sigterm, os.Interrupt)
|
signal.Notify(util.Sigterm, os.Interrupt)
|
||||||
signal.Stop(c)
|
signal.Stop(sigChan)
|
||||||
|
|
||||||
return
|
return wrappedFile{}, err
|
||||||
}
|
}
|
||||||
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil {
|
} else {
|
||||||
return
|
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
|
||||||
}
|
if err != nil {
|
||||||
|
return wrappedFile{}, err
|
||||||
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
|
|
||||||
err = fn(w)
|
|
||||||
|
|
||||||
if err2 := w.Flush(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
// Call Sync() on the file to make sure the content is safely on disk.
|
|
||||||
// Does not work with sudo as we don't have direct access to the file.
|
|
||||||
if !withSudo {
|
|
||||||
f := writeCloser.(*os.File)
|
|
||||||
if err2 := f.Sync(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err2 := writeCloser.Close(); err2 != nil && err == nil {
|
|
||||||
err = err2
|
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf wrappedFile) Write(b *Buffer) (int, error) {
|
||||||
|
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
|
||||||
|
|
||||||
|
b.Lock()
|
||||||
|
defer b.Unlock()
|
||||||
|
|
||||||
|
if len(b.lines) == 0 {
|
||||||
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if withSudo {
|
// end of line
|
||||||
|
var eol []byte
|
||||||
|
if b.Endings == FFDos {
|
||||||
|
eol = []byte{'\r', '\n'}
|
||||||
|
} else {
|
||||||
|
eol = []byte{'\n'}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wf.withSudo {
|
||||||
|
f := wf.writeCloser.(*os.File)
|
||||||
|
err := f.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write lines
|
||||||
|
size, err := file.Write(b.lines[0].data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, l := range b.lines[1:] {
|
||||||
|
if _, err = file.Write(eol); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err = file.Write(l.data); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
size += len(eol) + len(l.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = file.Flush()
|
||||||
|
if err == nil && !wf.withSudo {
|
||||||
|
// Call Sync() on the file to make sure the content is safely on disk.
|
||||||
|
f := wf.writeCloser.(*os.File)
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf wrappedFile) Close() error {
|
||||||
|
err := wf.writeCloser.Close()
|
||||||
|
if wf.withSudo {
|
||||||
// wait for dd to finish and restart the screen if we used sudo
|
// wait for dd to finish and restart the screen if we used sudo
|
||||||
err := cmd.Wait()
|
err := wf.cmd.Wait()
|
||||||
screen.TempStart(screenb)
|
screen.TempStart(wf.screenb)
|
||||||
|
|
||||||
signal.Notify(util.Sigterm, os.Interrupt)
|
signal.Notify(util.Sigterm, os.Interrupt)
|
||||||
signal.Stop(c)
|
signal.Stop(wf.sigChan)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return
|
func (b *Buffer) overwriteFile(name string) (int, error) {
|
||||||
|
file, err := openFile(name, false)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := file.Write(b)
|
||||||
|
|
||||||
|
err2 := file.Close()
|
||||||
|
if err2 != nil && err == nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
return size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the buffer to its default path
|
// Save saves the buffer to its default path
|
||||||
@ -152,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the last time this file was updated after saving
|
filename, err = util.ReplaceHome(filename)
|
||||||
defer func() {
|
if err != nil {
|
||||||
b.ModTime, _ = util.GetModTime(filename)
|
return err
|
||||||
err = b.Serialize()
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Removes any tilde and replaces with the absolute path to home
|
newFile := false
|
||||||
absFilename, _ := util.ReplaceHome(filename)
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newFile = true
|
||||||
|
}
|
||||||
|
if err == nil && fileInfo.IsDir() {
|
||||||
|
return errors.New("Error: " + filename + " is a directory and cannot be saved")
|
||||||
|
}
|
||||||
|
if err == nil && !fileInfo.Mode().IsRegular() {
|
||||||
|
return errors.New("Error: " + filename + " is not a regular file and cannot be saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
absFilename, err := filepath.Abs(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the leading path to the file | "." is returned if there's no leading path provided
|
// Get the leading path to the file | "." is returned if there's no leading path provided
|
||||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||||
// Check if the parent dirs don't exist
|
// Check if the parent dirs don't exist
|
||||||
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
|
||||||
// Prompt to make sure they want to create the dirs that are missing
|
// Prompt to make sure they want to create the dirs that are missing
|
||||||
if b.Settings["mkparents"].(bool) {
|
if b.Settings["mkparents"].(bool) {
|
||||||
// Create all leading dir(s) since they don't exist
|
// Create all leading dir(s) since they don't exist
|
||||||
@ -178,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSize int
|
saveResponseChan := make(chan saveResponse)
|
||||||
|
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
|
||||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
result := <-saveResponseChan
|
||||||
|
err = result.err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if errors.Is(err, util.ErrOverwrite) {
|
||||||
}
|
screen.TermMessage(err)
|
||||||
|
err = errors.Unwrap(err)
|
||||||
|
|
||||||
fwriter := func(file io.Writer) (e error) {
|
b.UpdateModTime()
|
||||||
if len(b.lines) == 0 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of line
|
|
||||||
var eol []byte
|
|
||||||
if b.Endings == FFDos {
|
|
||||||
eol = []byte{'\r', '\n'}
|
|
||||||
} else {
|
|
||||||
eol = []byte{'\n'}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write lines
|
|
||||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range b.lines[1:] {
|
|
||||||
if _, e = file.Write(eol); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, e = file.Write(l.data); e != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileSize += len(eol) + len(l.data)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.Settings["fastdirty"].(bool) {
|
if !b.Settings["fastdirty"].(bool) {
|
||||||
if fileSize > LargeFileThreshold {
|
if result.size > LargeFileThreshold {
|
||||||
// For large files 'fastdirty' needs to be on
|
// For large files 'fastdirty' needs to be on
|
||||||
b.Settings["fastdirty"] = true
|
b.Settings["fastdirty"] = true
|
||||||
} else {
|
} else {
|
||||||
@ -229,9 +323,64 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.Path = filename
|
b.Path = filename
|
||||||
absPath, _ := filepath.Abs(filename)
|
b.AbsPath = absFilename
|
||||||
b.AbsPath = absPath
|
|
||||||
b.isModified = false
|
b.isModified = false
|
||||||
|
b.UpdateModTime()
|
||||||
b.ReloadSettings(true)
|
b.ReloadSettings(true)
|
||||||
|
|
||||||
|
err = b.Serialize()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the
|
||||||
|
// contents of the file if it fails to write the new contents.
|
||||||
|
// This means that the file is not overwritten directly but by writing to the
|
||||||
|
// backup file first.
|
||||||
|
func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
|
||||||
|
file, err := openFile(path, withSudo)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if newFile && err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
backupDir := b.backupDir()
|
||||||
|
if _, err := os.Stat(backupDir); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backupName := util.DetermineEscapePath(backupDir, path)
|
||||||
|
_, err = b.overwriteFile(backupName)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(backupName)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.forceKeepBackup = true
|
||||||
|
size, err := file.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
err = util.OverwriteError{err, backupName}
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
b.forceKeepBackup = false
|
||||||
|
|
||||||
|
if !b.keepBackup() {
|
||||||
|
os.Remove(backupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
err2 := file.Close()
|
||||||
|
if err2 != nil && err == nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/encoding"
|
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))
|
var buf bytes.Buffer
|
||||||
|
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
|
||||||
return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
|
b.EventHandler,
|
||||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
b.GetActiveCursor().Loc,
|
||||||
b.EventHandler,
|
b.ModTime,
|
||||||
b.GetActiveCursor().Loc,
|
})
|
||||||
b.ModTime,
|
if err != nil {
|
||||||
})
|
|
||||||
return err
|
return err
|
||||||
}, false)
|
}
|
||||||
|
|
||||||
|
name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
|
||||||
|
return util.SafeWrite(name, buf.Bytes(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
||||||
@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error {
|
|||||||
if b.Path == "" {
|
if b.Path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)))
|
file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
var buffer SerializedBuffer
|
var buffer SerializedBuffer
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
ulua "github.com/zyedidia/micro/v2/internal/lua"
|
||||||
"github.com/zyedidia/micro/v2/internal/screen"
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
|
"golang.org/x/text/encoding/htmlindex"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
luar "layeh.com/gopher-luar"
|
luar "layeh.com/gopher-luar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -97,6 +99,12 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
|
|||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
}
|
}
|
||||||
} else if option == "encoding" {
|
} else if option == "encoding" {
|
||||||
|
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||||
|
if err != nil {
|
||||||
|
enc = unicode.UTF8
|
||||||
|
b.Settings["encoding"] = "utf-8"
|
||||||
|
}
|
||||||
|
b.encoding = enc
|
||||||
b.isModified = true
|
b.isModified = true
|
||||||
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
||||||
b.Type.Readonly = nativeValue.(bool)
|
b.Type.Readonly = nativeValue.(bool)
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -81,7 +80,7 @@ func (rf realFile) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rf realFile) Data() ([]byte, error) {
|
func (rf realFile) Data() ([]byte, error) {
|
||||||
return ioutil.ReadFile(string(rf))
|
return os.ReadFile(string(rf))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (af assetFile) Name() string {
|
func (af assetFile) Name() string {
|
||||||
@ -107,7 +106,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
|||||||
// AddRuntimeFilesFromDirectory registers each file from the given directory for
|
// AddRuntimeFilesFromDirectory registers each file from the given directory for
|
||||||
// the filetype which matches the file-pattern
|
// the filetype which matches the file-pattern
|
||||||
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
|
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
|
||||||
files, _ := ioutil.ReadDir(directory)
|
files, _ := os.ReadDir(directory)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
||||||
fullPath := filepath.Join(directory, f.Name())
|
fullPath := filepath.Join(directory, f.Name())
|
||||||
@ -194,14 +193,14 @@ func InitPlugins() {
|
|||||||
|
|
||||||
// Search ConfigDir for plugin-scripts
|
// Search ConfigDir for plugin-scripts
|
||||||
plugdir := filepath.Join(ConfigDir, "plug")
|
plugdir := filepath.Join(ConfigDir, "plug")
|
||||||
files, _ := ioutil.ReadDir(plugdir)
|
files, _ := os.ReadDir(plugdir)
|
||||||
|
|
||||||
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
|
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
|
||||||
|
|
||||||
for _, d := range files {
|
for _, d := range files {
|
||||||
plugpath := filepath.Join(plugdir, d.Name())
|
plugpath := filepath.Join(plugdir, d.Name())
|
||||||
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
|
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
|
||||||
srcs, _ := ioutil.ReadDir(plugpath)
|
srcs, _ := os.ReadDir(plugpath)
|
||||||
p := new(Plugin)
|
p := new(Plugin)
|
||||||
p.Name = d.Name()
|
p.Name = d.Name()
|
||||||
p.DirName = d.Name()
|
p.DirName = d.Name()
|
||||||
@ -209,7 +208,7 @@ func InitPlugins() {
|
|||||||
if strings.HasSuffix(f.Name(), ".lua") {
|
if strings.HasSuffix(f.Name(), ".lua") {
|
||||||
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
|
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
|
||||||
} else if strings.HasSuffix(f.Name(), ".json") {
|
} else if strings.HasSuffix(f.Name(), ".json") {
|
||||||
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
|
data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -156,6 +155,10 @@ var (
|
|||||||
VolatileSettings map[string]bool
|
VolatileSettings map[string]bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func writeFile(name string, txt []byte) error {
|
||||||
|
return util.SafeWrite(name, txt, false)
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ModifiedSettings = make(map[string]bool)
|
ModifiedSettings = make(map[string]bool)
|
||||||
VolatileSettings = make(map[string]bool)
|
VolatileSettings = make(map[string]bool)
|
||||||
@ -222,7 +225,7 @@ func ReadSettings() error {
|
|||||||
parsedSettings = make(map[string]interface{})
|
parsedSettings = make(map[string]interface{})
|
||||||
filename := filepath.Join(ConfigDir, "settings.json")
|
filename := filepath.Join(ConfigDir, "settings.json")
|
||||||
if _, e := os.Stat(filename); e == nil {
|
if _, e := os.Stat(filename); e == nil {
|
||||||
input, err := ioutil.ReadFile(filename)
|
input, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
settingsParseError = true
|
settingsParseError = true
|
||||||
return errors.New("Error reading settings.json file: " + err.Error())
|
return errors.New("Error reading settings.json file: " + err.Error())
|
||||||
@ -356,7 +359,8 @@ func WriteSettings(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
err = writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -377,8 +381,9 @@ func OverwriteSettings(filename string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
txt, _ := json.MarshalIndent(settings, "", " ")
|
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
txt = append(txt, '\n')
|
||||||
|
err = writeFile(filename, txt)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package info
|
package info
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/zyedidia/micro/v2/internal/config"
|
"github.com/zyedidia/micro/v2/internal/config"
|
||||||
|
"github.com/zyedidia/micro/v2/internal/screen"
|
||||||
"github.com/zyedidia/micro/v2/internal/util"
|
"github.com/zyedidia/micro/v2/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() {
|
|||||||
if config.GetGlobalOption("savehistory").(bool) {
|
if config.GetGlobalOption("savehistory").(bool) {
|
||||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
|
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
|
||||||
var decodedMap map[string][]string
|
var decodedMap map[string][]string
|
||||||
if err == nil {
|
if err != nil {
|
||||||
defer file.Close()
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
decoder := gob.NewDecoder(file)
|
i.Error("Error loading history: ", err)
|
||||||
err = decoder.Decode(&decodedMap)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
i.Error("Error loading history:", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
err = gob.NewDecoder(file).Decode(&decodedMap)
|
||||||
|
if err != nil {
|
||||||
|
i.Error("Error decoding history: ", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if decodedMap != nil {
|
if decodedMap != nil {
|
||||||
i.History = decodedMap
|
i.History = decodedMap
|
||||||
} else {
|
|
||||||
i.History = make(map[string][]string)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
i.History = make(map[string][]string)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,16 +52,18 @@ func (i *InfoBuf) SaveHistory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history"))
|
var buf bytes.Buffer
|
||||||
if err == nil {
|
err := gob.NewEncoder(&buf).Encode(i.History)
|
||||||
defer file.Close()
|
if err != nil {
|
||||||
encoder := gob.NewEncoder(file)
|
screen.TermMessage("Error encoding history: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = encoder.Encode(i.History)
|
filename := filepath.Join(config.ConfigDir, "buffers", "history")
|
||||||
if err != nil {
|
err = util.SafeWrite(filename, buf.Bytes(), true)
|
||||||
i.Error("Error saving history:", err)
|
if err != nil {
|
||||||
return
|
screen.TermMessage("Error saving history: ", err)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -43,8 +45,44 @@ var (
|
|||||||
Stdout *bytes.Buffer
|
Stdout *bytes.Buffer
|
||||||
// Sigterm is a channel where micro exits when written
|
// Sigterm is a channel where micro exits when written
|
||||||
Sigterm chan os.Signal
|
Sigterm chan os.Signal
|
||||||
|
|
||||||
|
// To be used for fails on (over-)write with safe writes
|
||||||
|
ErrOverwrite = OverwriteError{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// To be used for file writes before umask is applied
|
||||||
|
const FileMode os.FileMode = 0666
|
||||||
|
|
||||||
|
const OverwriteFailMsg = `An error occurred while writing to the file:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
The file may be corrupted now. The good news is that it has been
|
||||||
|
successfully backed up. Next time you open this file with Micro,
|
||||||
|
Micro will ask if you want to recover it from the backup.
|
||||||
|
|
||||||
|
The backup path is:
|
||||||
|
|
||||||
|
%s`
|
||||||
|
|
||||||
|
// OverwriteError is a custom error to add additional information
|
||||||
|
type OverwriteError struct {
|
||||||
|
What error
|
||||||
|
BackupName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Error() string {
|
||||||
|
return fmt.Sprintf(OverwriteFailMsg, e.What, e.BackupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Is(target error) bool {
|
||||||
|
return target == ErrOverwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e OverwriteError) Unwrap() error {
|
||||||
|
return e.What
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
SemVersion, err = semver.Make(Version)
|
SemVersion, err = semver.Make(Version)
|
||||||
@ -408,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
|
|||||||
return info.ModTime(), nil
|
return info.ModTime(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EscapePath replaces every path separator in a given path with a %
|
func AppendBackupSuffix(path string) string {
|
||||||
func EscapePath(path string) string {
|
return path + ".micro-backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapePathUrl encodes the path in URL query form
|
||||||
|
func EscapePathUrl(path string) string {
|
||||||
|
return url.QueryEscape(filepath.ToSlash(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapePathLegacy replaces every path separator in a given path with a %
|
||||||
|
func EscapePathLegacy(path string) string {
|
||||||
path = filepath.ToSlash(path)
|
path = filepath.ToSlash(path)
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// ':' is not valid in a path name on Windows but is ok on Unix
|
// ':' is not valid in a path name on Windows but is ok on Unix
|
||||||
@ -418,6 +465,24 @@ func EscapePath(path string) string {
|
|||||||
return strings.ReplaceAll(path, "/", "%")
|
return strings.ReplaceAll(path, "/", "%")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetermineEscapePath escapes a path, determining whether it should be escaped
|
||||||
|
// using URL encoding (preferred, since it encodes unambiguously) or
|
||||||
|
// legacy encoding with '%' (for backward compatibility, if the legacy-escaped
|
||||||
|
// path exists in the given directory).
|
||||||
|
func DetermineEscapePath(dir string, path string) string {
|
||||||
|
url := filepath.Join(dir, EscapePathUrl(path))
|
||||||
|
if _, err := os.Stat(url); err == nil {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
legacy := filepath.Join(dir, EscapePathLegacy(path))
|
||||||
|
if _, err := os.Stat(legacy); err == nil {
|
||||||
|
return legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
// GetLeadingWhitespace returns the leading whitespace of the given byte array
|
// GetLeadingWhitespace returns the leading whitespace of the given byte array
|
||||||
func GetLeadingWhitespace(b []byte) []byte {
|
func GetLeadingWhitespace(b []byte) []byte {
|
||||||
ws := []byte{}
|
ws := []byte{}
|
||||||
@ -590,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
|
|||||||
}
|
}
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SafeWrite writes bytes to a file in a "safe" way, preventing loss of the
|
||||||
|
// contents of the file if it fails to write the new contents.
|
||||||
|
// This means that the file is not overwritten directly but by writing to a
|
||||||
|
// temporary file first.
|
||||||
|
//
|
||||||
|
// If rename is true, write is performed atomically, by renaming the temporary
|
||||||
|
// file to the target file after the data is successfully written to the
|
||||||
|
// temporary file. This guarantees that the file will not remain in a corrupted
|
||||||
|
// state, but it also has limitations, e.g. the file should not be a symlink
|
||||||
|
// (otherwise SafeWrite silently replaces this symlink with a regular file),
|
||||||
|
// the file creation date in Linux is not preserved (since the file inode
|
||||||
|
// changes) etc. Use SafeWrite with rename=true for files that are only created
|
||||||
|
// and used by micro for its own needs and are not supposed to be used directly
|
||||||
|
// by the user.
|
||||||
|
//
|
||||||
|
// If rename is false, write is performed by overwriting the target file after
|
||||||
|
// the data is successfully written to the temporary file.
|
||||||
|
// This means that the target file may remain corrupted if overwrite fails,
|
||||||
|
// but in such case the temporary file is preserved as a backup so the file
|
||||||
|
// can be recovered later. So it is less convenient than atomic write but more
|
||||||
|
// universal. Use SafeWrite with rename=false for files that may be managed
|
||||||
|
// directly by the user, like settings.json and bindings.json.
|
||||||
|
func SafeWrite(path string, bytes []byte, rename bool) error {
|
||||||
|
var err error
|
||||||
|
if _, err = os.Stat(path); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Force rename for new files!
|
||||||
|
rename = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var file *os.File
|
||||||
|
if !rename {
|
||||||
|
file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, FileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := AppendBackupSuffix(path)
|
||||||
|
err = os.WriteFile(tmp, bytes, FileMode)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rename {
|
||||||
|
err = os.Rename(tmp, path)
|
||||||
|
} else {
|
||||||
|
err = file.Truncate(0)
|
||||||
|
if err == nil {
|
||||||
|
_, err = file.Write(bytes)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = file.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if rename {
|
||||||
|
os.Remove(tmp)
|
||||||
|
} else {
|
||||||
|
err = OverwriteError{err, tmp}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rename {
|
||||||
|
os.Remove(tmp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -34,7 +33,7 @@ func main() {
|
|||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
os.Chdir(os.Args[1])
|
os.Chdir(os.Args[1])
|
||||||
}
|
}
|
||||||
files, _ := ioutil.ReadDir(".")
|
files, _ := os.ReadDir(".")
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
fname := f.Name()
|
fname := f.Name()
|
||||||
if strings.HasSuffix(fname, ".yaml") {
|
if strings.HasSuffix(fname, ".yaml") {
|
||||||
@ -46,7 +45,7 @@ func main() {
|
|||||||
func convert(name string) {
|
func convert(name string) {
|
||||||
filename := name + ".yaml"
|
filename := name + ".yaml"
|
||||||
var hdr HeaderYaml
|
var hdr HeaderYaml
|
||||||
source, err := ioutil.ReadFile(filename)
|
source, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -68,7 +67,7 @@ func encode(name string, c HeaderYaml) {
|
|||||||
|
|
||||||
func decode(name string) Header {
|
func decode(name string) Header {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
data, _ := ioutil.ReadFile(name + ".hdr")
|
data, _ := os.ReadFile(name + ".hdr")
|
||||||
strs := bytes.Split(data, []byte{'\n'})
|
strs := bytes.Split(data, []byte{'\n'})
|
||||||
var hdr Header
|
var hdr Header
|
||||||
hdr.FileType = string(strs[0])
|
hdr.FileType = string(strs[0])
|
||||||
|
@ -4,7 +4,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@ -161,6 +160,6 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, _ := ioutil.ReadFile(os.Args[1])
|
data, _ := os.ReadFile(os.Args[1])
|
||||||
fmt.Print(generateFile(parseFile(string(data), os.Args[1])))
|
fmt.Print(generateFile(parseFile(string(data), os.Args[1])))
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ func main() {
|
|||||||
</plist>
|
</plist>
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0644)
|
err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@ -19,7 +19,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
var data interface{}
|
var data interface{}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -210,7 +209,7 @@ func main() {
|
|||||||
var tests []test
|
var tests []test
|
||||||
|
|
||||||
for _, filename := range os.Args[1:] {
|
for _, filename := range os.Args[1:] {
|
||||||
source, err := ioutil.ReadFile(filename)
|
source, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user