From 9094c174ccdbe7eafa444fad93e80adb075d197e Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Thu, 4 Jan 2018 17:03:08 -0500 Subject: [PATCH] Initial support for terminal within micro This commit adds beta support for running a shell or other program within a micro view. Use the `> term` command. With no arguments, `term` will open your shell in interactive mode. You can also run an arbitrary command with `> term cmd` and the command with be executed and output shown. One issue at the moment is the terminal window will close immediately after the process dies. No mouse events are sent to programs running within micro. Ref #243 --- cmd/micro/buffer.go | 11 ++++ cmd/micro/colorscheme.go | 10 ++-- cmd/micro/command.go | 20 ++++++++ cmd/micro/lineArray.go | 10 +++- cmd/micro/messenger.go | 21 ++++++++ cmd/micro/micro.go | 14 +++-- cmd/micro/scrollbar.go | 6 ++- cmd/micro/statusline.go | 6 +++ cmd/micro/tab.go | 3 ++ cmd/micro/view.go | 108 ++++++++++++++++++++++++++++++++++++++- 10 files changed, 198 insertions(+), 11 deletions(-) diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index 409647d8..c791deaa 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -52,6 +52,7 @@ type Buffer struct { // Stores the last modification time of the file the buffer is pointing to ModTime time.Time + // NumLines is the number of lines in the buffer NumLines int syntaxDef *highlight.Def @@ -72,6 +73,8 @@ type SerializedBuffer struct { ModTime time.Time } +// NewBufferFromString creates a new buffer containing the given +// string func NewBufferFromString(text, path string) *Buffer { return NewBuffer(strings.NewReader(text), int64(len(text)), path) } @@ -201,6 +204,8 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer { return b } +// GetName returns the name that should be displayed in the statusline +// for this buffer func (b *Buffer) GetName() string { if b.name == "" { if b.Path == "" { @@ -333,6 +338,8 @@ func (b *Buffer) Update() { b.NumLines = len(b.lines) } +// MergeCursors merges any cursors that are at the same position +// into one cursor func (b *Buffer) MergeCursors() { var cursors []*Cursor for i := 0; i < len(b.cursors); i++ { @@ -359,6 +366,7 @@ func (b *Buffer) MergeCursors() { } } +// UpdateCursors updates all the cursors indicies func (b *Buffer) UpdateCursors() { for i, c := range b.cursors { c.Num = i @@ -488,6 +496,8 @@ func (b *Buffer) SaveAsWithSudo(filename string) error { return err } +// Modified returns if this buffer has been modified since +// being opened func (b *Buffer) Modified() bool { if b.Settings["fastdirty"].(bool) { return b.IsModified @@ -539,6 +549,7 @@ func (b *Buffer) Line(n int) string { return string(b.lines[n].data) } +// LinesNum returns the number of lines in the buffer func (b *Buffer) LinesNum() int { return len(b.lines) } diff --git a/cmd/micro/colorscheme.go b/cmd/micro/colorscheme.go index bae9a705..8a649b17 100644 --- a/cmd/micro/colorscheme.go +++ b/cmd/micro/colorscheme.go @@ -54,7 +54,7 @@ func InitColorscheme() { Foreground(tcell.ColorDefault). Background(tcell.ColorDefault) if screen != nil { - screen.SetStyle(defStyle) + // screen.SetStyle(defStyle) } LoadDefaultColorscheme() @@ -109,7 +109,7 @@ func ParseColorscheme(text string) Colorscheme { defStyle = style } if screen != nil { - screen.SetStyle(defStyle) + // screen.SetStyle(defStyle) } } else { fmt.Println("Color-link statement is not valid:", line) @@ -252,5 +252,9 @@ func GetColor256(color int) tcell.Color { tcell.Color253, tcell.Color254, tcell.Color255, } - return colors[color] + if color >= 0 && color < len(colors) { + return colors[color] + } + + return tcell.ColorDefault } diff --git a/cmd/micro/command.go b/cmd/micro/command.go index c50ad0e9..958bc14b 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -56,6 +56,7 @@ func init() { "Pwd": Pwd, "Open": Open, "TabSwitch": TabSwitch, + "Term": Term, "MemUsage": MemUsage, "Retab": Retab, "Raw": Raw, @@ -114,12 +115,16 @@ func DefaultCommands() map[string]StrCommand { "pwd": {"Pwd", []Completion{NoCompletion}}, "open": {"Open", []Completion{FileCompletion}}, "tabswitch": {"TabSwitch", []Completion{NoCompletion}}, + "term": {"Term", []Completion{NoCompletion}}, "memusage": {"MemUsage", []Completion{NoCompletion}}, "retab": {"Retab", []Completion{NoCompletion}}, "raw": {"Raw", []Completion{NoCompletion}}, } } +// CommandEditAction returns a bindable function that opens a prompt with +// the given string and executes the command when the user presses +// enter func CommandEditAction(prompt string) func(*View, bool) bool { return func(v *View, usePlugin bool) bool { input, canceled := messenger.Prompt("> ", prompt, "Command", CommandCompletion) @@ -130,6 +135,8 @@ func CommandEditAction(prompt string) func(*View, bool) bool { } } +// CommandAction returns a bindable function which executes the +// given command func CommandAction(cmd string) func(*View, bool) bool { return func(v *View, usePlugin bool) bool { HandleCommand(cmd) @@ -699,6 +706,19 @@ func ReplaceAll(args []string) { Replace(append(args, "-a")) } +// Term opens a terminal in the current view +func Term(args []string) { + var err error + if len(args) == 0 { + err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}) + } else { + err = CurView().StartTerminal(args) + } + if err != nil { + messenger.Error(err) + } +} + // RunShellCommand executes a shell command and returns the output/error func RunShellCommand(input string) (string, error) { args, err := shellwords.Split(input) diff --git a/cmd/micro/lineArray.go b/cmd/micro/lineArray.go index 93004ac4..20fde585 100644 --- a/cmd/micro/lineArray.go +++ b/cmd/micro/lineArray.go @@ -29,6 +29,8 @@ func runeToByteIndex(n int, txt []byte) int { return count } +// A Line contains the data in bytes as well as a highlight state, match +// and a flag for whether the highlighting needs to be updated type Line struct { data []byte @@ -43,10 +45,12 @@ type LineArray struct { lines []Line } +// Append efficiently appends lines together +// It allocates an additional 10000 lines if the original estimate +// is incorrect func Append(slice []Line, data ...Line) []Line { l := len(slice) if l+len(data) > cap(slice) { // reallocate - // Allocate double what's needed, for future growth. newSlice := make([]Line, (l+len(data))+10000) // The copy function is predeclared and works for any slice type. copy(newSlice, slice) @@ -243,18 +247,22 @@ func (la *LineArray) Substr(start, end Loc) string { return str } +// State gets the highlight state for the given line number func (la *LineArray) State(lineN int) highlight.State { return la.lines[lineN].state } +// SetState sets the highlight state at the given line number func (la *LineArray) SetState(lineN int, s highlight.State) { la.lines[lineN].state = s } +// SetMatch sets the match at the given line number func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) { la.lines[lineN].match = m } +// Match retrieves the match for the given line number func (la *LineArray) Match(lineN int) highlight.LineMatch { return la.lines[lineN].match } diff --git a/cmd/micro/messenger.go b/cmd/micro/messenger.go index 646d8123..a62ca8a7 100644 --- a/cmd/micro/messenger.go +++ b/cmd/micro/messenger.go @@ -348,6 +348,7 @@ func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTy return response, canceled } +// DownHistory fetches the previous item in the history func (m *Messenger) UpHistory(history []string) { if m.historyNum > 0 { m.historyNum-- @@ -355,6 +356,8 @@ func (m *Messenger) UpHistory(history []string) { m.cursorx = Count(m.response) } } + +// DownHistory fetches the next item in the history func (m *Messenger) DownHistory(history []string) { if m.historyNum < len(history)-1 { m.historyNum++ @@ -362,33 +365,47 @@ func (m *Messenger) DownHistory(history []string) { m.cursorx = Count(m.response) } } + +// CursorLeft moves the cursor one character left func (m *Messenger) CursorLeft() { if m.cursorx > 0 { m.cursorx-- } } + +// CursorRight moves the cursor one character right func (m *Messenger) CursorRight() { if m.cursorx < Count(m.response) { m.cursorx++ } } + +// Start moves the cursor to the start of the line func (m *Messenger) Start() { m.cursorx = 0 } + +// End moves the cursor to the end of the line func (m *Messenger) End() { m.cursorx = Count(m.response) } + +// Backspace deletes one character func (m *Messenger) Backspace() { if m.cursorx > 0 { m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:]) m.cursorx-- } } + +// Paste pastes the clipboard func (m *Messenger) Paste() { clip, _ := clipboard.ReadAll("clipboard") m.response = Insert(m.response, m.cursorx, clip) m.cursorx += Count(clip) } + +// WordLeft moves the cursor one word to the left func (m *Messenger) WordLeft() { response := []rune(m.response) m.CursorLeft() @@ -410,6 +427,8 @@ func (m *Messenger) WordLeft() { } m.CursorRight() } + +// WordRight moves the cursor one word to the right func (m *Messenger) WordRight() { response := []rune(m.response) if m.cursorx >= len(response) { @@ -433,6 +452,8 @@ func (m *Messenger) WordRight() { } } } + +// DeleteWordLeft deletes one word to the left func (m *Messenger) DeleteWordLeft() { m.WordLeft() m.response = string([]rune(m.response)[:m.cursorx]) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 6eaf207e..bf3f8e9d 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -57,8 +57,10 @@ var ( // Channel of jobs running in the background jobs chan JobFunction // Event channel - events chan tcell.Event - autosave chan bool + events chan tcell.Event + autosave chan bool + updateterm chan bool + closeterm chan int ) // LoadInput determines which files should be loaded into buffers @@ -206,7 +208,7 @@ func InitScreen() { screen.EnableMouse() } - screen.SetStyle(defStyle) + // screen.SetStyle(defStyle) } // RedrawAll redraws everything -- all the views and the messenger @@ -423,6 +425,8 @@ func main() { jobs = make(chan JobFunction, 100) events = make(chan tcell.Event, 100) autosave = make(chan bool) + updateterm = make(chan bool) + closeterm = make(chan int) LoadPlugins() @@ -474,6 +478,10 @@ func main() { if CurView().Buf.Path != "" { CurView().Save(true) } + case <-updateterm: + continue + case vnum := <-closeterm: + tabs[curTab].views[vnum].CloseTerminal() case event = <-events: } diff --git a/cmd/micro/scrollbar.go b/cmd/micro/scrollbar.go index 1310112b..6389b9a6 100644 --- a/cmd/micro/scrollbar.go +++ b/cmd/micro/scrollbar.go @@ -1,15 +1,17 @@ package main +// Scrollbar represents an optional scrollbar that can be used type ScrollBar struct { view *View } +// Display shows the scrollbar func (sb *ScrollBar) Display() { style := defStyle.Reverse(true) - screen.SetContent(sb.view.x+sb.view.Width-1, sb.view.y+sb.Pos(), ' ', nil, style) + screen.SetContent(sb.view.x+sb.view.Width-1, sb.view.y+sb.pos(), ' ', nil, style) } -func (sb *ScrollBar) Pos() int { +func (sb *ScrollBar) pos() int { numlines := sb.view.Buf.NumLines h := sb.view.Height filepercent := float32(sb.view.Topline) / float32(numlines) diff --git a/cmd/micro/statusline.go b/cmd/micro/statusline.go index 19e409d7..8e8aa8e2 100644 --- a/cmd/micro/statusline.go +++ b/cmd/micro/statusline.go @@ -73,6 +73,12 @@ func (sline *Statusline) Display() { // Maybe there is a unicode filename? fileRunes := []rune(file) + + if sline.view.Type == vtTerm { + fileRunes = []rune(sline.view.termtitle) + rightText = "" + } + viewX := sline.view.x if viewX != 0 { screen.SetContent(viewX, y, ' ', nil, statusLineStyle) diff --git a/cmd/micro/tab.go b/cmd/micro/tab.go index 6917dfeb..650ce269 100644 --- a/cmd/micro/tab.go +++ b/cmd/micro/tab.go @@ -73,6 +73,9 @@ func (t *Tab) Resize() { for i, v := range t.views { v.Num = i + if v.Type == vtTerm { + v.term.Resize(v.Width, v.Height) + } } } diff --git a/cmd/micro/view.go b/cmd/micro/view.go index e53c6618..9172eeaa 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -3,11 +3,13 @@ package main import ( "fmt" "os" + "os/exec" "reflect" "strconv" "strings" "time" + "github.com/james4k/terminal" "github.com/zyedidia/tcell" ) @@ -24,6 +26,7 @@ var ( vtLog = ViewType{2, true, true} vtScratch = ViewType{3, false, true} vtRaw = ViewType{4, true, true} + vtTerm = ViewType{5, true, true} ) // The View struct stores information about a view into a buffer. @@ -99,6 +102,11 @@ type View struct { splitNode *LeafNode scrollbar *ScrollBar + + termState terminal.State + pty *os.File + term *terminal.VT + termtitle string } // NewView returns a new fullscreen view @@ -156,6 +164,46 @@ func (v *View) ToggleStatusLine() { } } +// StartTerminal execs a command in this view +func (v *View) StartTerminal(execCmd []string) error { + // cmd := exec.Command(os.Getenv("SHELL"), "-i") + if len(execCmd) <= 0 { + return nil + } + cmd := exec.Command(execCmd[0], execCmd[1:]...) + term, pty, err := terminal.Start(&v.termState, cmd) + if err != nil { + return err + } + term.Resize(v.Width, v.Height) + v.Type = vtTerm + v.term = term + v.termtitle = execCmd[0] + v.pty = pty + + go func() { + for { + err := term.Parse() + if err != nil { + fmt.Fprintln(os.Stderr, err) + break + } + updateterm <- true + } + closeterm <- v.Num + }() + + return nil +} + +// CloseTerminal shuts down the tty running in this view +// and returns it to the default view type +func (v *View) CloseTerminal() { + v.pty.Close() + v.term.Close() + v.Type = vtDefault +} + // ToggleTabbar creates an extra row for the tabbar if necessary func (v *View) ToggleTabbar() { if len(tabs) > 1 { @@ -366,6 +414,10 @@ func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) { return 0, 0 } +// Bottomline returns the line number of the lowest line in the view +// You might think that this is obviously just v.Topline + v.Height +// but if softwrap is enabled things get complicated since one buffer +// line can take up multiple lines in the view func (v *View) Bottomline() int { if !v.Buf.Settings["softwrap"].(bool) { return v.Topline + v.Height @@ -504,6 +556,13 @@ func (v *View) SetCursor(c *Cursor) bool { // HandleEvent handles an event passed by the main loop func (v *View) HandleEvent(event tcell.Event) { + if v.Type == vtTerm { + if _, ok := event.(*tcell.EventMouse); !ok { + v.pty.WriteString(event.EscSeq()) + } + return + } + if v.Type == vtRaw { v.Buf.Insert(v.Cursor.Loc, reflect.TypeOf(event).String()[7:]) v.Buf.Insert(v.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq())) @@ -736,8 +795,53 @@ func (v *View) openHelp(helpPage string) { } } +// DisplayTerm draws a terminal in this window +// The view's type must be vtTerm +func (v *View) DisplayTerm() { + divider := 0 + if v.x != 0 { + divider = 1 + dividerStyle := defStyle + if style, ok := colorscheme["divider"]; ok { + dividerStyle = style + } + for i := 0; i < v.Height; i++ { + screen.SetContent(v.x, v.y+i, '|', nil, dividerStyle.Reverse(true)) + } + } + v.termState.Lock() + defer v.termState.Unlock() + + for y := 0; y < v.Height; y++ { + for x := 0; x < v.Width; x++ { + + c, f, b := v.termState.Cell(x, y) + + fg, bg := int(f), int(b) + if f == terminal.DefaultFG { + fg = int(tcell.ColorDefault) + } + if b == terminal.DefaultBG { + bg = int(tcell.ColorDefault) + } + st := tcell.StyleDefault.Foreground(GetColor256(int(fg))).Background(GetColor256(int(bg))) + + screen.SetContent(v.x+x+divider, v.y+y, c, nil, st) + } + } + if v.termState.CursorVisible() && tabs[curTab].CurView == v.Num { + curx, cury := v.termState.Cursor() + screen.ShowCursor(curx+v.x+divider, cury+v.y) + } +} + // DisplayView draws the view to the screen func (v *View) DisplayView() { + if v.Type == vtTerm { + v.DisplayTerm() + return + } + if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 { v.leftCol = 0 } @@ -807,11 +911,11 @@ func (v *View) DisplayView() { } colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64)) - if colorcolumn != 0 { + if colorcolumn != 0 && xOffset+colorcolumn-v.leftCol < v.Width { style := GetColor("color-column") fg, _, _ := style.Decompose() st := defStyle.Background(fg) - screen.SetContent(xOffset+colorcolumn, yOffset+visualLineN, ' ', nil, st) + screen.SetContent(xOffset+colorcolumn-v.leftCol, yOffset+visualLineN, ' ', nil, st) } screenX = v.x