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