diff --git a/src/buffer.go b/src/buffer.go index fa5b0dfe..1a4bb067 100644 --- a/src/buffer.go +++ b/src/buffer.go @@ -67,6 +67,11 @@ func (b *Buffer) SaveAs(filename string) error { return err } +// IsDirty returns whether or not the buffer has been modified compared to the one on disk +func (b *Buffer) IsDirty() bool { + return b.savedText != b.text +} + // Insert a string into the rope func (b *Buffer) Insert(idx int, value string) { b.r.Insert(idx, value) diff --git a/src/message.go b/src/message.go index 908987c0..bd41b855 100644 --- a/src/message.go +++ b/src/message.go @@ -4,36 +4,143 @@ import ( "github.com/zyedidia/tcell" ) -var ( - curMessage string - curStyle tcell.Style -) +// Messenger is an object that can send messages to the user and get input from the user (with a prompt) +type Messenger struct { + hasPrompt bool + hasMessage bool -func Message(msg string) { - curMessage = msg - curStyle = tcell.StyleDefault + message string + response string + style tcell.Style + + cursorx int + + s tcell.Screen +} + +// NewMessenger returns a new Messenger struct +func NewMessenger(s tcell.Screen) *Messenger { + m := new(Messenger) + m.s = s + return m +} + +// Message sends a message to the user +func (m *Messenger) Message(msg string) { + m.message = msg + m.style = tcell.StyleDefault if _, ok := colorscheme["message"]; ok { - curStyle = colorscheme["message"] + m.style = colorscheme["message"] } + m.hasMessage = true } -func Error(msg string) { - curMessage = msg - curStyle = tcell.StyleDefault. +// Error sends an error message to the user +func (m *Messenger) Error(msg string) { + m.message = msg + m.style = tcell.StyleDefault. Foreground(tcell.ColorBlack). - Background(tcell.ColorRed) + Background(tcell.ColorMaroon) if _, ok := colorscheme["error-message"]; ok { - curStyle = colorscheme["error-message"] + m.style = colorscheme["error-message"] + } + m.hasMessage = true +} + +// 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 string) (string, bool) { + m.hasPrompt = true + m.Message(prompt) + + response, canceled := "", true + + for m.hasPrompt { + m.Clear() + m.Display() + + event := m.s.PollEvent() + + switch e := event.(type) { + case *tcell.EventKey: + if e.Key() == tcell.KeyEscape { + // Cancel + m.hasPrompt = false + } else if e.Key() == tcell.KeyCtrlC { + // Cancel + m.hasPrompt = false + } else if e.Key() == tcell.KeyCtrlQ { + // Cancel + m.hasPrompt = false + } else if e.Key() == tcell.KeyEnter { + // User is done entering their response + m.hasPrompt = false + response, canceled = m.response, false + } + } + + m.HandleEvent(event) + } + + m.Reset() + return response, canceled +} + +// HandleEvent handles an event for the prompter +func (m *Messenger) HandleEvent(event tcell.Event) { + switch e := event.(type) { + case *tcell.EventKey: + switch e.Key() { + case tcell.KeyLeft: + m.cursorx-- + case tcell.KeyRight: + m.cursorx++ + case tcell.KeyBackspace2: + if m.cursorx > 0 { + m.response = string([]rune(m.response)[:Count(m.response)-1]) + m.cursorx-- + } + case tcell.KeySpace: + m.response += " " + m.cursorx++ + case tcell.KeyRune: + m.response += string(e.Rune()) + m.cursorx++ + } + } + if m.cursorx < 0 { + m.cursorx = 0 } } -func DisplayMessage(s tcell.Screen) { - _, h := s.Size() +// Reset resets the messenger's cursor, message and response +func (m *Messenger) Reset() { + m.cursorx = 0 + m.message = "" + m.response = "" +} - runes := []rune(curMessage) - for x := 0; x < len(runes); x++ { - s.SetContent(x, h-1, runes[x], nil, curStyle) +// Clear clears the line at the bottom of the editor +func (m *Messenger) Clear() { + w, h := m.s.Size() + for x := 0; x < w; x++ { + m.s.SetContent(x, h-1, ' ', nil, tcell.StyleDefault) + } +} + +// Display displays and messages or prompts +func (m *Messenger) Display() { + _, h := m.s.Size() + if m.hasMessage { + runes := []rune(m.message + m.response) + for x := 0; x < len(runes); x++ { + m.s.SetContent(x, h-1, runes[x], nil, m.style) + } + } + if m.hasPrompt { + m.s.ShowCursor(Count(m.message)+m.cursorx, h-1) + m.s.Show() } } diff --git a/src/micro.go b/src/micro.go index 3ffa0821..67b617fd 100644 --- a/src/micro.go +++ b/src/micro.go @@ -82,9 +82,8 @@ func main() { s.SetStyle(defStyle) s.EnableMouse() - v := NewView(NewBuffer(string(input), filename), s) - - Message("welcome to micro") + m := NewMessenger(s) + v := NewView(NewBuffer(string(input), filename), m, s) // Initially everything needs to be drawn redraw := 2 @@ -92,28 +91,19 @@ func main() { if redraw == 2 { v.matches = Match(v.buf.rules, v.buf, v) s.Clear() - DisplayMessage(s) v.Display() v.cursor.Display() v.sl.Display() + m.Display() s.Show() } else if redraw == 1 { v.cursor.Display() - DisplayMessage(s) v.sl.Display() + m.Display() s.Show() } event := s.PollEvent() - - switch e := event.(type) { - case *tcell.EventKey: - if e.Key() == tcell.KeyCtrlQ { - s.Fini() - os.Exit(0) - } - } - redraw = v.HandleEvent(event) } } diff --git a/src/statusline.go b/src/statusline.go index 2623cf8b..4a3fb1cf 100644 --- a/src/statusline.go +++ b/src/statusline.go @@ -19,7 +19,7 @@ func (sl *Statusline) Display() { if file == "" { file = "Untitled" } - if sl.v.buf.text != sl.v.buf.savedText { + if sl.v.buf.IsDirty() { file += " +" } file += " (" + strconv.Itoa(sl.v.cursor.y+1) + "," + strconv.Itoa(sl.v.cursor.GetVisualX()+1) + ")" diff --git a/src/view.go b/src/view.go index 971d5a54..f78151ee 100644 --- a/src/view.go +++ b/src/view.go @@ -3,7 +3,9 @@ package main import ( "github.com/atotto/clipboard" "github.com/zyedidia/tcell" + "os" "strconv" + "strings" ) // The View struct stores information about a view into a buffer. @@ -34,20 +36,23 @@ type View struct { // Syntax highlighting matches matches map[int]tcell.Style + m *Messenger + s tcell.Screen } // NewView returns a new view with fullscreen width and height -func NewView(buf *Buffer, s tcell.Screen) *View { - return NewViewWidthHeight(buf, s, 1, 1) +func NewView(buf *Buffer, m *Messenger, s tcell.Screen) *View { + return NewViewWidthHeight(buf, m, s, 1, 1) } // NewViewWidthHeight returns a new view with the specified width and height percentages -func NewViewWidthHeight(buf *Buffer, s tcell.Screen, w, h float32) *View { +func NewViewWidthHeight(buf *Buffer, m *Messenger, s tcell.Screen, w, h float32) *View { v := new(View) v.buf = buf v.s = s + v.m = m v.widthPercent = w v.heightPercent = h @@ -146,6 +151,23 @@ func (v *View) HandleEvent(event tcell.Event) int { ret = 2 case *tcell.EventKey: switch e.Key() { + case tcell.KeyCtrlQ: + if v.buf.IsDirty() { + quit, canceled := v.m.Prompt("You have unsaved changes. Quit anyway? ") + if !canceled { + if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" { + v.s.Fini() + os.Exit(0) + } else { + return 2 + } + } else { + return 2 + } + } else { + v.s.Fini() + os.Exit(0) + } case tcell.KeyUp: v.cursor.Up() ret = 1 @@ -189,9 +211,18 @@ func (v *View) HandleEvent(event tcell.Event) int { v.cursor.Right() ret = 2 case tcell.KeyCtrlS: + if v.buf.path == "" { + filename, canceled := v.m.Prompt("Filename: ") + if !canceled { + v.buf.path = filename + v.buf.name = filename + } else { + return 2 + } + } err := v.buf.Save() if err != nil { - Error(err.Error()) + v.m.Error(err.Error()) } // Need to redraw the status line ret = 1 diff --git a/todolist.md b/todolist.md index 0d8f3354..19fab07d 100644 --- a/todolist.md +++ b/todolist.md @@ -20,10 +20,6 @@ - [ ] Help screen which lists keybindings and commands - [ ] Opened with Ctrl-h -- [ ] Messages/Prompts - - [x] Messages at the bottom of the screen - - [ ] Prompts at the bottom of the screen - - [ ] Command execution - [ ] Allow executing simple commands at the bottom of the editor (like vim or emacs) @@ -48,3 +44,7 @@ - [x] Colorschemes - [x] Support for 256 color and true color + +- [x] Messages/Prompts + - [x] Messages at the bottom of the screen + - [x] Prompts at the bottom of the screen