mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-22 16:57:12 +09:00
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
664 lines
16 KiB
Go
664 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
"github.com/zyedidia/clipboard"
|
|
"github.com/zyedidia/micro/cmd/micro/shellwords"
|
|
"github.com/zyedidia/tcell"
|
|
)
|
|
|
|
// TermMessage sends a message to the user in the terminal. This usually occurs before
|
|
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
|
|
// regular expressions
|
|
// The function must be called when the screen is not initialized
|
|
// This will write the message, and wait for the user
|
|
// to press and key to continue
|
|
func TermMessage(msg ...interface{}) {
|
|
screenWasNil := screen == nil
|
|
if !screenWasNil {
|
|
screen.Fini()
|
|
screen = nil
|
|
}
|
|
|
|
fmt.Println(msg...)
|
|
fmt.Print("\nPress enter to continue")
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
reader.ReadString('\n')
|
|
|
|
if !screenWasNil {
|
|
InitScreen()
|
|
}
|
|
}
|
|
|
|
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
|
|
// as an error
|
|
func TermError(filename string, lineNum int, err string) {
|
|
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
|
|
}
|
|
|
|
// Messenger is an object that makes it easy to send messages to the user
|
|
// and get input from the user
|
|
type Messenger struct {
|
|
log *Buffer
|
|
// Are we currently prompting the user?
|
|
hasPrompt bool
|
|
// Is there a message to print
|
|
hasMessage bool
|
|
|
|
// Message to print
|
|
message string
|
|
// The user's response to a prompt
|
|
response string
|
|
// style to use when drawing the message
|
|
style tcell.Style
|
|
|
|
// We have to keep track of the cursor for prompting
|
|
cursorx int
|
|
|
|
// This map stores the history for all the different kinds of uses Prompt has
|
|
// It's a map of history type -> history array
|
|
history map[string][]string
|
|
historyNum int
|
|
|
|
// Is the current message a message from the gutter
|
|
gutterMessage bool
|
|
}
|
|
|
|
// AddLog sends a message to the log view
|
|
func (m *Messenger) AddLog(msg ...interface{}) {
|
|
logMessage := fmt.Sprint(msg...)
|
|
buffer := m.getBuffer()
|
|
buffer.insert(buffer.End(), []byte(logMessage+"\n"))
|
|
buffer.Cursor.Loc = buffer.End()
|
|
buffer.Cursor.Relocate()
|
|
}
|
|
|
|
func (m *Messenger) getBuffer() *Buffer {
|
|
if m.log == nil {
|
|
m.log = NewBufferFromString("", "")
|
|
m.log.name = "Log"
|
|
}
|
|
return m.log
|
|
}
|
|
|
|
// Message sends a message to the user
|
|
func (m *Messenger) Message(msg ...interface{}) {
|
|
displayMessage := fmt.Sprint(msg...)
|
|
// only display a new message if there isn't an active prompt
|
|
// this is to prevent overwriting an existing prompt to the user
|
|
if m.hasPrompt == false {
|
|
// if there is no active prompt then style and display the message as normal
|
|
m.message = displayMessage
|
|
|
|
m.style = defStyle
|
|
|
|
if _, ok := colorscheme["message"]; ok {
|
|
m.style = colorscheme["message"]
|
|
}
|
|
|
|
m.hasMessage = true
|
|
}
|
|
// add the message to the log regardless of active prompts
|
|
m.AddLog(displayMessage)
|
|
}
|
|
|
|
// Error sends an error message to the user
|
|
func (m *Messenger) Error(msg ...interface{}) {
|
|
buf := new(bytes.Buffer)
|
|
fmt.Fprint(buf, msg...)
|
|
|
|
// only display a new message if there isn't an active prompt
|
|
// this is to prevent overwriting an existing prompt to the user
|
|
if m.hasPrompt == false {
|
|
// if there is no active prompt then style and display the message as normal
|
|
m.message = buf.String()
|
|
m.style = defStyle.
|
|
Foreground(tcell.ColorBlack).
|
|
Background(tcell.ColorMaroon)
|
|
|
|
if _, ok := colorscheme["error-message"]; ok {
|
|
m.style = colorscheme["error-message"]
|
|
}
|
|
m.hasMessage = true
|
|
}
|
|
// add the message to the log regardless of active prompts
|
|
m.AddLog(buf.String())
|
|
}
|
|
|
|
func (m *Messenger) PromptText(msg ...interface{}) {
|
|
displayMessage := fmt.Sprint(msg...)
|
|
// if there is no active prompt then style and display the message as normal
|
|
m.message = displayMessage
|
|
|
|
m.style = defStyle
|
|
|
|
if _, ok := colorscheme["message"]; ok {
|
|
m.style = colorscheme["message"]
|
|
}
|
|
|
|
m.hasMessage = true
|
|
// add the message to the log regardless of active prompts
|
|
m.AddLog(displayMessage)
|
|
}
|
|
|
|
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
|
|
func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
|
|
m.hasPrompt = true
|
|
m.PromptText(prompt)
|
|
|
|
_, h := screen.Size()
|
|
for {
|
|
m.Clear()
|
|
m.Display()
|
|
screen.ShowCursor(Count(m.message), h-1)
|
|
screen.Show()
|
|
event := <-events
|
|
|
|
switch e := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch e.Key() {
|
|
case tcell.KeyRune:
|
|
if e.Rune() == 'y' || e.Rune() == 'Y' {
|
|
m.AddLog("\t--> y")
|
|
m.hasPrompt = false
|
|
return true, false
|
|
} else if e.Rune() == 'n' || e.Rune() == 'N' {
|
|
m.AddLog("\t--> n")
|
|
m.hasPrompt = false
|
|
return false, false
|
|
}
|
|
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
|
m.AddLog("\t--> (cancel)")
|
|
m.Clear()
|
|
m.Reset()
|
|
m.hasPrompt = false
|
|
return false, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// LetterPrompt gives the user a prompt and waits for a one letter response
|
|
func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
|
|
m.hasPrompt = true
|
|
m.PromptText(prompt)
|
|
|
|
_, h := screen.Size()
|
|
for {
|
|
m.Clear()
|
|
m.Display()
|
|
screen.ShowCursor(Count(m.message), h-1)
|
|
screen.Show()
|
|
event := <-events
|
|
|
|
switch e := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch e.Key() {
|
|
case tcell.KeyRune:
|
|
for _, r := range responses {
|
|
if e.Rune() == r {
|
|
m.AddLog("\t--> " + string(r))
|
|
m.Clear()
|
|
m.Reset()
|
|
m.hasPrompt = false
|
|
return r, false
|
|
}
|
|
}
|
|
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
|
m.AddLog("\t--> (cancel)")
|
|
m.Clear()
|
|
m.Reset()
|
|
m.hasPrompt = false
|
|
return ' ', true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type Completion int
|
|
|
|
const (
|
|
NoCompletion Completion = iota
|
|
FileCompletion
|
|
CommandCompletion
|
|
HelpCompletion
|
|
OptionCompletion
|
|
PluginCmdCompletion
|
|
PluginNameCompletion
|
|
OptionValueCompletion
|
|
)
|
|
|
|
// Prompt sends the user a message and waits for a response to be typed in
|
|
// This function blocks the main loop while waiting for input
|
|
func (m *Messenger) Prompt(prompt, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
|
|
m.hasPrompt = true
|
|
m.PromptText(prompt)
|
|
if _, ok := m.history[historyType]; !ok {
|
|
m.history[historyType] = []string{""}
|
|
} else {
|
|
m.history[historyType] = append(m.history[historyType], "")
|
|
}
|
|
m.historyNum = len(m.history[historyType]) - 1
|
|
|
|
response, canceled := placeholder, true
|
|
m.response = response
|
|
m.cursorx = Count(placeholder)
|
|
|
|
RedrawAll()
|
|
for m.hasPrompt {
|
|
var suggestions []string
|
|
m.Clear()
|
|
|
|
event := <-events
|
|
|
|
switch e := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch e.Key() {
|
|
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
|
|
// Cancel
|
|
m.AddLog("\t--> (cancel)")
|
|
m.hasPrompt = false
|
|
case tcell.KeyEnter:
|
|
// User is done entering their response
|
|
m.AddLog("\t--> " + m.response)
|
|
m.hasPrompt = false
|
|
response, canceled = m.response, false
|
|
m.history[historyType][len(m.history[historyType])-1] = response
|
|
case tcell.KeyTab:
|
|
args, err := shellwords.Split(m.response)
|
|
if err != nil {
|
|
break
|
|
}
|
|
currentArg := ""
|
|
currentArgNum := 0
|
|
if len(args) > 0 {
|
|
currentArgNum = len(args) - 1
|
|
currentArg = args[currentArgNum]
|
|
}
|
|
var completionType Completion
|
|
|
|
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
|
|
if command, ok := commands[args[0]]; ok {
|
|
completionTypes = append([]Completion{CommandCompletion}, command.completions...)
|
|
}
|
|
}
|
|
|
|
if currentArgNum >= len(completionTypes) {
|
|
completionType = completionTypes[len(completionTypes)-1]
|
|
} else {
|
|
completionType = completionTypes[currentArgNum]
|
|
}
|
|
|
|
var chosen string
|
|
if completionType == FileCompletion {
|
|
chosen, suggestions = FileComplete(currentArg)
|
|
} else if completionType == CommandCompletion {
|
|
chosen, suggestions = CommandComplete(currentArg)
|
|
} else if completionType == HelpCompletion {
|
|
chosen, suggestions = HelpComplete(currentArg)
|
|
} else if completionType == OptionCompletion {
|
|
chosen, suggestions = OptionComplete(currentArg)
|
|
} else if completionType == OptionValueCompletion {
|
|
if currentArgNum-1 > 0 {
|
|
chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
|
|
}
|
|
} else if completionType == PluginCmdCompletion {
|
|
chosen, suggestions = PluginCmdComplete(currentArg)
|
|
} else if completionType == PluginNameCompletion {
|
|
chosen, suggestions = PluginNameComplete(currentArg)
|
|
} else if completionType < NoCompletion {
|
|
chosen, suggestions = PluginComplete(completionType, currentArg)
|
|
}
|
|
|
|
if len(suggestions) > 1 {
|
|
chosen = chosen + CommonSubstring(suggestions...)
|
|
}
|
|
|
|
if len(suggestions) != 0 {
|
|
m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
|
|
m.cursorx = Count(m.response)
|
|
}
|
|
}
|
|
}
|
|
|
|
m.HandleEvent(event, m.history[historyType])
|
|
|
|
m.Clear()
|
|
for _, v := range tabs[curTab].views {
|
|
v.Display()
|
|
}
|
|
DisplayTabs()
|
|
m.Display()
|
|
if len(suggestions) > 1 {
|
|
m.DisplaySuggestions(suggestions)
|
|
}
|
|
screen.Show()
|
|
}
|
|
|
|
m.Clear()
|
|
m.Reset()
|
|
return response, canceled
|
|
}
|
|
|
|
// DownHistory fetches the previous item in the history
|
|
func (m *Messenger) UpHistory(history []string) {
|
|
if m.historyNum > 0 {
|
|
m.historyNum--
|
|
m.response = history[m.historyNum]
|
|
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++
|
|
m.response = history[m.historyNum]
|
|
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()
|
|
if m.cursorx <= 0 {
|
|
return
|
|
}
|
|
for IsWhitespace(response[m.cursorx]) {
|
|
if m.cursorx <= 0 {
|
|
return
|
|
}
|
|
m.CursorLeft()
|
|
}
|
|
m.CursorLeft()
|
|
for IsWordChar(string(response[m.cursorx])) {
|
|
if m.cursorx <= 0 {
|
|
return
|
|
}
|
|
m.CursorLeft()
|
|
}
|
|
m.CursorRight()
|
|
}
|
|
|
|
// WordRight moves the cursor one word to the right
|
|
func (m *Messenger) WordRight() {
|
|
response := []rune(m.response)
|
|
if m.cursorx >= len(response) {
|
|
return
|
|
}
|
|
for IsWhitespace(response[m.cursorx]) {
|
|
m.CursorRight()
|
|
if m.cursorx >= len(response) {
|
|
m.CursorRight()
|
|
return
|
|
}
|
|
}
|
|
m.CursorRight()
|
|
if m.cursorx >= len(response) {
|
|
return
|
|
}
|
|
for IsWordChar(string(response[m.cursorx])) {
|
|
m.CursorRight()
|
|
if m.cursorx >= len(response) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeleteWordLeft deletes one word to the left
|
|
func (m *Messenger) DeleteWordLeft() {
|
|
m.WordLeft()
|
|
m.response = string([]rune(m.response)[:m.cursorx])
|
|
}
|
|
|
|
// HandleEvent handles an event for the prompter
|
|
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
|
|
switch e := event.(type) {
|
|
case *tcell.EventKey:
|
|
switch e.Key() {
|
|
case tcell.KeyCtrlA:
|
|
m.Start()
|
|
case tcell.KeyCtrlE:
|
|
m.End()
|
|
case tcell.KeyUp:
|
|
m.UpHistory(history)
|
|
case tcell.KeyDown:
|
|
m.DownHistory(history)
|
|
case tcell.KeyLeft:
|
|
if e.Modifiers() == tcell.ModCtrl {
|
|
m.Start()
|
|
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
|
|
m.WordLeft()
|
|
} else {
|
|
m.CursorLeft()
|
|
}
|
|
case tcell.KeyRight:
|
|
if e.Modifiers() == tcell.ModCtrl {
|
|
m.End()
|
|
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
|
|
m.WordRight()
|
|
} else {
|
|
m.CursorRight()
|
|
}
|
|
case tcell.KeyBackspace2, tcell.KeyBackspace:
|
|
if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
|
|
m.DeleteWordLeft()
|
|
} else {
|
|
m.Backspace()
|
|
}
|
|
case tcell.KeyCtrlW:
|
|
m.DeleteWordLeft()
|
|
case tcell.KeyCtrlV:
|
|
m.Paste()
|
|
case tcell.KeyCtrlF:
|
|
m.WordRight()
|
|
case tcell.KeyCtrlB:
|
|
m.WordLeft()
|
|
case tcell.KeyRune:
|
|
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
|
|
m.cursorx++
|
|
}
|
|
history[m.historyNum] = m.response
|
|
|
|
case *tcell.EventPaste:
|
|
clip := e.Text()
|
|
m.response = Insert(m.response, m.cursorx, clip)
|
|
m.cursorx += Count(clip)
|
|
case *tcell.EventMouse:
|
|
x, y := e.Position()
|
|
x -= Count(m.message)
|
|
button := e.Buttons()
|
|
_, screenH := screen.Size()
|
|
|
|
if y == screenH-1 {
|
|
switch button {
|
|
case tcell.Button1:
|
|
m.cursorx = x
|
|
if m.cursorx < 0 {
|
|
m.cursorx = 0
|
|
} else if m.cursorx > Count(m.response) {
|
|
m.cursorx = Count(m.response)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset resets the messenger's cursor, message and response
|
|
func (m *Messenger) Reset() {
|
|
m.cursorx = 0
|
|
m.message = ""
|
|
m.response = ""
|
|
}
|
|
|
|
// Clear clears the line at the bottom of the editor
|
|
func (m *Messenger) Clear() {
|
|
w, h := screen.Size()
|
|
for x := 0; x < w; x++ {
|
|
screen.SetContent(x, h-1, ' ', nil, defStyle)
|
|
}
|
|
}
|
|
|
|
func (m *Messenger) DisplaySuggestions(suggestions []string) {
|
|
w, screenH := screen.Size()
|
|
|
|
y := screenH - 2
|
|
|
|
statusLineStyle := defStyle.Reverse(true)
|
|
if style, ok := colorscheme["statusline"]; ok {
|
|
statusLineStyle = style
|
|
}
|
|
|
|
for x := 0; x < w; x++ {
|
|
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
|
}
|
|
|
|
x := 0
|
|
for _, suggestion := range suggestions {
|
|
for _, c := range suggestion {
|
|
screen.SetContent(x, y, c, nil, statusLineStyle)
|
|
x++
|
|
}
|
|
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
|
x++
|
|
}
|
|
}
|
|
|
|
// Display displays messages or prompts
|
|
func (m *Messenger) Display() {
|
|
_, h := screen.Size()
|
|
if m.hasMessage {
|
|
if m.hasPrompt || globalSettings["infobar"].(bool) {
|
|
runes := []rune(m.message + m.response)
|
|
posx := 0
|
|
for x := 0; x < len(runes); x++ {
|
|
screen.SetContent(posx, h-1, runes[x], nil, m.style)
|
|
posx += runewidth.RuneWidth(runes[x])
|
|
}
|
|
}
|
|
}
|
|
|
|
if m.hasPrompt {
|
|
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
|
|
screen.Show()
|
|
}
|
|
}
|
|
|
|
// LoadHistory attempts to load user history from configDir/buffers/history
|
|
// into the history map
|
|
// The savehistory option must be on
|
|
func (m *Messenger) LoadHistory() {
|
|
if GetGlobalOption("savehistory").(bool) {
|
|
file, err := os.Open(configDir + "/buffers/history")
|
|
var decodedMap map[string][]string
|
|
if err == nil {
|
|
decoder := gob.NewDecoder(file)
|
|
err = decoder.Decode(&decodedMap)
|
|
file.Close()
|
|
|
|
if err != nil {
|
|
m.Error("Error loading history:", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if decodedMap != nil {
|
|
m.history = decodedMap
|
|
} else {
|
|
m.history = make(map[string][]string)
|
|
}
|
|
} else {
|
|
m.history = make(map[string][]string)
|
|
}
|
|
}
|
|
|
|
// SaveHistory saves the user's command history to configDir/buffers/history
|
|
// only if the savehistory option is on
|
|
func (m *Messenger) SaveHistory() {
|
|
if GetGlobalOption("savehistory").(bool) {
|
|
// Don't save history past 100
|
|
for k, v := range m.history {
|
|
if len(v) > 100 {
|
|
m.history[k] = v[len(m.history[k])-100:]
|
|
}
|
|
}
|
|
|
|
file, err := os.Create(configDir + "/buffers/history")
|
|
if err == nil {
|
|
encoder := gob.NewEncoder(file)
|
|
|
|
err = encoder.Encode(m.history)
|
|
if err != nil {
|
|
m.Error("Error saving history:", err)
|
|
return
|
|
}
|
|
file.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// A GutterMessage is a message displayed on the side of the editor
|
|
type GutterMessage struct {
|
|
lineNum int
|
|
msg string
|
|
kind int
|
|
}
|
|
|
|
// These are the different types of messages
|
|
const (
|
|
// GutterInfo represents a simple info message
|
|
GutterInfo = iota
|
|
// GutterWarning represents a compiler warning
|
|
GutterWarning
|
|
// GutterError represents a compiler error
|
|
GutterError
|
|
)
|