diff options
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | up.go | 125 |
2 files changed, 117 insertions, 30 deletions
@@ -38,6 +38,16 @@ then: Wireless interface Centrino Advanced-N 6235 Ethernet interface RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller + - **WARNING: Please be careful when using it! It could be dangerous.** + In particular, writing "rm" or "dd" into it could be like running around + with a chainsaw. But you'd be careful writing "rm" anywhere in Linux + anyway, no? Also, why would you want to pipe something into "rm"? Other + than that, I don't really have good ideas how to protect against cases + like this. And in the other, non-dangerous cases, I find the tool + immensely useful. If you have some ideas how to + try to protect, [please share!](https://github.com/akavel/up/issues) + That said, a tool wouldn't be really Unixy if you couldn't hurt yourself + with it, right? ;P - when you are satisfied with the result, you can **press *Ctrl-X* to exit** the Ultimate Plumber, and the command you built will be **written into `up1.sh` file** in the current working directory (or, if it already existed, @@ -61,6 +71,18 @@ then: you reach this limit, a `+` character should get displayed in the top-left corner of the screen. (This is intended to be changed to a dynamically/manually growable buffer in a future version of *up*.) +- **MacOSX support:** I don't have a Mac, thus I have no idea if it works on + one. You are welcome to try, and also to send PRs. If you're interested in + me providing some kind of official-like support for MacOSX, please consider + trying to find a way to send me some usable-enough Mac computer. Please note + I'm not trying to "take advantage" of you by this, as I'm actually not at all + interested in achieving a Mac otherwise. (Also, trying to commit to this kind + of support will be an extra burden and obligation on me. Knowing someone out + there cares enough to do a fancy physical gesture would really help alleviate + this.) If you're serious enough to consider this option, please contact me by + email (mailto:czapkofan@gmail.com) or keybase (https://keybase.io/akavel), so + that we could try to research possible ways to achieve this. + Thanks for understanding! - **Prior art:** I was surprised no one seemed to write a similar tool before, that I could find. It should have been possible to write this since the dawn of Unix already, or earlier! And indeed, after I announced *up*, I got enough @@ -34,11 +34,14 @@ import ( "github.com/spf13/pflag" ) +const version = "0.2 (2018-10-25)" + +// TODO: [#4] in case of error, show it in red (bg?), then below show again initial normal output +// TODO: F1 should display help, and it should be multi-line, and scrolling licensing credits // TODO: some key shortcut to increase stdin capture buffer size (unless EOF already reached) // TODO: show status infos: // - red fg + "up: process returned with error code %d" -- when subprocess returned an error // - yellow fg -- when process is still not finished -// TODO: readme, asciinema // TODO: on github: add issues, incl. up-for-grabs / help-wanted // TODO: [LATER] make it work on Windows; maybe with mattn/go-shellwords ? // TODO: [LATER] Ctrl-O shows input via `less` or $PAGER @@ -47,7 +50,6 @@ import ( // TODO: [LATER] allow adding more elements of pipeline (initially, just writing `foo | bar` should work) // TODO: [LATER] allow invocation with partial command, like: `up grep -i` // TODO: [LATER][MAYBE] allow reading upN.sh scripts -// TODO: [LATER] auto-save and/or save on Ctrl-S or something // TODO: [MUCH LATER] readline-like rich editing support? and completion? // TODO: [MUCH LATER] integration with fzf? and pindexis/marker? // TODO: [LATER] forking and unforking pipelines @@ -60,10 +62,19 @@ import ( // TODO: [LATER] become pluggable into http://luna-lang.org // TODO: [LATER][MAYBE] allow "plugins" ("combos" - commands with default options) e.g. for Lua `lua -e`+auto-quote, etc. // TODO: [LATER] make it more friendly to infrequent Linux users by providing "descriptive" commands like "search" etc. -// TODO: [LATER] advertise on: HN, r/programming, r/golang, r/commandline, r/linux, up-for-grabs.net; data exploration? data science? +// TODO: [LATER] advertise on some reddits for data exploration / data science +// TODO: [LATER] undo/redo - history of commands +// TODO: [LATER] if writing upN.sh failed, write to $TMP/up.$RANDOM.sh (a.k.a. tempfile()) +// TODO: [LATER] jump between buffers saved from earlier pipe fragments; OR: allow saving/recalling "snapshots" of (cmd, results) pairs +// TODO: [LATER] on ^C, print version & commandline on stderr +// TODO: [LATER] ^-, U -- to switch to "unsafe mode"? -u to switch back? + some visual marker var ( - debugMode = pflag.Bool("debug", false, "debug mode") + // TODO: dangerous? immediate? raw? unsafe? ... + // FIXME(akavel): mark the unsafe mode vs. safe mode with some colour or status; also inform/mark what command's results are displayed... + unsafeMode = pflag.Bool("unsafe-full-throttle", false, "enable mode in which command is executed immediately after any change") + outputScript = pflag.StringP("output-script", "o", "", "save the command to specified `file` if Ctrl-X is pressed (default: up<N>.sh)") + debugMode = pflag.Bool("debug", false, "debug mode") ) func main() { @@ -91,7 +102,7 @@ func main() { // The rest of the screen is a view of the results of the command commandOutput = BufView{} // Sometimes, a message may be displayed at the bottom of the screen, with help or other info - message = `^X exit (^C nosave) PgUp/PgDn/Up/Dn/^</^> scroll ^S pause (^Q end) [Ultimate Plumber v0.1 by akavel]` + message = `Enter runs ^X exit (^C nosave) PgUp/PgDn/Up/Dn/^</^> scroll ^S pause (^Q end) [Ultimate Plumber v` + version + ` by akavel]` ) // Initialize main data flow @@ -109,11 +120,12 @@ func main() { // Main loop lastCommand := "" + restart := false for { // If user edited the command, immediately run it in background, and // kill the previously running command. command := commandEditor.String() - if command != lastCommand { + if restart || (*unsafeMode && command != lastCommand) { commandSubprocess.Kill() if command != "" { commandSubprocess = StartSubprocess(command, stdinCapture, func() { triggerRefresh(tui) }) @@ -123,13 +135,18 @@ func main() { commandSubprocess = nil commandOutput.Buf = stdinCapture } + restart = false + lastCommand = command } - lastCommand = command // Draw UI w, h := tui.Size() - stdinCapture.DrawStatus(TuiRegion(tui, 0, 0, 1, 1)) - commandEditor.DrawTo(TuiRegion(tui, 1, 0, w-1, 1), + style := whiteOnBlue + if command == lastCommand { + style = whiteOnDBlue + } + stdinCapture.DrawStatus(TuiRegion(tui, 0, 0, 1, 1), style) + commandEditor.DrawTo(TuiRegion(tui, 1, 0, w-1, 1), style, func(x, y int) { tui.ShowCursor(x+1, 0) }) commandOutput.DrawTo(TuiRegion(tui, 0, 1, w, h-1)) drawText(TuiRegion(tui, 0, h-1, w, 1), whiteOnBlue, message) @@ -151,6 +168,12 @@ func main() { } // Some other global key combinations switch getKey(ev) { + case key(tcell.KeyEnter): + restart = true + case key(tcell.KeyCtrlUnderscore), + ctrlKey(tcell.KeyCtrlUnderscore): + // TODO: ask for another character to trigger command-line option, like in `less` + case key(tcell.KeyCtrlS), ctrlKey(tcell.KeyCtrlS): stdinCapture.Pause(true) @@ -164,11 +187,14 @@ func main() { key(tcell.KeyCtrlD), ctrlKey(tcell.KeyCtrlD): // Quit - // TODO: print the command in case user did this accidentally + tui.Fini() + os.Stderr.WriteString("up: Ultimate Plumber v" + version + " https://github.com/akavel/up\n") + os.Stderr.WriteString("up: | " + commandEditor.String() + "\n") return case key(tcell.KeyCtrlX), ctrlKey(tcell.KeyCtrlX): // Write script 'upN.sh' and quit + tui.Fini() writeScript(commandEditor.String(), tui) return } @@ -220,9 +246,8 @@ type Editor struct { func (e *Editor) String() string { return string(e.value) } -func (e *Editor) DrawTo(region Region, setcursor func(x, y int)) { +func (e *Editor) DrawTo(region Region, style tcell.Style, setcursor func(x, y int)) { // Draw prompt & the edited value - use white letters on blue background - style := whiteOnBlue for i, ch := range e.prompt { region.SetCell(i, 0, style, ch) } @@ -516,7 +541,7 @@ func (b *Buf) Pause(pause bool) { b.mu.Unlock() } -func (b *Buf) DrawStatus(region Region) { +func (b *Buf) DrawStatus(region Region, style tcell.Style) { status := '~' // default: still reading input b.mu.Lock() @@ -530,7 +555,7 @@ func (b *Buf) DrawStatus(region Region) { } b.mu.Unlock() - region.SetCell(0, 0, whiteOnBlue, status) + region.SetCell(0, 0, style, status) } func (b *Buf) NewReader(blocking bool) io.Reader { @@ -610,33 +635,72 @@ func altKey(base tcell.Key) key { return key(tcell.ModAlt)<<16 + key(base) } func ctrlKey(base tcell.Key) key { return key(tcell.ModCtrl)<<16 + key(base) } func writeScript(command string, tui tcell.Screen) { + os.Stderr.WriteString("up: Ultimate Plumber v" + version + " https://github.com/akavel/up\n") var f *os.File var err error - // TODO: if we hit loop end, panic with some message + if *outputScript != "" { + os.Stderr.WriteString("up: writing " + *outputScript) + f, err = os.OpenFile(*outputScript, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + goto fallback_tmp + } + goto try_file + } + + os.Stderr.WriteString("up: writing: .") for i := 1; i < 1000; i++ { f, err = os.OpenFile(fmt.Sprintf("up%d.sh", i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755) - if err != nil { - if os.IsExist(err) { - continue - } - // FIXME: don't panic, instead show error and let user try to copy & paste visually - panic(err) - } else { - break + switch { + case os.IsExist(err): + continue + case err != nil: + goto fallback_tmp + default: + os.Stderr.WriteString("/" + f.Name()) + goto try_file } } + os.Stderr.WriteString(" - error: up1.sh-up999.sh already exist\n") + goto fallback_tmp + +try_file: _, err = fmt.Fprintf(f, "#!/bin/bash\n%s\n", command) if err != nil { - // FIXME: don't panic, instead show error and let user try to copy & paste visually - panic(err) + goto fallback_tmp } err = f.Close() if err != nil { - // FIXME: don't panic, instead show error and let user try to copy & paste visually - panic(err) + goto fallback_tmp + } + os.Stderr.WriteString(" - OK\n") + return + +fallback_tmp: + // TODO: test if the fallbacks etc. protections actually work + os.Stderr.WriteString(" - error: " + err.Error() + "\n") + f, err = ioutil.TempFile("", "up-*.sh") + if err != nil { + goto fallback_print + } + _, err = fmt.Fprintf(f, "#!/bin/bash\n%s\n", command) + if err != nil { + goto fallback_print + } + err = f.Close() + if err != nil { + goto fallback_print + } + os.Stderr.WriteString("up: writing: " + f.Name() + " - OK\n") + os.Chmod(f.Name(), 0755) + return + +fallback_print: + fname := "TMP" + if f != nil { + fname = f.Name() } - tui.Fini() - fmt.Printf("up: command written to: %s\n", f.Name()) + os.Stderr.WriteString("up: writing: " + fname + " - error: " + err.Error() + "\n") + os.Stderr.WriteString("up: | " + command + "\n") } type Region struct { @@ -656,7 +720,8 @@ func TuiRegion(tui tcell.Screen, x, y, w, h int) Region { } var ( - whiteOnBlue = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlue) + whiteOnBlue = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlue) + whiteOnDBlue = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorNavy) ) func drawText(region Region, style tcell.Style, text string) { |