diff --git a/src/constants.go b/src/constants.go index a8e2091..5f583e4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -22,10 +22,11 @@ const ( readerPollIntervalMax = 50 * time.Millisecond // Terminal - initialDelay = 20 * time.Millisecond - initialDelayTac = 100 * time.Millisecond - spinnerDuration = 200 * time.Millisecond - maxPatternLength = 300 + initialDelay = 20 * time.Millisecond + initialDelayTac = 100 * time.Millisecond + spinnerDuration = 200 * time.Millisecond + previewCancelWait = 500 * time.Millisecond + maxPatternLength = 300 // Matcher numPartitionsMultiplier = 8 @@ -76,6 +77,7 @@ const ( ) const ( + exitCancel = -1 exitOk = 0 exitNoMatch = 1 exitError = 2 diff --git a/src/reader.go b/src/reader.go index 5fd6d87..b418f54 100644 --- a/src/reader.go +++ b/src/reader.go @@ -103,7 +103,7 @@ func (r *Reader) readFromStdin() bool { } func (r *Reader) readFromCommand(shell string, cmd string) bool { - listCommand := util.ExecCommandWith(shell, cmd) + listCommand := util.ExecCommandWith(shell, cmd, false) out, err := listCommand.StdoutPipe() if err != nil { return false diff --git a/src/terminal.go b/src/terminal.go index 139aaca..cae349d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -114,6 +114,7 @@ type Terminal struct { prevLines []itemLine suppress bool startChan chan bool + killChan chan int slab *util.Slab theme *tui.ColorTheme tui tui.Renderer @@ -414,6 +415,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), + killChan: make(chan int), tui: renderer, initFunc: func() { renderer.Init() }} t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0) @@ -1298,7 +1300,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo return } command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) - cmd := util.ExecCommand(command) + cmd := util.ExecCommand(command, false) if !background { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -1381,6 +1383,20 @@ func (t *Terminal) toggleItem(item *Item) { } } +func (t *Terminal) killPreview(code int) { + select { + case t.killChan <- code: + default: + if code != exitCancel { + os.Exit(code) + } + } +} + +func (t *Terminal) cancelPreview() { + t.killPreview(exitCancel) +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() { // prof := profile.Start(profile.ProfilePath("/tmp/")) @@ -1458,15 +1474,43 @@ func (t *Terminal) Loop() { if request[0] != nil { command := replacePlaceholder(t.preview.command, t.ansi, t.delimiter, false, string(t.input), request) - cmd := util.ExecCommand(command) + cmd := util.ExecCommand(command, true) if t.pwindow != nil { env := os.Environ() env = append(env, fmt.Sprintf("LINES=%d", t.pwindow.Height())) env = append(env, fmt.Sprintf("COLUMNS=%d", t.pwindow.Width())) cmd.Env = env } - out, _ := cmd.CombinedOutput() - t.reqBox.Set(reqPreviewDisplay, string(out)) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + cmd.Start() + finishChan := make(chan bool, 1) + updateChan := make(chan bool) + go func() { + select { + case code := <-t.killChan: + if code != exitCancel { + util.KillCommand(cmd) + os.Exit(code) + } else { + select { + case <-time.After(previewCancelWait): + util.KillCommand(cmd) + updateChan <- true + case <-finishChan: + updateChan <- false + } + } + case <-finishChan: + updateChan <- false + } + }() + cmd.Wait() + finishChan <- true + if out.Len() > 0 || !<-updateChan { + t.reqBox.Set(reqPreviewDisplay, string(out.Bytes())) + } } else { t.reqBox.Set(reqPreviewDisplay, "") } @@ -1484,7 +1528,7 @@ func (t *Terminal) Loop() { t.history.append(string(t.input)) } // prof.Stop() - os.Exit(code) + t.killPreview(code) } go func() { @@ -1511,6 +1555,7 @@ func (t *Terminal) Loop() { focused = currentFocus if t.isPreviewEnabled() { _, list := t.buildPlusList(t.preview.command, false) + t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, list) } } @@ -1620,6 +1665,7 @@ func (t *Terminal) Loop() { if t.previewer.enabled { valid, list := t.buildPlusList(t.preview.command, false) if valid { + t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, list) } } diff --git a/src/util/util_unix.go b/src/util/util_unix.go index fc63c02..6331275 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -9,17 +9,26 @@ import ( ) // ExecCommand executes the given command with $SHELL -func ExecCommand(command string) *exec.Cmd { +func ExecCommand(command string, setpgid bool) *exec.Cmd { shell := os.Getenv("SHELL") if len(shell) == 0 { shell = "sh" } - return ExecCommandWith(shell, command) + return ExecCommandWith(shell, command, setpgid) } // ExecCommandWith executes the given command with the specified shell -func ExecCommandWith(shell string, command string) *exec.Cmd { - return exec.Command(shell, "-c", command) +func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { + cmd := exec.Command(shell, "-c", command) + if setpgid { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + } + return cmd +} + +// KillCommand kills the process for the given command +func KillCommand(cmd *exec.Cmd) error { + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) } // IsWindows returns true on Windows @@ -27,7 +36,7 @@ func IsWindows() bool { return false } -// SetNonBlock executes syscall.SetNonblock on file descriptor +// SetNonblock executes syscall.SetNonblock on file descriptor func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(int(file.Fd()), nonblock) } diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 41a9a5c..51715cd 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -10,13 +10,15 @@ import ( ) // ExecCommand executes the given command with cmd -func ExecCommand(command string) *exec.Cmd { +func ExecCommand(command string, setpgid bool) *exec.Cmd { return ExecCommandWith("cmd", command) } // ExecCommandWith executes the given command with cmd. _shell parameter is // ignored on Windows. -func ExecCommandWith(_shell string, command string) *exec.Cmd { +// FIXME: setpgid is unused. We set it in the Unix implementation so that we +// can kill preview process with its child processes at once. +func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd { cmd := exec.Command("cmd") cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: false, @@ -26,12 +28,17 @@ func ExecCommandWith(_shell string, command string) *exec.Cmd { return cmd } +// KillCommand kills the process for the given command +func KillCommand(cmd *exec.Cmd) error { + return cmd.Process.Kill() +} + // IsWindows returns true on Windows func IsWindows() bool { return true } -// SetNonBlock executes syscall.SetNonblock on file descriptor +// SetNonblock executes syscall.SetNonblock on file descriptor func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) }