lf/app.go

487 lines
9.8 KiB
Go
Raw Normal View History

2016-08-13 12:49:04 +00:00
package main
import (
"bufio"
2016-08-13 12:49:04 +00:00
"fmt"
"io"
2016-08-13 12:49:04 +00:00
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
2016-08-13 12:49:04 +00:00
"strings"
2020-06-11 01:11:40 +00:00
"syscall"
"time"
"github.com/gdamore/tcell/v2"
2016-08-13 12:49:04 +00:00
)
2017-05-15 09:30:50 +00:00
type cmdItem struct {
2017-11-19 18:55:13 +00:00
prefix string
value string
2017-05-15 09:30:50 +00:00
}
2016-12-17 21:47:37 +00:00
type app struct {
2018-05-15 21:16:49 +00:00
ui *ui
nav *nav
ticker *time.Ticker
quitChan chan struct{}
2018-05-15 21:16:49 +00:00
cmd *exec.Cmd
cmdIn io.WriteCloser
cmdOutBuf []byte
cmdHistory []cmdItem
cmdHistoryBeg int
2018-05-15 21:16:49 +00:00
cmdHistoryInd int
}
func newApp(screen tcell.Screen) *app {
ui := newUI(screen)
nav := newNav(ui.wins[0].h)
2016-10-29 23:20:35 +00:00
quitChan := make(chan struct{}, 1)
2020-10-20 21:31:04 +00:00
app := &app{
2017-11-19 18:55:13 +00:00
ui: ui,
nav: nav,
ticker: new(time.Ticker),
quitChan: quitChan,
}
2020-10-20 21:31:04 +00:00
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM)
go func() {
switch <-sigChan {
case os.Interrupt:
return
case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM:
app.writeHistory()
os.Remove(gLogPath)
os.Exit(3)
return
}
}()
return app
2016-08-13 12:49:04 +00:00
}
2017-11-19 18:55:13 +00:00
func (app *app) readFile(path string) {
log.Printf("reading file: %s", path)
2016-08-13 12:49:04 +00:00
2017-11-19 18:55:13 +00:00
f, err := os.Open(path)
if err != nil {
app.ui.echoerrf("opening file: %s", err)
2017-11-19 18:55:13 +00:00
return
}
defer f.Close()
2016-08-13 12:49:04 +00:00
2017-11-19 18:55:13 +00:00
p := newParser(f)
2018-05-20 17:30:41 +00:00
2017-11-19 18:55:13 +00:00
for p.parse() {
p.expr.eval(app, nil)
2016-08-13 12:49:04 +00:00
}
2017-11-19 18:55:13 +00:00
if p.err != nil {
app.ui.echoerrf("%s", p.err)
2017-11-19 18:55:13 +00:00
}
2016-08-13 12:49:04 +00:00
}
func (app *app) readHistory() error {
f, err := os.Open(gHistoryPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("opening history file: %s", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
toks := strings.SplitN(scanner.Text(), " ", 2)
if toks[0] != ":" && toks[0] != "$" && toks[0] != "%" && toks[0] != "!" && toks[0] != "&" {
continue
}
if len(toks) < 2 {
continue
}
app.cmdHistory = append(app.cmdHistory, cmdItem{toks[0], toks[1]})
}
app.cmdHistoryBeg = len(app.cmdHistory)
if err := scanner.Err(); err != nil {
return fmt.Errorf("reading history file: %s", err)
}
return nil
}
func (app *app) writeHistory() error {
if len(app.cmdHistory) == 0 {
return nil
}
local := make([]cmdItem, len(app.cmdHistory)-app.cmdHistoryBeg)
copy(local, app.cmdHistory[app.cmdHistoryBeg:])
app.cmdHistory = nil
if err := app.readHistory(); err != nil {
return fmt.Errorf("reading history file: %s", err)
}
app.cmdHistory = append(app.cmdHistory, local...)
if err := os.MkdirAll(filepath.Dir(gHistoryPath), os.ModePerm); err != nil {
return fmt.Errorf("creating data directory: %s", err)
}
f, err := os.Create(gHistoryPath)
if err != nil {
return fmt.Errorf("creating history file: %s", err)
}
defer f.Close()
if len(app.cmdHistory) > 1000 {
app.cmdHistory = app.cmdHistory[len(app.cmdHistory)-1000:]
}
for _, cmd := range app.cmdHistory {
_, err = f.WriteString(fmt.Sprintf("%s %s\n", cmd.prefix, cmd.value))
if err != nil {
return fmt.Errorf("writing history file: %s", err)
}
}
return nil
}
2018-05-20 17:30:41 +00:00
// This is the main event loop of the application. Expressions are read from
// the client and the server on separate goroutines and sent here over channels
// for evaluation. Similarly directories and regular files are also read in
// separate goroutines and sent here for update.
2017-11-19 18:55:13 +00:00
func (app *app) loop() {
serverChan := readExpr()
2016-10-29 23:20:35 +00:00
app.ui.readExpr()
if gSelect != "" {
go func() {
lstat, err := os.Lstat(gSelect)
if err != nil {
app.ui.exprChan <- &callExpr{"echoerr", []string{err.Error()}, 1}
} else if lstat.IsDir() {
app.ui.exprChan <- &callExpr{"cd", []string{gSelect}, 1}
} else {
app.ui.exprChan <- &callExpr{"select", []string{gSelect}, 1}
}
}()
}
for _, path := range gConfigPaths {
if _, err := os.Stat(path); !os.IsNotExist(err) {
app.readFile(path)
}
}
if gCommand != "" {
p := newParser(strings.NewReader(gCommand))
for p.parse() {
p.expr.eval(app, nil)
}
if p.err != nil {
app.ui.echoerrf("%s", p.err)
}
}
2016-08-13 12:49:04 +00:00
for {
select {
2017-11-19 18:55:13 +00:00
case <-app.quitChan:
if app.nav.copyTotal > 0 {
app.ui.echoerr("quit: copy operation in progress")
continue
}
if app.nav.moveTotal > 0 {
app.ui.echoerr("quit: move operation in progress")
continue
}
if app.nav.deleteTotal > 0 {
app.ui.echoerr("quit: delete operation in progress")
continue
}
app.nav.previewChan <- ""
2016-08-13 12:49:04 +00:00
log.Print("bye!")
2016-08-14 12:45:24 +00:00
if err := app.writeHistory(); err != nil {
log.Printf("writing history file: %s", err)
}
2016-08-14 12:45:24 +00:00
if gLastDirPath != "" {
f, err := os.Create(gLastDirPath)
if err != nil {
2016-08-17 19:09:34 +00:00
log.Printf("opening last dir file: %s", err)
2016-08-14 12:45:24 +00:00
}
defer f.Close()
2020-07-19 23:47:33 +00:00
_, err = f.WriteString(app.nav.currDir().path)
2016-08-14 12:45:24 +00:00
if err != nil {
2016-08-17 19:09:34 +00:00
log.Printf("writing last dir file: %s", err)
2016-08-14 12:45:24 +00:00
}
}
2016-08-13 12:49:04 +00:00
return
2019-02-28 18:04:38 +00:00
case n := <-app.nav.copyBytesChan:
2019-03-01 14:44:50 +00:00
app.nav.copyBytes += n
2019-03-01 01:01:33 +00:00
// n is usually 4096B so update roughly per 4096B x 1024 = 4MB copied
2019-02-28 18:04:38 +00:00
if app.nav.copyUpdate++; app.nav.copyUpdate >= 1024 {
app.nav.copyUpdate = 0
app.ui.draw(app.nav)
}
case n := <-app.nav.copyTotalChan:
app.nav.copyTotal += n
if n < 0 {
app.nav.copyBytes += n
}
2019-03-01 14:44:50 +00:00
if app.nav.copyTotal == 0 {
app.nav.copyUpdate = 0
}
app.ui.draw(app.nav)
case n := <-app.nav.moveCountChan:
app.nav.moveCount += n
if app.nav.moveUpdate++; app.nav.moveUpdate >= 1000 {
app.nav.moveUpdate = 0
app.ui.draw(app.nav)
}
case n := <-app.nav.moveTotalChan:
app.nav.moveTotal += n
if n < 0 {
app.nav.moveCount += n
}
if app.nav.moveTotal == 0 {
app.nav.moveUpdate = 0
}
2019-02-28 18:04:38 +00:00
app.ui.draw(app.nav)
case n := <-app.nav.deleteCountChan:
app.nav.deleteCount += n
if app.nav.deleteUpdate++; app.nav.deleteUpdate >= 1000 {
app.nav.deleteUpdate = 0
app.ui.draw(app.nav)
}
case n := <-app.nav.deleteTotalChan:
app.nav.deleteTotal += n
if n < 0 {
app.nav.deleteCount += n
}
if app.nav.deleteTotal == 0 {
app.nav.deleteUpdate = 0
}
app.ui.draw(app.nav)
case d := <-app.nav.dirChan:
app.nav.checkDir(d)
prev, ok := app.nav.dirCache[d.path]
if ok {
d.ind = prev.ind
d.sel(prev.name(), app.nav.height)
}
app.nav.dirCache[d.path] = d
for i := range app.nav.dirs {
if app.nav.dirs[i].path == d.path {
app.nav.dirs[i] = d
}
}
app.nav.position()
curr, err := app.nav.currFile()
if err == nil {
if d.path == app.nav.currDir().path {
app.ui.loadFile(app.nav, true)
}
if d.path == curr.path {
app.ui.dirPrev = d
}
}
app.ui.draw(app.nav)
case r := <-app.nav.regChan:
app.nav.checkReg(r)
app.nav.regCache[r.path] = r
curr, err := app.nav.currFile()
if err == nil {
if r.path == curr.path {
app.ui.regPrev = r
}
}
app.ui.draw(app.nav)
case ev := <-app.ui.evChan:
e := app.ui.readEvent(ev)
if e == nil {
continue
}
e.eval(app, nil)
loop:
for {
select {
case ev := <-app.ui.evChan:
e = app.ui.readEvent(ev)
if e == nil {
continue
}
e.eval(app, nil)
default:
break loop
}
}
app.ui.draw(app.nav)
case e := <-app.ui.exprChan:
e.eval(app, nil)
app.ui.draw(app.nav)
2016-10-29 23:20:35 +00:00
case e := <-serverChan:
e.eval(app, nil)
app.ui.draw(app.nav)
case <-app.ticker.C:
app.nav.renew()
app.ui.loadFile(app.nav, false)
app.ui.draw(app.nav)
2016-08-13 12:49:04 +00:00
}
}
}
func (app *app) exportFiles() {
2019-03-17 17:16:19 +00:00
var currFile string
2020-07-19 23:47:33 +00:00
if curr, err := app.nav.currFile(); err == nil {
currFile = curr.path
2016-08-13 12:49:04 +00:00
}
2019-03-17 17:16:19 +00:00
currSelections := app.nav.currSelections()
2016-08-13 12:49:04 +00:00
2019-03-17 17:16:19 +00:00
exportFiles(currFile, currSelections)
2016-08-13 12:49:04 +00:00
}
2017-11-19 18:55:13 +00:00
func waitKey() error {
cmd := pauseCommand()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("waiting key: %s", err)
}
return nil
}
2018-05-20 17:30:41 +00:00
// This function is used to run a shell command. Modes are as follows:
2016-08-13 12:49:04 +00:00
//
2018-05-20 17:30:41 +00:00
// Prefix Wait Async Stdin Stdout Stderr UI action
// $ No No Yes Yes Yes Pause and then resume
// % No No Yes Yes Yes Statline for input/output
// ! Yes No Yes Yes Yes Pause and then resume
// & No Yes No No No Do nothing
func (app *app) runShell(s string, args []string, prefix string) {
app.exportFiles()
exportOpts()
2016-08-13 12:49:04 +00:00
cmd := shellCommand(s, args)
2016-08-13 12:49:04 +00:00
var out io.Reader
2019-10-07 16:08:39 +00:00
var err error
switch prefix {
case "$", "!":
2016-08-13 12:49:04 +00:00
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
app.nav.previewChan <- ""
2016-08-13 12:49:04 +00:00
app.ui.pause()
defer app.ui.resume()
2018-04-15 15:18:39 +00:00
defer app.nav.renew()
2019-10-07 16:08:39 +00:00
err = cmd.Run()
case "%":
2018-04-03 19:22:58 +00:00
stdin, err := cmd.StdinPipe()
if err != nil {
2018-04-03 19:22:58 +00:00
log.Printf("writing stdin: %s", err)
}
2018-04-03 19:22:58 +00:00
app.cmdIn = stdin
stdout, err := cmd.StdoutPipe()
if err != nil {
2018-04-03 19:22:58 +00:00
log.Printf("reading stdout: %s", err)
}
2018-04-03 19:22:58 +00:00
out = stdout
cmd.Stderr = cmd.Stdout
2019-10-07 16:08:39 +00:00
fallthrough
case "&":
err = cmd.Start()
2016-08-13 12:49:04 +00:00
}
if err != nil {
app.ui.echoerrf("running shell: %s", err)
2016-08-13 12:49:04 +00:00
}
switch prefix {
case "!":
2016-08-13 12:49:04 +00:00
if err := waitKey(); err != nil {
app.ui.echoerrf("waiting key: %s", err)
2016-08-13 12:49:04 +00:00
}
}
app.ui.loadFile(app.nav, true)
switch prefix {
case "%":
app.cmd = cmd
app.cmdOutBuf = nil
app.ui.msg = ""
app.ui.cmdPrefix = ">"
go func() {
2020-11-22 21:13:31 +00:00
eol := false
reader := bufio.NewReader(out)
for {
b, err := reader.ReadByte()
if err == io.EOF {
break
}
2020-11-22 21:13:31 +00:00
if eol {
eol = false
app.cmdOutBuf = nil
}
app.cmdOutBuf = append(app.cmdOutBuf, b)
if b == '\n' || b == '\r' {
2020-11-22 21:13:31 +00:00
eol = true
}
if reader.Buffered() > 0 {
continue
2018-04-03 19:22:58 +00:00
}
2020-11-22 21:13:31 +00:00
app.ui.exprChan <- &callExpr{"echo", []string{string(app.cmdOutBuf)}, 1}
2018-04-03 19:22:58 +00:00
}
if err := cmd.Wait(); err != nil {
log.Printf("running shell: %s", err)
}
app.cmd = nil
2018-04-03 19:22:58 +00:00
app.ui.cmdPrefix = ""
app.ui.exprChan <- &callExpr{"load", nil, 1}
2018-04-03 19:22:58 +00:00
}()
case "&":
go func() {
if err := cmd.Wait(); err != nil {
log.Printf("running shell: %s", err)
}
}()
}
2016-08-13 12:49:04 +00:00
}