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