mirror of
https://github.com/zyedidia/micro.git
synced 2026-02-05 14:40:20 +09:00
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
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user