lf/app.go
Gokcehan 5f87cb2542 implement asynchronous read commands
This commit changes previous reading command implementation to an
asynchronous implementation. By the nature of this change, this commit
touches many places in the ui and evaluator. Aim is to fix the following
problems:

- There is no race condition anymore when reading commands and other
commands update the ui at the same time.

- Autocompletion and keymenu is now drawn in the main draw event. This
should fix some ui glitches when a new menu is smaller than the previous
one.

- Window resize event when reading a command is now properly handled.

- Readline actions are now regular commands. This should make it
possible to change the default keybindings for these actions in the
future.

Mentioned in #36.
2016-12-15 12:26:06 +03:00

176 lines
3.3 KiB
Go

package main
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"strconv"
"strings"
)
type App struct {
ui *UI
nav *Nav
quit chan bool
}
func newApp() *App {
ui := newUI()
nav := newNav(ui.wins[0].h)
quit := make(chan bool, 1)
return &App{
ui: ui,
nav: nav,
quit: quit,
}
}
func waitKey() error {
// TODO: this should be done with termbox somehow
c := `echo
echo -n 'Press any key to continue'
old=$(stty -g)
stty raw -echo
eval "ignore=\$(dd bs=1 count=1 2> /dev/null)"
stty $old
echo`
cmd := exec.Command(gOpts.shell, "-c", c)
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
}
// This is the main event loop of the application. There are two channels to
// read expressions from client and server. Reading and evaluation are done on
// separate goroutines.
func (app *App) handleInp() {
clientChan := app.ui.readExpr()
var serverChan chan Expr
c, err := net.Dial("unix", gSocketPath)
if err != nil {
msg := fmt.Sprintf("connecting server: %s", err)
app.ui.message = msg
log.Printf(msg)
} else {
serverChan = readExpr(c)
}
for {
select {
case <-app.quit:
log.Print("bye!")
if gLastDirPath != "" {
f, err := os.Create(gLastDirPath)
if err != nil {
log.Printf("opening last dir file: %s", err)
}
defer f.Close()
dir := app.nav.currDir()
_, err = f.WriteString(dir.path)
if err != nil {
log.Printf("writing last dir file: %s", err)
}
}
return
case e := <-clientChan:
for i := 0; i < e.count; i++ {
e.expr.eval(app, nil)
}
app.ui.draw(app.nav)
case e := <-serverChan:
e.eval(app, nil)
app.ui.draw(app.nav)
}
}
}
func (app *App) exportVars() {
var envFile string
if f, err := app.nav.currFile(); err == nil {
envFile = f.Path
}
marks := app.nav.currMarks()
envFiles := strings.Join(marks, ":")
os.Setenv("f", envFile)
os.Setenv("fs", envFiles)
if len(marks) == 0 {
os.Setenv("fx", envFile)
} else {
os.Setenv("fx", envFiles)
}
os.Setenv("id", strconv.Itoa(gClientId))
}
// This function is used to run a command in shell. Following modes are used:
//
// Prefix Wait Async Stdin/Stdout/Stderr UI action
// $ No No Yes Pause and then resume
// ! Yes No Yes Pause and then resume
// & No Yes No Do nothing
//
// Waiting async commands are not used for now.
func (app *App) runShell(s string, args []string, wait bool, async bool) {
app.exportVars()
if len(gOpts.ifs) != 0 {
s = fmt.Sprintf("IFS='%s'; %s", gOpts.ifs, s)
}
args = append([]string{"-c", s, "--"}, args...)
cmd := exec.Command(gOpts.shell, args...)
if !async {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
app.ui.pause()
defer app.ui.resume()
defer app.nav.renew(app.ui.wins[0].h)
}
var err error
if async {
err = cmd.Start()
} else {
err = cmd.Run()
}
if err != nil {
msg := fmt.Sprintf("running shell: %s", err)
app.ui.message = msg
log.Print(msg)
}
if wait {
if err := waitKey(); err != nil {
msg := fmt.Sprintf("waiting shell: %s", err)
app.ui.message = msg
log.Print(msg)
}
}
}