702 lines
18 KiB
Go
702 lines
18 KiB
Go
|
package window
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math/bits"
|
||
|
"math/rand"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"os/signal"
|
||
|
"os/user"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"b612.me/apps/b612/bed/event"
|
||
|
"b612.me/apps/b612/bed/layout"
|
||
|
"b612.me/apps/b612/bed/state"
|
||
|
)
|
||
|
|
||
|
// Manager manages the windows and files.
|
||
|
type Manager struct {
|
||
|
width int
|
||
|
height int
|
||
|
windows []*window
|
||
|
layout layout.Layout
|
||
|
mu *sync.Mutex
|
||
|
windowIndex int
|
||
|
prevWindowIndex int
|
||
|
prevDir string
|
||
|
files map[string]file
|
||
|
eventCh chan<- event.Event
|
||
|
redrawCh chan<- struct{}
|
||
|
}
|
||
|
|
||
|
type file struct {
|
||
|
path string
|
||
|
file *os.File
|
||
|
perm os.FileMode
|
||
|
}
|
||
|
|
||
|
// NewManager creates a new Manager.
|
||
|
func NewManager() *Manager {
|
||
|
return &Manager{}
|
||
|
}
|
||
|
|
||
|
// Init initializes the Manager.
|
||
|
func (m *Manager) Init(eventCh chan<- event.Event, redrawCh chan<- struct{}) {
|
||
|
m.eventCh, m.redrawCh = eventCh, redrawCh
|
||
|
m.mu, m.files = new(sync.Mutex), make(map[string]file)
|
||
|
}
|
||
|
|
||
|
// Open a new window.
|
||
|
func (m *Manager) Open(name string) error {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
window, err := m.open(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return m.init(window)
|
||
|
}
|
||
|
|
||
|
// Read opens a new window from [io.Reader].
|
||
|
func (m *Manager) Read(r io.Reader) error {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
return m.read(r)
|
||
|
}
|
||
|
|
||
|
func (m *Manager) init(window *window) error {
|
||
|
m.addWindow(window)
|
||
|
m.layout = layout.NewLayout(m.windowIndex).Resize(0, 0, m.width, m.height)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) addWindow(window *window) {
|
||
|
for i, w := range m.windows {
|
||
|
if w == window {
|
||
|
m.windowIndex, m.prevWindowIndex = i, m.windowIndex
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
m.windows = append(m.windows, window)
|
||
|
m.windowIndex, m.prevWindowIndex = len(m.windows)-1, m.windowIndex
|
||
|
}
|
||
|
|
||
|
func (m *Manager) open(name string) (*window, error) {
|
||
|
if name == "" {
|
||
|
window, err := newWindow(bytes.NewReader(nil), "", "", m.eventCh, m.redrawCh)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return window, nil
|
||
|
}
|
||
|
if name == "#" {
|
||
|
return m.windows[m.prevWindowIndex], nil
|
||
|
}
|
||
|
if strings.HasPrefix(name, "#") {
|
||
|
index, err := strconv.Atoi(name[1:])
|
||
|
if err != nil || index <= 0 || len(m.windows) < index {
|
||
|
return nil, errors.New("invalid window index: " + name)
|
||
|
}
|
||
|
return m.windows[index-1], nil
|
||
|
}
|
||
|
name, err := expandBacktick(name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
path, err := expandPath(name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
r, err := m.openFile(path, name)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return newWindow(r, path, filepath.Base(path), m.eventCh, m.redrawCh)
|
||
|
}
|
||
|
|
||
|
func (m *Manager) openFile(path, name string) (readAtSeeker, error) {
|
||
|
fi, err := os.Stat(path)
|
||
|
if err != nil {
|
||
|
if !os.IsNotExist(err) {
|
||
|
return nil, err
|
||
|
}
|
||
|
return bytes.NewReader(nil), nil
|
||
|
} else if fi.IsDir() {
|
||
|
return nil, errors.New(name + " is a directory")
|
||
|
}
|
||
|
f, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
m.addFile(path, f, fi)
|
||
|
return f, nil
|
||
|
}
|
||
|
|
||
|
func expandBacktick(name string) (string, error) {
|
||
|
if len(name) <= 2 || name[0] != '`' || name[len(name)-1] != '`' {
|
||
|
return name, nil
|
||
|
}
|
||
|
name = strings.TrimSpace(name[1 : len(name)-1])
|
||
|
xs := strings.Fields(name)
|
||
|
if len(xs) < 1 {
|
||
|
return name, nil
|
||
|
}
|
||
|
out, err := exec.Command(xs[0], xs[1:]...).Output()
|
||
|
if err != nil {
|
||
|
return name, err
|
||
|
}
|
||
|
return strings.TrimSpace(string(out)), nil
|
||
|
}
|
||
|
|
||
|
func expandPath(path string) (string, error) {
|
||
|
switch {
|
||
|
case strings.HasPrefix(path, "~"):
|
||
|
if name, rest, _ := strings.Cut(path[1:], string(filepath.Separator)); name != "" {
|
||
|
user, err := user.Lookup(name)
|
||
|
if err != nil {
|
||
|
return path, nil
|
||
|
}
|
||
|
return filepath.Join(user.HomeDir, rest), nil
|
||
|
}
|
||
|
homeDir, err := os.UserHomeDir()
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return filepath.Join(homeDir, path[1:]), nil
|
||
|
case strings.HasPrefix(path, "$"):
|
||
|
name, rest, _ := strings.Cut(path[1:], string(filepath.Separator))
|
||
|
value := os.Getenv(name)
|
||
|
if value == "" {
|
||
|
return path, nil
|
||
|
}
|
||
|
return filepath.Join(value, rest), nil
|
||
|
default:
|
||
|
return filepath.Abs(path)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Manager) read(r io.Reader) error {
|
||
|
bs, err := func() ([]byte, error) {
|
||
|
r, stop := newReader(r)
|
||
|
defer stop()
|
||
|
return io.ReadAll(r)
|
||
|
}()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
window, err := newWindow(bytes.NewReader(bs), "", "", m.eventCh, m.redrawCh)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return m.init(window)
|
||
|
}
|
||
|
|
||
|
type reader struct {
|
||
|
io.Reader
|
||
|
abort chan os.Signal
|
||
|
}
|
||
|
|
||
|
func newReader(r io.Reader) (*reader, func()) {
|
||
|
done := make(chan struct{})
|
||
|
abort := make(chan os.Signal, 1)
|
||
|
signal.Notify(abort, os.Interrupt)
|
||
|
go func() {
|
||
|
select {
|
||
|
case <-time.After(time.Second):
|
||
|
fmt.Fprint(os.Stderr, "Reading stdin took more than 1 second, press <C-c> to abort...")
|
||
|
case <-done:
|
||
|
}
|
||
|
}()
|
||
|
return &reader{r, abort}, func() {
|
||
|
signal.Stop(abort)
|
||
|
close(abort)
|
||
|
close(done)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *reader) Read(p []byte) (int, error) {
|
||
|
select {
|
||
|
case <-r.abort:
|
||
|
return 0, io.EOF
|
||
|
default:
|
||
|
}
|
||
|
return r.Reader.Read(p)
|
||
|
}
|
||
|
|
||
|
// SetSize sets the size of the screen.
|
||
|
func (m *Manager) SetSize(width, height int) {
|
||
|
m.width, m.height = width, height
|
||
|
}
|
||
|
|
||
|
// Resize sets the size of the screen.
|
||
|
func (m *Manager) Resize(width, height int) {
|
||
|
if m.width != width || m.height != height {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
m.width, m.height = width, height
|
||
|
m.layout = m.layout.Resize(0, 0, width, height)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Emit an event to the current window.
|
||
|
func (m *Manager) Emit(e event.Event) {
|
||
|
switch e.Type {
|
||
|
case event.Edit:
|
||
|
if err := m.edit(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.Enew:
|
||
|
if err := m.enew(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.New:
|
||
|
if err := m.newWindow(e, false); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.Vnew:
|
||
|
if err := m.newWindow(e, true); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.Only:
|
||
|
if err := m.only(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.Alternative:
|
||
|
m.alternative(e)
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
case event.Wincmd:
|
||
|
if e.Arg == "" {
|
||
|
m.eventCh <- event.Event{Type: event.Error,
|
||
|
Error: errors.New("an argument is required for " + e.CmdName)}
|
||
|
} else if err := m.wincmd(e.Arg); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowDown:
|
||
|
if err := m.wincmd("j"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowUp:
|
||
|
if err := m.wincmd("k"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowLeft:
|
||
|
if err := m.wincmd("h"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowRight:
|
||
|
if err := m.wincmd("l"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowTopLeft:
|
||
|
if err := m.wincmd("t"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowBottomRight:
|
||
|
if err := m.wincmd("b"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.FocusWindowPrevious:
|
||
|
if err := m.wincmd("p"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.MoveWindowTop:
|
||
|
if err := m.wincmd("K"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.MoveWindowBottom:
|
||
|
if err := m.wincmd("J"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.MoveWindowLeft:
|
||
|
if err := m.wincmd("H"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.MoveWindowRight:
|
||
|
if err := m.wincmd("L"); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
case event.Pwd:
|
||
|
if e.Arg != "" {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: errors.New("too many arguments for " + e.CmdName)}
|
||
|
break
|
||
|
}
|
||
|
fallthrough
|
||
|
case event.Chdir:
|
||
|
if dir, err := m.chdir(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Info, Error: errors.New(dir)}
|
||
|
}
|
||
|
case event.Quit:
|
||
|
if err := m.quit(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
}
|
||
|
case event.Write:
|
||
|
if name, n, err := m.write(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else {
|
||
|
m.eventCh <- event.Event{Type: event.Info,
|
||
|
Error: fmt.Errorf("%s: %[2]d (0x%[2]x) bytes written", name, n)}
|
||
|
}
|
||
|
case event.WriteQuit:
|
||
|
if _, _, err := m.write(e); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
} else if err := m.quit(event.Event{Bang: e.Bang}); err != nil {
|
||
|
m.eventCh <- event.Event{Type: event.Error, Error: err}
|
||
|
}
|
||
|
default:
|
||
|
m.windows[m.windowIndex].emit(e)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Manager) edit(e event.Event) error {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
name := e.Arg
|
||
|
if name == "" {
|
||
|
name = m.windows[m.windowIndex].path
|
||
|
}
|
||
|
window, err := m.open(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
m.addWindow(window)
|
||
|
m.layout = m.layout.Replace(m.windowIndex)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) enew(e event.Event) error {
|
||
|
if e.Arg != "" {
|
||
|
return errors.New("too many arguments for " + e.CmdName)
|
||
|
}
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
window, err := m.open("")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
m.addWindow(window)
|
||
|
m.layout = m.layout.Replace(m.windowIndex)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) newWindow(e event.Event, vertical bool) error {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
window, err := m.open(e.Arg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
m.addWindow(window)
|
||
|
if vertical {
|
||
|
m.layout = m.layout.SplitLeft(m.windowIndex).Resize(0, 0, m.width, m.height)
|
||
|
} else {
|
||
|
m.layout = m.layout.SplitTop(m.windowIndex).Resize(0, 0, m.width, m.height)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) only(e event.Event) error {
|
||
|
if e.Arg != "" {
|
||
|
return errors.New("too many arguments for " + e.CmdName)
|
||
|
}
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
if !e.Bang {
|
||
|
for windowIndex, w := range m.layout.Collect() {
|
||
|
if window := m.windows[windowIndex]; !w.Active && window.changedTick != window.savedChangedTick {
|
||
|
return errors.New("you have unsaved changes in " + window.getName() + ", add ! to force :only")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
m.layout = layout.NewLayout(m.windowIndex).Resize(0, 0, m.width, m.height)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) alternative(e event.Event) {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
if e.Count == 0 {
|
||
|
m.windowIndex, m.prevWindowIndex = m.prevWindowIndex, m.windowIndex
|
||
|
} else if 0 < e.Count && e.Count <= int64(len(m.windows)) {
|
||
|
m.windowIndex, m.prevWindowIndex = int(e.Count)-1, m.windowIndex
|
||
|
}
|
||
|
m.layout = m.layout.Replace(m.windowIndex)
|
||
|
}
|
||
|
|
||
|
func (m *Manager) wincmd(arg string) error {
|
||
|
switch arg {
|
||
|
case "n":
|
||
|
return m.newWindow(event.Event{}, false)
|
||
|
case "o":
|
||
|
return m.only(event.Event{})
|
||
|
case "l":
|
||
|
m.focus(func(x, y layout.Window) bool {
|
||
|
return x.LeftMargin()+x.Width()+1 == y.LeftMargin() &&
|
||
|
y.TopMargin() <= x.TopMargin() &&
|
||
|
x.TopMargin() < y.TopMargin()+y.Height()
|
||
|
})
|
||
|
case "h":
|
||
|
m.focus(func(x, y layout.Window) bool {
|
||
|
return y.LeftMargin()+y.Width()+1 == x.LeftMargin() &&
|
||
|
y.TopMargin() <= x.TopMargin() &&
|
||
|
x.TopMargin() < y.TopMargin()+y.Height()
|
||
|
})
|
||
|
case "k":
|
||
|
m.focus(func(x, y layout.Window) bool {
|
||
|
return y.TopMargin()+y.Height() == x.TopMargin() &&
|
||
|
y.LeftMargin() <= x.LeftMargin() &&
|
||
|
x.LeftMargin() < y.LeftMargin()+y.Width()
|
||
|
})
|
||
|
case "j":
|
||
|
m.focus(func(x, y layout.Window) bool {
|
||
|
return x.TopMargin()+x.Height() == y.TopMargin() &&
|
||
|
y.LeftMargin() <= x.LeftMargin() &&
|
||
|
x.LeftMargin() < y.LeftMargin()+y.Width()
|
||
|
})
|
||
|
case "t":
|
||
|
m.focus(func(_, y layout.Window) bool {
|
||
|
return y.LeftMargin() == 0 && y.TopMargin() == 0
|
||
|
})
|
||
|
case "b":
|
||
|
m.focus(func(_, y layout.Window) bool {
|
||
|
return m.layout.LeftMargin()+m.layout.Width() == y.LeftMargin()+y.Width() &&
|
||
|
m.layout.TopMargin()+m.layout.Height() == y.TopMargin()+y.Height()
|
||
|
})
|
||
|
case "p":
|
||
|
m.focus(func(_, y layout.Window) bool {
|
||
|
return y.Index == m.prevWindowIndex
|
||
|
})
|
||
|
case "K":
|
||
|
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
|
||
|
return layout.Horizontal{Top: x, Bottom: y}
|
||
|
})
|
||
|
case "J":
|
||
|
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
|
||
|
return layout.Horizontal{Top: y, Bottom: x}
|
||
|
})
|
||
|
case "H":
|
||
|
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
|
||
|
return layout.Vertical{Left: x, Right: y}
|
||
|
})
|
||
|
case "L":
|
||
|
m.move(func(x layout.Window, y layout.Layout) layout.Layout {
|
||
|
return layout.Vertical{Left: y, Right: x}
|
||
|
})
|
||
|
default:
|
||
|
return errors.New("Invalid argument for wincmd: " + arg)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) focus(search func(layout.Window, layout.Window) bool) {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
activeWindow := m.layout.ActiveWindow()
|
||
|
newWindow := m.layout.Lookup(func(l layout.Window) bool {
|
||
|
return search(activeWindow, l)
|
||
|
})
|
||
|
if newWindow.Index >= 0 {
|
||
|
m.windowIndex, m.prevWindowIndex = newWindow.Index, m.windowIndex
|
||
|
m.layout = m.layout.Activate(m.windowIndex)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Manager) move(modifier func(layout.Window, layout.Layout) layout.Layout) {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
w, h := m.layout.Count()
|
||
|
if w != 1 || h != 1 {
|
||
|
activeWindow := m.layout.ActiveWindow()
|
||
|
m.layout = modifier(activeWindow, m.layout.Close()).Activate(
|
||
|
activeWindow.Index).Resize(0, 0, m.width, m.height)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Manager) chdir(e event.Event) (string, error) {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
if e.Arg == "-" && m.prevDir == "" {
|
||
|
return "", errors.New("no previous working directory")
|
||
|
}
|
||
|
dir, err := os.Getwd()
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
if e.Arg == "" {
|
||
|
return dir, nil
|
||
|
}
|
||
|
if e.Arg != "-" {
|
||
|
dir, m.prevDir = e.Arg, dir
|
||
|
} else {
|
||
|
dir, m.prevDir = m.prevDir, dir
|
||
|
}
|
||
|
if dir, err = expandPath(dir); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
if err = os.Chdir(dir); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return os.Getwd()
|
||
|
}
|
||
|
|
||
|
func (m *Manager) quit(e event.Event) error {
|
||
|
if e.Arg != "" {
|
||
|
return errors.New("too many arguments for " + e.CmdName)
|
||
|
}
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
window := m.windows[m.windowIndex]
|
||
|
if window.changedTick != window.savedChangedTick && !e.Bang {
|
||
|
return errors.New("you have unsaved changes in " + window.getName() + ", add ! to force :quit")
|
||
|
}
|
||
|
w, h := m.layout.Count()
|
||
|
if w == 1 && h == 1 {
|
||
|
m.eventCh <- event.Event{Type: event.QuitAll}
|
||
|
} else {
|
||
|
m.layout = m.layout.Close().Resize(0, 0, m.width, m.height)
|
||
|
m.windowIndex, m.prevWindowIndex = m.layout.ActiveWindow().Index, m.windowIndex
|
||
|
m.eventCh <- event.Event{Type: event.Redraw}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (m *Manager) write(e event.Event) (string, int64, error) {
|
||
|
if e.Range != nil && e.Arg == "" {
|
||
|
return "", 0, errors.New("cannot overwrite partially with " + e.CmdName)
|
||
|
}
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
window := m.windows[m.windowIndex]
|
||
|
var path string
|
||
|
name := e.Arg
|
||
|
if name == "" {
|
||
|
if window.name == "" {
|
||
|
return "", 0, errors.New("no file name")
|
||
|
}
|
||
|
path, name = window.path, window.name
|
||
|
} else {
|
||
|
var err error
|
||
|
path, err = expandPath(name)
|
||
|
if err != nil {
|
||
|
return "", 0, err
|
||
|
}
|
||
|
}
|
||
|
if runtime.GOOS == "windows" && m.opened(path) {
|
||
|
return "", 0, errors.New("cannot overwrite the original file on Windows")
|
||
|
}
|
||
|
if window.path == "" && window.name == "" {
|
||
|
window.setPathName(path, filepath.Base(path))
|
||
|
}
|
||
|
tmpf, err := os.OpenFile(
|
||
|
path+"-"+strconv.FormatUint(rand.Uint64(), 36),
|
||
|
os.O_RDWR|os.O_CREATE|os.O_EXCL, m.filePerm(path),
|
||
|
) //#nosec G404
|
||
|
if err != nil {
|
||
|
return "", 0, err
|
||
|
}
|
||
|
defer os.Remove(tmpf.Name())
|
||
|
n, err := window.writeTo(e.Range, tmpf)
|
||
|
if err != nil {
|
||
|
_ = tmpf.Close()
|
||
|
return "", 0, err
|
||
|
}
|
||
|
if err = tmpf.Close(); err != nil {
|
||
|
return "", 0, err
|
||
|
}
|
||
|
if window.path == path {
|
||
|
window.savedChangedTick = window.changedTick
|
||
|
}
|
||
|
return name, n, os.Rename(tmpf.Name(), path)
|
||
|
}
|
||
|
|
||
|
func (m *Manager) addFile(path string, f *os.File, fi os.FileInfo) {
|
||
|
m.files[path] = file{path: path, file: f, perm: fi.Mode().Perm()}
|
||
|
}
|
||
|
|
||
|
func (m *Manager) opened(path string) bool {
|
||
|
_, ok := m.files[path]
|
||
|
return ok
|
||
|
}
|
||
|
|
||
|
func (m *Manager) filePerm(path string) os.FileMode {
|
||
|
if f, ok := m.files[path]; ok {
|
||
|
return f.perm
|
||
|
}
|
||
|
return os.FileMode(0o644)
|
||
|
}
|
||
|
|
||
|
// State returns the state of the windows.
|
||
|
func (m *Manager) State() (map[int]*state.WindowState, layout.Layout, int, error) {
|
||
|
m.mu.Lock()
|
||
|
defer m.mu.Unlock()
|
||
|
layouts := m.layout.Collect()
|
||
|
states := make(map[int]*state.WindowState, len(m.windows))
|
||
|
for i, window := range m.windows {
|
||
|
if l, ok := layouts[i]; ok {
|
||
|
var err error
|
||
|
if states[i], err = window.state(
|
||
|
hexWindowWidth(l.Width()), max(l.Height()-2, 1),
|
||
|
); err != nil {
|
||
|
return nil, m.layout, 0, err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return states, m.layout, m.windowIndex, nil
|
||
|
}
|
||
|
|
||
|
func hexWindowWidth(width int) int {
|
||
|
width = min(max((width-18)/4, 4), 256)
|
||
|
return width & (0b11 << (bits.Len(uint(width)) - 2))
|
||
|
}
|
||
|
|
||
|
// Close the Manager.
|
||
|
func (m *Manager) Close() {
|
||
|
for _, f := range m.files {
|
||
|
_ = f.file.Close()
|
||
|
}
|
||
|
}
|