1// Copyright 2018 The up AUTHORS
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// up is the Ultimate Plumber, a tool for writing Linux pipes in a
16// terminal-based UI interactively, with instant live preview of command
17// results.
18package main
19
20import (
21	"bufio"
22	"bytes"
23	"context"
24	"crypto/sha1"
25	"errors"
26	"fmt"
27	"io"
28	"io/ioutil"
29	"log"
30	"os"
31	"os/exec"
32	"sync"
33
34	"github.com/gdamore/tcell"
35	"github.com/gdamore/tcell/terminfo"
36	"github.com/mattn/go-isatty"
37	"github.com/spf13/pflag"
38)
39
40const version = "0.4 (2020-10-29)"
41
42// TODO: in case of error, show it in red (bg?), then below show again initial normal output (see also #4)
43// TODO: F1 should display help, and it should be multi-line, and scrolling licensing credits
44// TODO: some key shortcut to increase stdin capture buffer size (unless EOF already reached)
45// TODO: show status infos:
46//  - red fg + "up: process returned with error code %d" -- when subprocess returned an error
47//  - yellow fg -- when process is still not finished
48// TODO: on github: add issues, incl. up-for-grabs / help-wanted
49// TODO: [LATER] make it work on Windows; maybe with mattn/go-shellwords ?
50// TODO: [LATER] Ctrl-O shows input via `less` or $PAGER
51// TODO: properly show all licenses of dependencies on --version
52// TODO: [LATER] on ^X (?), leave TUI and run the command through buffered input, then unpause rest of input
53// TODO: [LATER] allow adding more elements of pipeline (initially, just writing `foo | bar` should work)
54// TODO: [LATER] allow invocation with partial command, like: `up grep -i` (see also #11)
55// TODO: [LATER][MAYBE] allow reading upN.sh scripts (see also #11)
56// TODO: [MUCH LATER] readline-like rich editing support? and completion? (see also #28)
57// TODO: [MUCH LATER] integration with fzf? and pindexis/marker?
58// TODO: [LATER] forking and unforking pipelines (see also #4)
59// TODO: [LATER] capture output of a running process (see: https://stackoverflow.com/q/19584825/98528)
60// TODO: [LATER] richer TUI:
61// - show # of read lines & kbytes
62// - show status (errorlevel) of process, or that it's still running (also with background colors)
63// - allow copying and pasting to/from command line
64// TODO: [LATER] allow connecting external editor (become server/engine via e.g. socket)
65// TODO: [LATER] become pluggable into http://luna-lang.org
66// TODO: [LATER][MAYBE] allow "plugins" ("combos" - commands with default options) e.g. for Lua `lua -e`+auto-quote, etc.
67// TODO: [LATER] make it more friendly to infrequent Linux users by providing "descriptive" commands like "search" etc.
68// TODO: [LATER] advertise on some reddits for data exploration / data science
69// TODO: [LATER] undo/redo - history of commands (see also #4)
70// TODO: [LATER] jump between buffers saved from earlier pipe fragments; OR: allow saving/recalling "snapshots" of (cmd, results) pairs (see also #4)
71// TODO: [LATER] ^-, U -- to switch to "unsafe mode"? -u to switch back? + some visual marker
72
73func init() {
74	pflag.Usage = func() {
75		fmt.Fprint(os.Stderr, `Usage: COMMAND | up [OPTIONS]
76
77up is the Ultimate Plumber, a tool for writing Linux pipes in a terminal-based
78UI interactively, with instant live preview of command results.
79
80To start using up, redirect any text-emitting command (or pipeline) into it -
81for example:
82
83    $ lshw |& ./up
84
85Ultimate Plumber then opens a full-screen terminal app. The top line of the
86screen can be edited in order to interactively build a pipeline. Every time you
87hit [Enter], the bottom of the screen will display the results of passing the
88up's standard input through the pipeline (executed using your default $SHELL).
89
90If a tilde '~' is visible in top-left corner, it indicates that Ultimate
91Plumber did not yet fully consume its input. Some pipelines may not finish with
92incomplete input; use Ctrl-S to freeze reading the input and to inject fake
93EOF; use Ctrl-Q to unfreeze back and continue reading.
94
95If a plus '+' is visible in top-left corner, the internal buffer limit
96(default: 40MB) was reached and Ultimate Plumber won't read more input.
97
98KEYS
99
100- alphanumeric & symbol keys, Left, Right, Ctrl-A/E/B/F/K/Y
101                      - navigate and edit the pipeline command
102- Enter   - execute the pipeline command, updating the pipeline output panel
103- Up, Dn, PgUp, PgDn, Ctrl-Left, Ctrl-Right
104                      - navigate (scroll) the pipeline output panel
105- Ctrl-X  - exit and write the pipeline to up1.sh (or if it exists then to
106            up2.sh, etc. till up1000.sh)
107- Ctrl-C  - quit without saving and emit the pipeline on standard output
108- Ctrl-S  - temporarily freeze a long-running input to Ultimate Plumber,
109            injecting a fake EOF into the buffer (shows '#' indicator in
110            top-left corner)
111- Ctrl-Q  - unfreeze back after Ctrl-S (disables '#' indicator)
112
113OPTIONS
114`)
115		pflag.PrintDefaults()
116		fmt.Fprint(os.Stderr, `
117HOMEPAGE: https://github.com/akavel/up
118VERSION: `+version+`
119`)
120	}
121	pflag.ErrHelp = errors.New("") // TODO: or something else?
122}
123
124var (
125	// TODO: dangerous? immediate? raw? unsafe? ...
126	// FIXME(akavel): mark the unsafe mode vs. safe mode with some colour or status; also inform/mark what command's results are displayed...
127	unsafeMode   = pflag.Bool("unsafe-full-throttle", false, "enable mode in which pipeline is executed immediately after any change (without pressing Enter)")
128	outputScript = pflag.StringP("output-script", "o", "", "save the command to specified `file` if Ctrl-X is pressed (default: up<N>.sh)")
129	debugMode    = pflag.Bool("debug", false, "debug mode")
130	noColors     = pflag.Bool("no-colors", false, "disable interface colors")
131	shellFlag    = pflag.StringArrayP("exec", "e", nil, "`command` to run pipeline with; repeat multiple times to pass multi-word command; defaults to '-e=$SHELL -e=-c'")
132	initialCmd   = pflag.StringP("pipeline", "c", "", "initial `commands` to use as pipeline (default empty)")
133	bufsize      = pflag.Int("buf", 40, "input buffer size & pipeline buffer sizes in `megabytes` (MiB)")
134	noinput      = pflag.Bool("noinput", false, "start with empty buffer regardless if any input was provided")
135)
136
137func main() {
138	// Handle command-line flags
139	pflag.Parse()
140
141	log.SetOutput(ioutil.Discard)
142	if *debugMode {
143		debug, err := os.Create("up.debug")
144		if err != nil {
145			die(err.Error())
146		}
147		log.SetOutput(debug)
148	}
149
150	// Find out what is the user's preferred login shell. This also allows user
151	// to choose the "engine" used for command execution.
152	shell := *shellFlag
153	if len(shell) == 0 {
154		log.Println("checking $SHELL...")
155		sh := os.Getenv("SHELL")
156		if sh != "" {
157			goto shell_found
158		}
159		log.Println("checking bash...")
160		sh, _ = exec.LookPath("bash")
161		if sh != "" {
162			goto shell_found
163		}
164		log.Println("checking sh...")
165		sh, _ = exec.LookPath("sh")
166		if sh != "" {
167			goto shell_found
168		}
169		die("cannot find shell: no -e flag, $SHELL is empty, neither bash nor sh are in $PATH")
170	shell_found:
171		shell = []string{sh, "-c"}
172	}
173	log.Println("found shell:", shell)
174
175	stdin := io.Reader(os.Stdin)
176	if *noinput {
177		stdin = bytes.NewReader(nil)
178	} else if isatty.IsTerminal(os.Stdin.Fd()) {
179		// TODO: Without this block, we'd hang when nothing is piped on input (see
180		// github.com/peco/peco, mattn/gof, fzf, etc.)
181		die("up requires some data piped on standard input, for example try: `echo hello world | up`")
182	}
183
184	// Initialize TUI infrastructure
185	tui := initTUI()
186	defer tui.Fini()
187
188	// Initialize 3 main UI parts
189	var (
190		// The top line of the TUI is an editable command, which will be used
191		// as a pipeline for data we read from stdin
192		commandEditor = NewEditor("| ", *initialCmd)
193		// The rest of the screen is a view of the results of the command
194		commandOutput = BufView{}
195		// Sometimes, a message may be displayed at the bottom of the screen, with help or other info
196		message = `Enter runs  ^X exit (^C nosave)  PgUp/PgDn/Up/Dn/^</^> scroll  ^S pause (^Q end)  [Ultimate Plumber v` + version + ` by akavel et al.]`
197	)
198
199	// Initialize main data flow
200	var (
201		// We capture data piped to 'up' on standard input into an internal buffer
202		// When some new data shows up on stdin, we raise a custom signal,
203		// so that main loop will refresh the buffers and the output.
204		stdinCapture = NewBuf(*bufsize*1024*1024).
205				StartCapturing(stdin, func() { triggerRefresh(tui) })
206		// Then, we pass this data as input to a subprocess.
207		// Initially, no subprocess is running, as no command is entered yet
208		commandSubprocess *Subprocess = nil
209	)
210	// Intially, for user's convenience, show the raw input data, as if `cat` command was typed
211	commandOutput.Buf = stdinCapture
212
213	// Main loop
214	lastCommand := ""
215	restart := false
216	for {
217		// If user edited the command, immediately run it in background, and
218		// kill the previously running command.
219		command := commandEditor.String()
220		if restart || (*unsafeMode && command != lastCommand) {
221			commandSubprocess.Kill()
222			if command != "" {
223				commandSubprocess = StartSubprocess(shell, command, stdinCapture, func() { triggerRefresh(tui) })
224				commandOutput.Buf = commandSubprocess.Buf
225			} else {
226				// If command is empty, show original input data again (~ equivalent of typing `cat`)
227				commandSubprocess = nil
228				commandOutput.Buf = stdinCapture
229			}
230			restart = false
231			lastCommand = command
232		}
233
234		// Draw UI
235		w, h := tui.Size()
236		style := whiteOnBlue
237		if command == lastCommand {
238			style = whiteOnDBlue
239		}
240		stdinCapture.DrawStatus(TuiRegion(tui, 0, 0, 1, 1), style)
241		commandEditor.DrawTo(TuiRegion(tui, 1, 0, w-1, 1), style,
242			func(x, y int) { tui.ShowCursor(x+1, 0) })
243		commandOutput.DrawTo(TuiRegion(tui, 0, 1, w, h-1))
244		drawText(TuiRegion(tui, 0, h-1, w, 1), whiteOnBlue, message)
245		tui.Show()
246
247		// Handle UI events
248		switch ev := tui.PollEvent().(type) {
249		// Key pressed
250		case *tcell.EventKey:
251			// Is it a command editor key?
252			if commandEditor.HandleKey(ev) {
253				message = ""
254				continue
255			}
256			// Is it a command output view key?
257			if commandOutput.HandleKey(ev, h-1) {
258				message = ""
259				continue
260			}
261			// Some other global key combinations
262			switch getKey(ev) {
263			case key(tcell.KeyEnter):
264				restart = true
265			case key(tcell.KeyCtrlUnderscore),
266				ctrlKey(tcell.KeyCtrlUnderscore):
267				// TODO: ask for another character to trigger command-line option, like in `less`
268
269			case key(tcell.KeyCtrlS),
270				ctrlKey(tcell.KeyCtrlS):
271				stdinCapture.Pause(true)
272				triggerRefresh(tui)
273			case key(tcell.KeyCtrlQ),
274				ctrlKey(tcell.KeyCtrlQ):
275				stdinCapture.Pause(false)
276				restart = true
277			case key(tcell.KeyCtrlC),
278				ctrlKey(tcell.KeyCtrlC),
279				key(tcell.KeyCtrlD),
280				ctrlKey(tcell.KeyCtrlD):
281				// Quit
282				tui.Fini()
283				os.Stderr.WriteString("up: Ultimate Plumber v" + version + " https://github.com/akavel/up\n")
284				os.Stderr.WriteString("up: | " + commandEditor.String() + "\n")
285				return
286			case key(tcell.KeyCtrlX),
287				ctrlKey(tcell.KeyCtrlX):
288				// Write script 'upN.sh' and quit
289				tui.Fini()
290				writeScript(shell, commandEditor.String(), tui)
291				return
292			}
293		}
294	}
295}
296
297func initTUI() tcell.Screen {
298	// TODO: maybe try gocui or termbox?
299	tui, err := tcell.NewScreen()
300	if err == terminfo.ErrTermNotFound {
301		term := os.Getenv("TERM")
302		hash := sha1.Sum([]byte(term))
303		// TODO: add a flag which would attempt to perform the download automatically if explicitly requested by user
304		die(fmt.Sprintf(`%[1]s
305Your terminal code:
306	TERM=%[2]s
307was not found in the database provided by tcell library. Please try checking if
308a supplemental database is found for your terminal at one of the following URLs:
309	https://github.com/gdamore/tcell/raw/master/terminfo/database/%.1[3]x/%.4[3]x
310	https://github.com/gdamore/tcell/raw/master/terminfo/database/%.1[3]x/%.4[3]x.gz
311If yes, download it and save in the following directory:
312	$HOME/.tcelldb/%.1[3]x/
313then try running "up" again. If that does not work for you, please first consult:
314	https://github.com/akavel/up/issues/15
315and if you don't see your terminal code mentioned there, please try asking on:
316	https://github.com/gdamore/tcell/issues
317Or, you might try changing TERM temporarily to some other value, for example by
318running "up" with:
319	TERM=xterm up
320Good luck!`,
321			err, term, hash))
322	}
323	if err != nil {
324		die(err.Error())
325	}
326	err = tui.Init()
327	if err != nil {
328		die(err.Error())
329	}
330	return tui
331}
332
333func triggerRefresh(tui tcell.Screen) {
334	tui.PostEvent(tcell.NewEventInterrupt(nil))
335}
336
337func die(message string) {
338	os.Stderr.WriteString("error: " + message + "\n")
339	os.Exit(1)
340}
341
342func NewEditor(prompt, value string) *Editor {
343	v := []rune(value)
344	return &Editor{
345		prompt: []rune(prompt),
346		value:  v,
347		cursor: len(v),
348		lastw:  len(v),
349	}
350}
351
352type Editor struct {
353	// TODO: make editor multiline. Reuse gocui or something for this?
354	prompt    []rune
355	value     []rune
356	killspace []rune
357	cursor    int
358	// lastw is length of value on last Draw; we need it to know how much to erase after backspace
359	lastw int
360}
361
362func (e *Editor) String() string { return string(e.value) }
363
364func (e *Editor) DrawTo(region Region, style tcell.Style, setcursor func(x, y int)) {
365	// Draw prompt & the edited value - use white letters on blue background
366	for i, ch := range e.prompt {
367		region.SetCell(i, 0, style, ch)
368	}
369	for i, ch := range e.value {
370		region.SetCell(len(e.prompt)+i, 0, style, ch)
371	}
372
373	// Clear remains of last value if needed
374	for i := len(e.value); i < e.lastw; i++ {
375		region.SetCell(len(e.prompt)+i, 0, tcell.StyleDefault, ' ')
376	}
377	e.lastw = len(e.value)
378
379	// Show cursor if requested
380	if setcursor != nil {
381		setcursor(len(e.prompt)+e.cursor, 0)
382	}
383}
384
385func (e *Editor) HandleKey(ev *tcell.EventKey) bool {
386	// If a character is entered, with no modifiers except maybe shift, then just insert it
387	if ev.Key() == tcell.KeyRune && ev.Modifiers()&(^tcell.ModShift) == 0 {
388		e.insert(ev.Rune())
389		return true
390	}
391	// Handle editing & movement keys
392	switch getKey(ev) {
393	case key(tcell.KeyBackspace), key(tcell.KeyBackspace2):
394		// See https://github.com/nsf/termbox-go/issues/145
395		e.delete(-1)
396	case key(tcell.KeyDelete):
397		e.delete(0)
398	case key(tcell.KeyLeft),
399		key(tcell.KeyCtrlB),
400		ctrlKey(tcell.KeyCtrlB):
401		if e.cursor > 0 {
402			e.cursor--
403		}
404	case key(tcell.KeyRight),
405		key(tcell.KeyCtrlF),
406		ctrlKey(tcell.KeyCtrlF):
407		if e.cursor < len(e.value) {
408			e.cursor++
409		}
410	case key(tcell.KeyCtrlA),
411		ctrlKey(tcell.KeyCtrlA):
412		e.cursor = 0
413	case key(tcell.KeyCtrlE),
414		ctrlKey(tcell.KeyCtrlE):
415		e.cursor = len(e.value)
416	case key(tcell.KeyCtrlK),
417		ctrlKey(tcell.KeyCtrlK):
418		e.kill()
419	case key(tcell.KeyCtrlY),
420		ctrlKey(tcell.KeyCtrlY):
421		e.insert(e.killspace...)
422	default:
423		// Unknown key/combination, not handled
424		return false
425	}
426	return true
427}
428
429func (e *Editor) insert(ch ...rune) {
430	// Based on https://github.com/golang/go/wiki/SliceTricks#insert
431	e.value = append(e.value, ch...)                     // = PREFIX + SUFFIX + (filler)
432	copy(e.value[e.cursor+len(ch):], e.value[e.cursor:]) // = PREFIX + (filler) + SUFFIX
433	copy(e.value[e.cursor:], ch)                         // = PREFIX + ch + SUFFIX
434	e.cursor += len(ch)
435}
436
437func (e *Editor) delete(dx int) {
438	pos := e.cursor + dx
439	if pos < 0 || pos >= len(e.value) {
440		return
441	}
442	e.value = append(e.value[:pos], e.value[pos+1:]...)
443	e.cursor = pos
444}
445
446func (e *Editor) kill() {
447	if e.cursor != len(e.value) {
448		e.killspace = append(e.killspace[:0], e.value[e.cursor:]...)
449	}
450	e.value = e.value[:e.cursor]
451}
452
453type BufView struct {
454	// TODO: Wrap bool
455	Y   int // Y of the view in the Buf, for down/up scrolling
456	X   int // X of the view in the Buf, for left/right scrolling
457	Buf *Buf
458}
459
460func (v *BufView) DrawTo(region Region) {
461	r := bufio.NewReader(v.Buf.NewReader(false))
462
463	// PgDn/PgUp etc. support
464	for y := v.Y; y > 0; y-- {
465		line, err := r.ReadBytes('\n')
466		switch err {
467		case nil:
468			// skip line
469			continue
470		case io.EOF:
471			r = bufio.NewReader(bytes.NewReader(line))
472			y = 0
473			break
474		default:
475			panic(err)
476		}
477	}
478
479	lclip := false
480	drawch := func(x, y int, ch rune) {
481		if x <= v.X && v.X != 0 {
482			x, ch = 0, '«'
483			lclip = true
484		} else {
485			x -= v.X
486		}
487		if x >= region.W {
488			x, ch = region.W-1, '»'
489		}
490		region.SetCell(x, y, tcell.StyleDefault, ch)
491	}
492	endline := func(x, y int) {
493		x -= v.X
494		if x < 0 {
495			x = 0
496		}
497		if x == 0 && lclip {
498			x++
499		}
500		lclip = false
501		for ; x < region.W; x++ {
502			region.SetCell(x, y, tcell.StyleDefault, ' ')
503		}
504	}
505
506	x, y := 0, 0
507	// TODO: handle runes properly, including their visual width (mattn/go-runewidth)
508	for {
509		ch, _, err := r.ReadRune()
510		if y >= region.H || err == io.EOF {
511			break
512		} else if err != nil {
513			panic(err)
514		}
515		switch ch {
516		case '\n':
517			endline(x, y)
518			x, y = 0, y+1
519			continue
520		case '\t':
521			const tabwidth = 8
522			drawch(x, y, ' ')
523			for x%tabwidth < (tabwidth - 1) {
524				x++
525				if x >= region.W {
526					break
527				}
528				drawch(x, y, ' ')
529			}
530		default:
531			drawch(x, y, ch)
532		}
533		x++
534	}
535	for ; y < region.H; y++ {
536		endline(x, y)
537		x = 0
538	}
539}
540
541func (v *BufView) HandleKey(ev *tcell.EventKey, scrollY int) bool {
542	const scrollX = 8 // When user scrolls horizontally, move by this many characters
543	switch getKey(ev) {
544	//
545	// Vertical scrolling
546	//
547	case key(tcell.KeyUp):
548		v.Y--
549		v.normalizeY()
550	case key(tcell.KeyDown):
551		v.Y++
552		v.normalizeY()
553	case key(tcell.KeyPgDn):
554		// TODO: in top-right corner of Buf area, draw current line number & total # of lines
555		v.Y += scrollY
556		v.normalizeY()
557	case key(tcell.KeyPgUp):
558		v.Y -= scrollY
559		v.normalizeY()
560	//
561	// Horizontal scrolling
562	//
563	case altKey(tcell.KeyLeft),
564		ctrlKey(tcell.KeyLeft):
565		v.X -= scrollX
566		if v.X < 0 {
567			v.X = 0
568		}
569	case altKey(tcell.KeyRight),
570		ctrlKey(tcell.KeyRight):
571		v.X += scrollX
572	case altKey(tcell.KeyHome),
573		ctrlKey(tcell.KeyHome):
574		v.X = 0
575	default:
576		// Unknown key/combination, not handled
577		return false
578	}
579	return true
580}
581
582func (v *BufView) normalizeY() {
583	nlines := count(v.Buf.NewReader(false), '\n') + 1
584	if v.Y >= nlines {
585		v.Y = nlines - 1
586	}
587	if v.Y < 0 {
588		v.Y = 0
589	}
590}
591
592func count(r io.Reader, b byte) (n int) {
593	buf := [256]byte{}
594	for {
595		i, err := r.Read(buf[:])
596		n += bytes.Count(buf[:i], []byte{b})
597		if err != nil {
598			return
599		}
600	}
601}
602
603func NewBuf(bufsize int) *Buf {
604	// TODO: make buffer size dynamic (growable by pressing a key)
605	buf := &Buf{bytes: make([]byte, bufsize)}
606	buf.cond = sync.NewCond(&buf.mu)
607	return buf
608}
609
610type Buf struct {
611	bytes []byte
612
613	mu     sync.Mutex // guards the following fields
614	cond   *sync.Cond
615	status bufStatus
616	n      int
617}
618
619type bufStatus int
620
621const (
622	bufReading bufStatus = iota
623	bufEOF
624	bufPaused
625)
626
627func (b *Buf) StartCapturing(r io.Reader, notify func()) *Buf {
628	go b.capture(r, notify)
629	return b
630}
631
632func (b *Buf) capture(r io.Reader, notify func()) {
633	// TODO: allow stopping - take context?
634	for {
635		n, err := r.Read(b.bytes[b.n:])
636
637		b.mu.Lock()
638		for b.status == bufPaused {
639			b.cond.Wait()
640		}
641		b.n += n
642		if err == io.EOF {
643			b.status = bufEOF
644		}
645		if b.n == len(b.bytes) {
646			// TODO: remove this when we can grow the buffer
647			err = io.EOF
648		}
649		b.cond.Broadcast()
650		b.mu.Unlock()
651
652		go notify()
653		if err == io.EOF {
654			log.Printf("capture EOF after: %q", b.bytes[:b.n]) // TODO: make sure no race here, and skipped if not debugging
655			return
656		} else if err != nil {
657			// TODO: better handling of errors
658			panic(err)
659		}
660	}
661}
662
663func (b *Buf) Pause(pause bool) {
664	b.mu.Lock()
665	if pause {
666		if b.status == bufReading {
667			b.status = bufPaused
668			// trigger all readers to emit fake EOF
669			b.cond.Broadcast()
670		}
671	} else {
672		if b.status == bufPaused {
673			b.status = bufReading
674			// wake up the capture func
675			b.cond.Broadcast()
676		}
677	}
678	b.mu.Unlock()
679}
680
681func (b *Buf) DrawStatus(region Region, style tcell.Style) {
682	status := '~' // default: still reading input
683
684	b.mu.Lock()
685	switch {
686	case b.status == bufPaused:
687		status = '#'
688	case b.status == bufEOF:
689		status = ' ' // all input read, nothing more to do
690	case b.n == len(b.bytes):
691		status = '+' // buffer full
692	}
693	b.mu.Unlock()
694
695	region.SetCell(0, 0, style, status)
696}
697
698func (b *Buf) NewReader(blocking bool) io.Reader {
699	i := 0
700	return funcReader(func(p []byte) (n int, err error) {
701		b.mu.Lock()
702		end := b.n
703		for blocking && end == i && b.status == bufReading && end < len(b.bytes) {
704			b.cond.Wait()
705			end = b.n
706		}
707		b.mu.Unlock()
708
709		n = copy(p, b.bytes[i:end])
710		i += n
711		if n > 0 {
712			return n, nil
713		} else {
714			if blocking {
715				log.Printf("blocking reader emitting EOF after: %q", b.bytes[:end])
716			}
717			return 0, io.EOF
718		}
719	})
720}
721
722type funcReader func([]byte) (int, error)
723
724func (f funcReader) Read(p []byte) (int, error) { return f(p) }
725
726type Subprocess struct {
727	Buf    *Buf
728	cancel context.CancelFunc
729}
730
731func StartSubprocess(shell []string, command string, stdin *Buf, notify func()) *Subprocess {
732	ctx, cancel := context.WithCancel(context.TODO())
733	r, w := io.Pipe()
734	p := &Subprocess{
735		Buf:    NewBuf(len(stdin.bytes)).StartCapturing(r, notify),
736		cancel: cancel,
737	}
738
739	cmd := exec.CommandContext(ctx, shell[0], append(shell[1:], command)...)
740	cmd.Stdout = w
741	cmd.Stderr = w
742	cmd.Stdin = stdin.NewReader(true)
743	err := cmd.Start()
744	if err != nil {
745		fmt.Fprintf(w, "up: %s", err)
746		w.Close()
747		return p
748	}
749	log.Println(cmd.Path)
750	go func() {
751		err = cmd.Wait()
752		if err != nil {
753			fmt.Fprintf(w, "up: %s", err)
754			log.Printf("Wait returned error: %s", err)
755		}
756		w.Close()
757	}()
758	return p
759}
760
761func (s *Subprocess) Kill() {
762	if s == nil {
763		return
764	}
765	s.cancel()
766}
767
768type key int32
769
770func getKey(ev *tcell.EventKey) key { return key(ev.Modifiers())<<16 + key(ev.Key()) }
771func altKey(base tcell.Key) key     { return key(tcell.ModAlt)<<16 + key(base) }
772func ctrlKey(base tcell.Key) key    { return key(tcell.ModCtrl)<<16 + key(base) }
773
774func writeScript(shell []string, command string, tui tcell.Screen) {
775	os.Stderr.WriteString("up: Ultimate Plumber v" + version + " https://github.com/akavel/up\n")
776	var f *os.File
777	var err error
778	if *outputScript != "" {
779		os.Stderr.WriteString("up: writing " + *outputScript)
780		f, err = os.OpenFile(*outputScript, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
781		if err != nil {
782			goto fallback_tmp
783		}
784		goto try_file
785	}
786
787	os.Stderr.WriteString("up: writing: .")
788	for i := 1; i < 1000; i++ {
789		f, err = os.OpenFile(fmt.Sprintf("up%d.sh", i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)
790		switch {
791		case os.IsExist(err):
792			continue
793		case err != nil:
794			goto fallback_tmp
795		default:
796			os.Stderr.WriteString("/" + f.Name())
797			goto try_file
798		}
799	}
800	os.Stderr.WriteString(" - error: up1.sh-up999.sh already exist\n")
801	goto fallback_tmp
802
803try_file:
804	// NOTE: currently not supporting multi-word shell in upNNN.sh unfortunately :(
805	_, err = fmt.Fprintf(f, "#!%s\n%s\n", shell[0], command)
806	if err != nil {
807		goto fallback_tmp
808	}
809	err = f.Close()
810	if err != nil {
811		goto fallback_tmp
812	}
813	os.Stderr.WriteString(" - OK\n")
814	return
815
816fallback_tmp:
817	// TODO: test if the fallbacks etc. protections actually work
818	os.Stderr.WriteString(" - error: " + err.Error() + "\n")
819	f, err = ioutil.TempFile("", "up-*.sh")
820	if err != nil {
821		goto fallback_print
822	}
823	_, err = fmt.Fprintf(f, "#!%s\n%s\n", shell, command)
824	if err != nil {
825		goto fallback_print
826	}
827	err = f.Close()
828	if err != nil {
829		goto fallback_print
830	}
831	os.Stderr.WriteString("up: writing: " + f.Name() + " - OK\n")
832	os.Chmod(f.Name(), 0755)
833	return
834
835fallback_print:
836	fname := "TMP"
837	if f != nil {
838		fname = f.Name()
839	}
840	os.Stderr.WriteString("up: writing: " + fname + " - error: " + err.Error() + "\n")
841	os.Stderr.WriteString("up: | " + command + "\n")
842}
843
844type Region struct {
845	W, H    int
846	SetCell func(x, y int, style tcell.Style, ch rune)
847}
848
849func TuiRegion(tui tcell.Screen, x, y, w, h int) Region {
850	return Region{
851		W: w, H: h,
852		SetCell: func(dx, dy int, style tcell.Style, ch rune) {
853			if dx >= 0 && dx < w && dy >= 0 && dy < h {
854				if *noColors {
855					style = tcell.StyleDefault
856				}
857				tui.SetCell(x+dx, y+dy, style, ch)
858			}
859		},
860	}
861}
862
863var (
864	whiteOnBlue  = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlue)
865	whiteOnDBlue = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorNavy)
866)
867
868func drawText(region Region, style tcell.Style, text string) {
869	for x, ch := range text {
870		region.SetCell(x, 0, style, ch)
871	}
872}
873