aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--commands.go120
-rw-r--r--editor/commands.go100
-rw-r--r--editor/editor.go107
-rw-r--r--editor/file.go13
-rw-r--r--editor/text.go (renamed from text.go)241
-rw-r--r--gapbuffer/gap.go81
-rw-r--r--gapbuffer/gap_test.go14
-rw-r--r--poe.go240
-rw-r--r--ui/cli.go55
-rw-r--r--ui/tcell/layout.go (renamed from layout.go)8
-rw-r--r--ui/tcell/style.go (renamed from style.go)8
-rw-r--r--ui/tcell/tcell.go258
-rw-r--r--ui/tcell/view.go (renamed from view.go)36
-rw-r--r--ui/tcell/window.go157
-rw-r--r--ui/ui.go36
-rw-r--r--window.go357
17 files changed, 1030 insertions, 802 deletions
diff --git a/.gitignore b/.gitignore
index 7f9529a..535230c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.DS_Store
acme.dump
poe
testfiles
diff --git a/commands.go b/commands.go
deleted file mode 100644
index 9d31614..0000000
--- a/commands.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package main
-
-import (
- "fmt"
- "os"
- "os/exec"
- "strings"
-)
-
-type CommandFunc func(args string)
-
-var poecmds map[string]CommandFunc
-
-func InitCommands() {
- poecmds = map[string]CommandFunc{
- "Exit": CmdExit,
- "New": CmdNew,
- "Del": CmdDel,
- "Edit": CmdEdit,
- "Newcol": CmdNewcol,
- }
-}
-
-func RunCommand(input string) {
- if input == "" {
- return
- }
-
- input = strings.Trim(input, "\t\n ")
-
- // check poe default commands
- cmd := strings.Split(string(input), " ")
- if fn, ok := poecmds[cmd[0]]; ok {
- fn(strings.TrimPrefix(input, cmd[0]))
- return
- }
-
- // Edit shortcuts for external commands and piping
- switch input[0] {
- case '!', '<', '>', '|':
- CmdEdit(input)
- }
-
- CmdEdit("!" + input)
-}
-
-func CmdExit(args string) {
- ok := true
- for _, win := range AllWindows() {
- if !win.CanClose() {
- ok = false
- }
- }
- if ok {
- quit <- true
- }
-}
-
-func CmdNewcol(args string) {
- screen.Clear()
- screen.Sync()
- workspace.AddCol()
- CmdNew("")
-}
-
-func CmdNew(args string) {
- screen.Clear()
- win := NewWindow(FnEmptyWin)
- workspace.LastCol().AddWindow(win)
-}
-
-func CmdOpen(fn string) {
- screen.Clear()
- var win *Window
- win = FindWindow(fn)
- if win == nil { // only load windows that do no already exists
- win := NewWindow(fn)
- workspace.LastCol().AddWindow(win)
- win.LoadBuffer()
- }
-}
-
-func CmdDel(args string) {
- CurWin.Close()
-}
-
-func CmdEdit(args string) {
- if len(args) < 2 {
- return
- }
-
- switch args[0] {
- case 'f':
- var names []string
- for _, win := range AllWindows() {
- names = append(names, fmt.Sprintf(" %3s %s", win.Flags(), win.Name()))
- }
- printMsg("buffers:\n%s\n", strings.Join(names, "\n"))
- case '!':
- os.Chdir(CurWin.Dir())
- cmd := strings.Split(string(args[1:]), " ")
- path, err := exec.LookPath(cmd[0])
- if err != nil { // path not found, break with silence
- //printMsg("path not found: %s\n", cmd[0])
- break
- }
- out, err := exec.Command(path, cmd[1:]...).Output()
- if err != nil {
- printMsg("error: %s\n", err)
- break
- }
- // if command produced output, print it
- outstr := string(out)
- if outstr != "" {
- printMsg("%s", outstr)
- }
- default:
- printMsg("?\n")
- }
-}
diff --git a/editor/commands.go b/editor/commands.go
new file mode 100644
index 0000000..e3a77b9
--- /dev/null
+++ b/editor/commands.go
@@ -0,0 +1,100 @@
+package editor
+
+import (
+ "fmt"
+ "strings"
+)
+
+type CommandFunc func(args string) string
+
+var poecmds map[string]CommandFunc
+
+func (e *editor) initCommands() {
+ poecmds = map[string]CommandFunc{
+ // "Exit": CmdExit,
+ // "New": CmdNew,
+ // "Del": CmdDel,
+ "Edit": e.CmdEdit,
+ // "Newcol": CmdNewcol,
+ }
+}
+
+func (e *editor) Run(input string) {
+ if input == "" {
+ return
+ }
+
+ input = strings.Trim(input, "\t\n ")
+
+ // check poe default commands
+ cmd := strings.Split(string(input), " ")
+ if fn, ok := poecmds[cmd[0]]; ok {
+ fn(strings.TrimPrefix(input, cmd[0]))
+ return
+ }
+
+ // Edit shortcuts for external commands and piping
+ switch input[0] {
+ case '!', '<', '>', '|':
+ e.CmdEdit(input)
+ }
+
+ e.CmdEdit("!" + input)
+}
+
+//func CmdExit(args string) {
+// ok := true
+// for _, win := range AllWindows() {
+// if !win.CanClose() {
+// ok = false
+// }
+// }
+// if ok {
+// quit <- true
+// }
+//}
+//
+//func CmdNewcol(args string) {
+// screen.Clear()
+// screen.Sync()
+// workspace.AddCol()
+// CmdNew("")
+//}
+//
+//
+//
+
+func (e *editor) CmdEdit(args string) string {
+ if len(args) < 2 {
+ return ""
+ }
+
+ switch args[0] {
+ case 'f':
+ var names []string
+ for _, buf := range e.buffers {
+ names = append(names, fmt.Sprintf("%s", buf.Name()))
+ }
+ return fmt.Sprintf("buffers:\n%s\n", strings.Join(names, "\n"))
+ // case '!':
+ // os.Chdir(CurWin.Dir())
+ // cmd := strings.Split(string(args[1:]), " ")
+ // path, err := exec.LookPath(cmd[0])
+ // if err != nil { // path not found, break with silence
+ // //printMsg("path not found: %s\n", cmd[0])
+ // break
+ // }
+ // out, err := exec.Command(path, cmd[1:]...).Output()
+ // if err != nil {
+ // printMsg("error: %s\n", err)
+ // break
+ // }
+ // // if command produced output, print it
+ // outstr := string(out)
+ // if outstr != "" {
+ // printMsg("%s", outstr)
+ // }
+ default:
+ return "?"
+ }
+}
diff --git a/editor/editor.go b/editor/editor.go
new file mode 100644
index 0000000..67f0ab3
--- /dev/null
+++ b/editor/editor.go
@@ -0,0 +1,107 @@
+package editor
+
+import (
+ "path/filepath"
+ "time"
+
+ "github.com/prodhe/poe/gapbuffer"
+)
+
+// Editor is the edit component that holds text buffers. A UI of some sort operates on the editor to manipulate buffers.
+type Editor interface {
+ NewBuffer() (id int64, buf *Buffer)
+ Buffer(id int64) *Buffer
+ Buffers() ([]int64, []*Buffer)
+ Current() *Buffer
+ LoadBuffers(filenames []string)
+ CloseBuffer(id int64)
+ WorkDir() string
+ Len() int
+ Run(cmd string)
+}
+
+// New returns an empty editor with no buffers loaded.
+func New() Editor {
+ e := &editor{}
+ e.buffers = map[int64]*Buffer{}
+ e.workdir, _ = filepath.Abs(".")
+ e.initCommands()
+ return e
+}
+
+// editor implements Editor.
+type editor struct {
+ buffers map[int64]*Buffer
+ current int64 // id ref to current buffer
+ workdir string
+}
+
+// NewBuffer creates an empty buffer and appends it to the editor. Returns the new id and the new buffer.
+func (e *editor) NewBuffer() (id int64, buf *Buffer) {
+ buf = &Buffer{buf: &gapbuffer.Buffer{}}
+ id = e.genBufferID()
+ e.buffers[id] = buf
+ e.current = id
+ return id, buf
+}
+
+// Buffer returns the buffer with given index. Nil if id not found.
+func (e *editor) Buffer(id int64) *Buffer {
+ if _, ok := e.buffers[id]; ok {
+ e.current = id
+ }
+ return e.buffers[id]
+}
+
+// Buffers returns a slice of IDs and a slice of buffers.
+func (e *editor) Buffers() ([]int64, []*Buffer) {
+ ids := make([]int64, 0, len(e.buffers))
+ bs := make([]*Buffer, 0, len(e.buffers))
+ for i, b := range e.buffers {
+ ids = append(ids, i)
+ bs = append(bs, b)
+ }
+ return ids, bs
+}
+
+// Current returns the current buffer.
+func (e *editor) Current() *Buffer {
+ return e.buffers[e.current]
+}
+
+// CloseBuffer deletes the given buffer from memory. No warnings. Here be dragons.
+func (e *editor) CloseBuffer(id int64) {
+ delete(e.buffers, id)
+}
+
+// Len returns number of buffers currently in the editor.
+func (e *editor) Len() int {
+ return len(e.buffers)
+}
+
+// WorkDir returns the base working directory of the editor.
+func (e *editor) WorkDir() string {
+ if e.workdir == "" {
+ d, _ := filepath.Abs(".")
+ return d
+ }
+ return e.workdir
+}
+
+// LoadBuffers reads files from disk and loads them into windows. Screen need to be initialized.
+func (e *editor) LoadBuffers(fns []string) {
+ // load given filenames and append to buffer list
+ for _, fn := range fns {
+ _, buf := e.NewBuffer()
+ buf.NewFile(fn)
+ buf.ReadFile()
+ }
+
+ if len(fns) == 0 {
+ e.NewBuffer()
+ }
+}
+
+func (e *editor) genBufferID() int64 {
+ return time.Now().UnixNano()
+}
diff --git a/editor/file.go b/editor/file.go
new file mode 100644
index 0000000..e22c279
--- /dev/null
+++ b/editor/file.go
@@ -0,0 +1,13 @@
+package editor
+
+import (
+ "time"
+)
+
+// File holds information about a file on disk.
+type File struct {
+ name string
+ read bool // true if file has been read
+ mtime time.Time // of file when last read/written
+ sha256 string // of file when last read/written
+}
diff --git a/text.go b/editor/text.go
index 8771f12..c9a62ce 100644
--- a/text.go
+++ b/editor/text.go
@@ -1,7 +1,12 @@
-package main
+package editor
import (
+ "crypto/sha256"
+ "fmt"
"io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
"unicode"
"unicode/utf8"
@@ -9,13 +14,20 @@ import (
"github.com/prodhe/poe/gapbuffer"
)
-type Buffer struct{}
+const (
+ BufferScratch uint8 = iota
+ BufferFile
+ BufferDir
+)
-// Text is a buffer for editing. It uses an underlying gap buffer for storage and manages all things text related, like insert, delete, selection, searching and undo/redo.
+// Buffer is a buffer for editing. It uses an underlying gap buffer for storage and manages all things text related, like insert, delete, selection, searching and undo/redo.
//
-// Although the underlying buffer is a pure byte slice, Text only works with runes and UTF-8.
-type Text struct {
+// Although the underlying buffer is a pure byte slice, Buffer only works with runes and UTF-8.
+type Buffer struct {
buf *gapbuffer.Buffer
+ file *File
+ what uint8
+ dirty bool
q0, q1 int // dot/cursor
off int // offset for reading runes in buffer
lastRune rune // save the last read rune
@@ -23,10 +35,164 @@ type Text struct {
history History // undo/redo stack
}
+func (t *Buffer) initBuffer() {
+ if t.buf == nil {
+ t.buf = &gapbuffer.Buffer{}
+ }
+}
+
+func (t *Buffer) NewFile(fn string) {
+ t.file = &File{name: fn}
+}
+
+func (t *Buffer) ReadFile() error {
+ t.initBuffer()
+
+ if t.file == nil || t.file.read {
+ return nil // silent
+ }
+
+ info, err := os.Stat(t.file.name)
+ if err != nil {
+ // if the file exists, print why we could not open it
+ // otherwise just close silently
+ if os.IsExist(err) {
+ return fmt.Errorf("%s", err)
+ }
+ return err
+ }
+
+ // name is a directory; list it's content into the buffer
+ if info.IsDir() {
+ files, err := ioutil.ReadDir(t.file.name)
+ if err != nil {
+ return fmt.Errorf("%s", err)
+ }
+
+ t.what = BufferDir
+
+ // list files in dir
+ for _, f := range files {
+ dirchar := ""
+ if f.IsDir() {
+ dirchar = string(filepath.Separator)
+ }
+ fmt.Fprintf(t.buf, "%s%s\n", f.Name(), dirchar)
+ }
+ return nil
+ }
+
+ // name is a file
+ fh, err := os.OpenFile(t.file.name, os.O_RDWR|os.O_CREATE, 0644)
+ if err != nil {
+ return fmt.Errorf("%s", err)
+ }
+ defer fh.Close()
+
+ if _, err := io.Copy(t.buf, fh); err != nil {
+ return fmt.Errorf("%s", err)
+ }
+ fh.Seek(0, 0)
+
+ h := sha256.New()
+ if _, err := io.Copy(h, fh); err != nil {
+ return fmt.Errorf("%s", err)
+ }
+ t.file.sha256 = fmt.Sprintf("%x", h.Sum(nil))
+
+ t.file.mtime = info.ModTime()
+ t.file.read = true
+
+ t.what = BufferFile
+
+ return nil
+}
+
+func (t *Buffer) SaveFile() (int, error) {
+ t.initBuffer()
+
+ if t.file == nil || t.file.name == "" {
+ return 0, errors.New("no filename")
+ }
+
+ if t.what != BufferFile { // can only save file buffers
+ return 0, nil
+ }
+
+ // check for file existence if we recently changed the file name
+ // openmasks := os.O_RDWR | os.O_CREATE
+ // var namechange bool
+ // if win.Name() != win.NameTag() { // user has changed name
+ // openmasks |= os.O_EXCL // must not already exist
+ // namechange = true // to skip sha256 checksum
+ // }
+
+ f, err := os.OpenFile(t.file.name, os.O_RDWR|os.O_CREATE, 0644)
+ if err != nil {
+ if os.IsExist(err) {
+ return 0, fmt.Errorf("%s already exists", t.file.name)
+ }
+ return 0, err
+ }
+ defer f.Close()
+
+ //h := sha256.New()
+ //if _, err := io.Copy(h, f); err != nil {
+ // return 0, errors.Wrap(err, "sha256")
+ //}
+ //hhex := fmt.Sprintf("%x", h.Sum(nil))
+
+ // verify checksum if the file is not newly created via a namechange
+ // if !namechange && hhex != win.file.sha256 {
+ // return 0, errors.Errorf("file has been modified outside of poe")
+ // }
+
+ n, err := f.WriteAt(t.buf.Bytes(), 0)
+ if err != nil {
+ return 0, err
+ }
+ f.Truncate(int64(n))
+ f.Sync()
+
+ t.file.sha256 = fmt.Sprintf("%x", sha256.Sum256(t.buf.Bytes()))
+
+ info, err := f.Stat()
+ if err != nil {
+ return n, err
+ }
+ t.file.mtime = info.ModTime()
+
+ t.dirty = false
+
+ return n, nil
+}
+
+// Name returns either the file from disk name or empty string if the buffer has no disk counterpart.
+func (t *Buffer) Name() string {
+ if t.file == nil || t.file.name == "" {
+ return ""
+ }
+ s, _ := filepath.Abs(t.file.name)
+ return s
+}
+
+func (t *Buffer) WorkDir() string {
+ switch t.what {
+ case BufferFile, BufferScratch:
+ return filepath.Dir(t.Name())
+ case BufferDir:
+ return t.Name()
+ default:
+ return ""
+ }
+}
+
// Write implements io.Writer, with the side effect of storing written data into a history stack for undo/redo.
//
// If dot has content, it will be replaced by an initial deletion before inserting the bytes.
-func (t *Text) Write(p []byte) (int, error) {
+func (t *Buffer) Write(p []byte) (int, error) {
+ t.initBuffer()
+
// handle replace
if len(t.ReadDot()) > 0 {
t.Delete()
@@ -44,7 +210,9 @@ func (t *Text) Write(p []byte) (int, error) {
}
// Delete removes current selection in dot. If dot is empty, it selects the previous rune and deletes that.
-func (t *Text) Delete() (int, error) {
+func (t *Buffer) Delete() (int, error) {
+ t.initBuffer()
+
if len(t.ReadDot()) == 0 {
t.q0--
c, _ := t.buf.ByteAt(t.q0)
@@ -66,17 +234,21 @@ func (t *Text) Delete() (int, error) {
}
// Len returns the number of bytes in buffer.
-func (t *Text) Len() int {
+func (t *Buffer) Len() int {
+ t.initBuffer()
+
return t.buf.Len()
}
// String returns the entire text buffer as a string.
-func (t *Text) String() string {
+func (t *Buffer) String() string {
+ t.initBuffer()
+
return string(t.buf.Bytes())
}
// ReadRune reads a rune from buffer and advances the internal offset. This could be called in sequence to get all runes from buffer. This populates LastRune().
-func (t *Text) ReadRune() (r rune, size int, err error) {
+func (t *Buffer) ReadRune() (r rune, size int, err error) {
r, size, err = t.ReadRuneAt(t.off)
t.off += size
t.lastRune = r
@@ -84,7 +256,7 @@ func (t *Text) ReadRune() (r rune, size int, err error) {
}
// UnreadRune returns the rune before the current Seek offset and moves the offset to point to that. This could be called in sequence to scan backwards.
-func (t *Text) UnreadRune() (r rune, size int, err error) {
+func (t *Buffer) UnreadRune() (r rune, size int, err error) {
t.off--
r, size, err = t.ReadRuneAt(t.off)
t.off++
@@ -98,7 +270,9 @@ func (t *Text) UnreadRune() (r rune, size int, err error) {
// ReadRuneAt returns the rune and its size at offset. If the given offset (in byte count) is not a valid rune, it will try to back up until it finds a valid starting point for a rune and return that one.
//
// This is basically a Seek(offset) followed by a ReadRune(), but does not affect the internal offset for future reads.
-func (t *Text) ReadRuneAt(offset int) (r rune, size int, err error) {
+func (t *Buffer) ReadRuneAt(offset int) (r rune, size int, err error) {
+ t.initBuffer()
+
var c byte
c, err = t.buf.ByteAt(offset)
if err != nil {
@@ -129,31 +303,34 @@ func (t *Text) ReadRuneAt(offset int) (r rune, size int, err error) {
}
// LastRune returns the last rune read by ReadRune().
-func (t *Text) LastRune() rune {
+func (t *Buffer) LastRune() rune {
return t.lastRune
}
// ReadDot returns content of current dot.
-func (t *Text) ReadDot() string {
+func (t *Buffer) ReadDot() string {
+ t.initBuffer()
+
if t.q0 == t.q1 {
return ""
}
buf := make([]byte, t.q1-t.q0)
_, err := t.buf.ReadAt(buf, t.q0)
if err != nil {
- printMsg("dot: %s", err)
return ""
}
return string(buf)
}
// Dot returns current offsets for dot.
-func (t *Text) Dot() (int, int) {
+func (t *Buffer) Dot() (int, int) {
return t.q0, t.q1
}
// Seek implements io.Seeker and sets the internal offset for next ReadRune() or UnreadRune(). If the offset is not a valid rune start, it will backup until it finds one.
-func (t *Text) Seek(offset, whence int) (int, error) {
+func (t *Buffer) Seek(offset, whence int) (int, error) {
+ t.initBuffer()
+
t.off = offset
switch whence {
@@ -177,7 +354,7 @@ func (t *Text) Seek(offset, whence int) (int, error) {
}
// SeekDot sets the dot to a single offset in the text buffer.
-func (t *Text) SeekDot(offset, whence int) (int, error) {
+func (t *Buffer) SeekDot(offset, whence int) (int, error) {
switch whence {
case io.SeekStart:
q0, _, err := t.SetDot(offset, offset)
@@ -194,7 +371,9 @@ func (t *Text) SeekDot(offset, whence int) (int, error) {
}
// SetDot sets both ends of the dot into an absolute position. It will check the given offsets and adjust them accordingly, so they are not out of bounds or on an invalid rune start. It returns the final offsets. Error is always nil.
-func (t *Text) SetDot(q0, q1 int) (int, int, error) {
+func (t *Buffer) SetDot(q0, q1 int) (int, int, error) {
+ t.initBuffer()
+
t.q0, t.q1 = q0, q1
// check out of bounds
@@ -236,7 +415,7 @@ func (t *Text) SetDot(q0, q1 int) (int, int, error) {
}
// ExpandDot expands the current selection in positive or negative offset. A positive offset expands forwards and a negative expands backwards. Q is 0 or 1, either the left or the right end of the dot.
-func (t *Text) ExpandDot(q, offset int) {
+func (t *Buffer) ExpandDot(q, offset int) {
if q < 0 || q > 1 {
return
}
@@ -255,7 +434,7 @@ func (t *Text) ExpandDot(q, offset int) {
// If on newline, select the whole line.
//
// Otherwise, select word (longest alphanumeric sequence).
-func (t *Text) Select(offset int) {
+func (t *Buffer) Select(offset int) {
offset, _ = t.Seek(offset, io.SeekStart)
start, end := offset, offset
@@ -278,7 +457,7 @@ func (t *Text) Select(offset int) {
t.SetDot(start, end)
}
-func (t *Text) NextSpace(offset int) (n int) {
+func (t *Buffer) NextSpace(offset int) (n int) {
offset, _ = t.Seek(offset, io.SeekStart)
r, size, err := t.ReadRune()
@@ -299,7 +478,7 @@ func (t *Text) NextSpace(offset int) (n int) {
return n
}
-func (t *Text) PrevSpace(offset int) (n int) {
+func (t *Buffer) PrevSpace(offset int) (n int) {
offset, _ = t.Seek(offset, io.SeekStart)
r, size, err := t.ReadRuneAt(offset)
@@ -323,7 +502,7 @@ func (t *Text) PrevSpace(offset int) (n int) {
return n
}
-func (t *Text) NextWord(offset int) (n int) {
+func (t *Buffer) NextWord(offset int) (n int) {
offset, _ = t.Seek(offset, io.SeekStart)
r, size, err := t.ReadRune()
@@ -344,7 +523,7 @@ func (t *Text) NextWord(offset int) (n int) {
return n
}
-func (t *Text) PrevWord(offset int) (n int) {
+func (t *Buffer) PrevWord(offset int) (n int) {
offset, _ = t.Seek(offset, io.SeekStart)
r, size, _ := t.ReadRuneAt(offset)
@@ -361,7 +540,7 @@ func (t *Text) PrevWord(offset int) (n int) {
}
// NextDelim returns number of bytes from given offset up until next delimiter.
-func (t *Text) NextDelim(delim rune, offset int) (n int) {
+func (t *Buffer) NextDelim(delim rune, offset int) (n int) {
t.Seek(offset, io.SeekStart)
r, size, err := t.ReadRune()
@@ -384,7 +563,7 @@ func (t *Text) NextDelim(delim rune, offset int) (n int) {
}
// PrevDelim returns number of bytes from given offset up until next delimiter.
-func (t *Text) PrevDelim(delim rune, offset int) (n int) {
+func (t *Buffer) PrevDelim(delim rune, offset int) (n int) {
t.Seek(offset, io.SeekStart)
r, size, err := t.UnreadRune()
if err != nil {
@@ -406,7 +585,7 @@ func (t *Text) PrevDelim(delim rune, offset int) (n int) {
return n
}
-func (t *Text) Undo() error {
+func (t *Buffer) Undo() error {
c, err := t.history.Undo()
if err != nil {
return errors.Wrap(err, "undo")
@@ -419,7 +598,7 @@ func (t *Text) Undo() error {
return nil
}
-func (t *Text) Redo() error {
+func (t *Buffer) Redo() error {
c, err := t.history.Redo()
if err != nil {
return errors.Wrap(err, "redo")
@@ -434,7 +613,9 @@ func (t *Text) Redo() error {
return nil
}
-func (t *Text) commit(c Change) (int, error) {
+func (t *Buffer) commit(c Change) (int, error) {
+ t.initBuffer()
+
switch c.action {
case HInsert:
t.buf.Seek(c.offset) // sync gap buffer
diff --git a/gapbuffer/gap.go b/gapbuffer/gap.go
index 5bf9ca6..b714c83 100644
--- a/gapbuffer/gap.go
+++ b/gapbuffer/gap.go
@@ -5,24 +5,14 @@ import (
"io"
)
-// BUFSIZE is the initial size of the Buffer when calling New().
-const BUFSIZE = 64
-
// ErrOutOfRange is returned when given position is out of range for the buffer.
var ErrOutOfRange = errors.New("index out of range")
type Buffer struct {
- data []byte
- start int // gap start, is considered empty
- end int // gap end, holds next byte counting from before the gap
-}
-
-func New() *Buffer {
- return &Buffer{
- data: make([]byte, BUFSIZE),
- start: 0,
- end: BUFSIZE,
- }
+ buf []byte
+ bootstrap [64]byte
+ start int // gap start, is considered empty
+ end int // gap end, holds next byte counting from before the gap
}
func (b *Buffer) Bytes() []byte {
@@ -34,16 +24,16 @@ func (b *Buffer) Bytes() []byte {
// Destroy will erase the Buffer by zeroising all fields.
func (b *Buffer) Destroy() {
b.start = 0
- b.end = len(b.data)
+ b.end = len(b.buf)
}
// Byte returns current byte right after the gap. If the gap is at the end, the return will be 0.
func (b *Buffer) Byte() byte {
- if b.end >= len(b.data) {
+ if b.end >= len(b.buf) {
return 0 // EOF
}
- return b.data[b.end]
+ return b.buf[b.end]
}
// ByteAt returns the byte at the given offset, ignoring and hiding the gap.
@@ -55,11 +45,11 @@ func (b *Buffer) ByteAt(offset int) (byte, error) {
if offset < 0 {
return 0, ErrOutOfRange
}
- if offset >= len(b.data) {
+ if offset >= len(b.buf) {
return 0, io.EOF
}
- return b.data[offset], nil
+ return b.buf[offset], nil
}
// gapLen returns the length of the gap.
@@ -69,12 +59,12 @@ func (b *Buffer) gapLen() int {
// Len returns the length of actual data.
func (b *Buffer) Len() int {
- return len(b.data) - b.gapLen()
+ return len(b.buf) - b.gapLen()
}
// Cap returns the capacity of the Buffer, including the gap.
func (b *Buffer) Cap() int {
- return cap(b.data)
+ return cap(b.buf)
}
// Pos returns the current start position of the gap. This is where next write will appear.
@@ -86,7 +76,8 @@ func (b *Buffer) Pos() int {
func (b *Buffer) Seek(newpos int) {
// out of range
if newpos < 0 {
- b.Seek(0)
+ //b.Seek(0)
+ panic("index below zero")
return
}
if newpos > b.Len() {
@@ -107,10 +98,10 @@ func (b *Buffer) Seek(newpos int) {
// Forward is moving the gap one byte forward.
func (b *Buffer) forward() {
- if b.end >= len(b.data) {
+ if b.end >= len(b.buf) {
return
}
- b.data[b.start] = b.data[b.end]
+ b.buf[b.start] = b.buf[b.end]
b.start++
b.end++
}
@@ -120,7 +111,7 @@ func (b *Buffer) backward() {
if b.start <= 0 {
return
}
- b.data[b.end-1] = b.data[b.start-1]
+ b.buf[b.end-1] = b.buf[b.start-1]
b.start--
b.end--
}
@@ -131,7 +122,31 @@ func (b *Buffer) Delete() byte {
return 0
}
b.start--
- return b.data[b.start]
+ return b.buf[b.start]
+}
+
+// grow will grow the buffer if necessary.
+func (b *Buffer) grow() {
+ if b.buf == nil {
+ b.buf = b.bootstrap[:]
+ fill := make([]byte, cap(b.buf))
+ b.buf = append(b.buf, fill...)
+
+ b.start = 0
+ b.end = cap(b.buf)
+ return
+ }
+ oldpos := b.start
+ b.Seek(cap(b.buf))
+
+ b.buf = append(b.buf, 0)
+ exp := cap(b.buf)
+ b.end += exp + 1
+
+ fill := make([]byte, exp)
+ b.buf = append(b.buf, fill...)
+
+ b.Seek(oldpos)
}
// Write writes p into the Buffer at current gap position. The Buffer will expand if needed and this is the only time any new memory allocation is done. The resizing and expanding strategy is handled by the underlying internal byte slice. Cap() will return the size of this slice.
@@ -143,19 +158,9 @@ func (b *Buffer) Write(p []byte) (int, error) {
}
for _, c := range p {
if b.gapLen() == 0 {
- oldpos := b.start
- b.Seek(cap(b.data))
-
- b.data = append(b.data, 0)
- exp := cap(b.data)
- b.end += exp + 1
-
- fill := make([]byte, exp)
- b.data = append(b.data, fill...)
-
- b.Seek(oldpos)
+ b.grow()
}
- b.data[b.start] = c
+ b.buf[b.start] = c
b.start++
}
return len(p), nil
diff --git a/gapbuffer/gap_test.go b/gapbuffer/gap_test.go
index a1eadb0..29a1f72 100644
--- a/gapbuffer/gap_test.go
+++ b/gapbuffer/gap_test.go
@@ -26,7 +26,7 @@ func sliceEqual(a []byte, b []byte) bool {
}
func TestReadAt(t *testing.T) {
- gb := gapbuffer.New()
+ gb := gapbuffer.Buffer{}
gb.Write([]byte(lipsum))
var tt = []struct {
@@ -55,7 +55,7 @@ func TestReadAt(t *testing.T) {
}
func TestSeekAndPos(t *testing.T) {
- gb := gapbuffer.New()
+ gb := gapbuffer.Buffer{}
gb.Write([]byte(lipsum))
var tt = []struct {
@@ -92,7 +92,7 @@ func TestWriteSingle(t *testing.T) {
}
for _, tc := range tt {
- b := gapbuffer.New()
+ b := gapbuffer.Buffer{}
n, err := b.Write(tc.input)
if n != tc.wantret {
t.Errorf("%s: expected %d bytes, got %d", tc.name, tc.wantret, n)
@@ -118,7 +118,7 @@ func TestWriteMulti(t *testing.T) {
}
for _, tc := range tt {
- b := gapbuffer.New()
+ b := gapbuffer.Buffer{}
b.Write(tc.input1)
b.Seek(tc.offset)
b.Write(tc.input2)
@@ -146,7 +146,7 @@ func TestByteAt(t *testing.T) {
}
for _, tc := range tt {
- b := gapbuffer.New()
+ b := gapbuffer.Buffer{}
b.Write(tc.input)
b.Seek(0)
c, err := b.ByteAt(tc.pos)
@@ -156,7 +156,7 @@ func TestByteAt(t *testing.T) {
}
for _, tc := range tt {
- b := gapbuffer.New()
+ b := gapbuffer.Buffer{}
b.Write(tc.input)
b.Seek(2)
c, err := b.ByteAt(tc.pos)
@@ -166,7 +166,7 @@ func TestByteAt(t *testing.T) {
}
for _, tc := range tt {
- b := gapbuffer.New()
+ b := gapbuffer.Buffer{}
b.Write(tc.input)
b.Seek(b.Len())
c, err := b.ByteAt(tc.pos)
diff --git a/poe.go b/poe.go
index 9feeb62..e7fff73 100644
--- a/poe.go
+++ b/poe.go
@@ -4,155 +4,27 @@ import (
"flag"
"fmt"
"os"
- "path/filepath"
"runtime"
- "github.com/gdamore/tcell"
- "github.com/prodhe/poe/gapbuffer"
+ "github.com/prodhe/poe/editor"
+ "github.com/prodhe/poe/ui"
)
-const (
- FnMessageWin = "+poe"
- FnEmptyWin = ""
- RuneWidthZero = '?'
-)
-
-var (
- // Main screen terminal
- screen tcell.Screen
-
- // menu is the main tagline above everything else
- menu *View
-
- // workspace contains windows.
- workspace *Workspace
-
- // CurWin is a pointer to the currently focused window.
- CurWin *Window
-
- // Channels
- events chan tcell.Event
- quit chan bool
-
- // baseDir stores the dir from which the program started in
- baseDir string
-)
-
-// InitScreen initializes the tcell terminal.
-func InitScreen() {
- var err error
- screen, err = tcell.NewScreen()
- if err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
- if err = screen.Init(); err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
- os.Exit(1)
- }
- screen.SetStyle(bodyStyle)
- screen.EnableMouse()
- screen.Clear()
- screen.Sync()
-}
-
-func InitMenu() {
- menu = &View{
- text: &Text{buf: gapbuffer.New()},
- what: ViewMenu,
- style: bodyStyle,
- cursorStyle: bodyCursorStyle,
- hilightStyle: bodyHilightStyle,
- tabstop: 4,
- }
- fmt.Fprintf(menu, "Exit New Newcol")
-}
-
-func InitWorkspace() {
- workspace = &Workspace{} // first resize event will set proper dimensions
- workspace.AddCol()
-}
-
-// LoadBuffers reads files from disk and loads them into windows. Screen need to be initialized.
-func LoadBuffers(fns []string) {
- if len(fns) < 1 {
- fns = append(fns, FnEmptyWin)
- }
-
- // setup windows
- for i, fn := range fns {
- win := NewWindow(fn)
- win.LoadBuffer()
- if i == 0 { // first window gets focus
- CurWin = win
- }
- workspace.Col(0).AddWindow(win)
- }
-
- // add a base directory listing in new col
- workspace.AddCol()
- CmdOpen(baseDir)
-}
-
-func printMsg(format string, a ...interface{}) {
- // get output window
- var poewin *Window
- for _, win := range AllWindows() {
- poename := win.Dir() + string(filepath.Separator) + FnMessageWin
- poename = filepath.Clean(poename)
- if win.NameAbs() == poename && CurWin.Dir() == win.Dir() {
- poewin = win
- }
- }
-
- if poewin == nil {
- poewin = NewWindow(CurWin.Dir() + string(filepath.Separator) + FnMessageWin)
- poewin.body.what = ViewScratch
-
- if len(workspace.cols) < 2 {
- workspace.AddCol()
- }
- workspace.LastCol().AddWindow(poewin)
- }
-
- poewin.body.SetCursor(poewin.body.text.buf.Len(), 0)
-
- if a == nil {
- fmt.Fprintf(poewin.body, format)
- return
- }
- fmt.Fprintf(poewin.body, format, a...)
-
-}
-
-func Redraw() {
- menu.Draw()
- workspace.Draw()
- screen.Show()
-}
-
func main() {
flag.Parse()
- // store current working dir
- var err error
- baseDir, err = filepath.Abs(".")
- if err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
- //baseDir += string(filepath.Separator)
+ e := editor.New()
+
+ e.LoadBuffers(flag.Args())
- // Init
- InitStyles()
- InitScreen()
+ ui := ui.NewTcell()
+ //ui := ui.NewCli()
+ ui.Init(e)
- // proper closing and terminal cleanup on exit and error message on a possible panic
+ // close ui and show stack trace on panic
defer func() {
- if screen != nil {
- screen.Clear()
- screen.Fini()
- }
+ ui.Close()
+
if err := recover(); err != nil {
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true)
@@ -162,92 +34,6 @@ func main() {
}
}()
- // Setup top menu
- InitMenu()
-
- // Create initial workspace
- InitWorkspace()
-
- InitCommands()
-
- // This loads all buffers reading file names from command line and populates the workspace.
- LoadBuffers(flag.Args())
-
- events = make(chan tcell.Event, 100)
- quit = make(chan bool, 1)
-
- go func() {
- for {
- if screen != nil {
- events <- screen.PollEvent()
- }
- }
- }()
-
- // main loop
-loop:
- for {
- Redraw()
-
- // Check for events
- var event tcell.Event
- select {
- case <-quit:
- break loop
- case event = <-events:
- }
-
- for event != nil {
- switch e := event.(type) {
- case *tcell.EventResize:
- w, h := screen.Size()
- menu.Resize(0, 0, w, 1)
- workspace.Resize(0, 1, w, h-1)
- screen.Clear()
- screen.Sync()
- case *tcell.EventKey: // system wide shortcuts
- switch e.Key() {
- case tcell.KeyCtrlL: // refresh terminal
- screen.Clear()
- screen.Sync()
- default: // let the focused view handle event
- if menu.focused {
- menu.HandleEvent(e)
- break
- }
- CurWin.HandleEvent(e)
- }
- case *tcell.EventMouse:
- mx, my := e.Position()
-
- // find which window to send the event to
- for _, win := range AllWindows() {
- win.UnFocus()
- if mx >= win.x && mx < win.x+win.w &&
- my >= win.y && my < win.y+win.h {
- CurWin = win
- }
- }
-
- // check if we are in the menu
- menu.focused = false
- if my < 1 {
- menu.focused = true
- menu.HandleEvent(e)
- break
- }
-
- CurWin.HandleEvent(e)
- }
-
- event = nil
-
- // check tcell event queue before returning to main event check
- select {
- case event = <-events:
- default:
- event = nil
- }
- }
- }
+ // This will loop and listen on chosen UI.
+ ui.Listen()
}
diff --git a/ui/cli.go b/ui/cli.go
new file mode 100644
index 0000000..a7cf9e4
--- /dev/null
+++ b/ui/cli.go
@@ -0,0 +1,55 @@
+package ui
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/prodhe/poe/editor"
+)
+
+// Cli implements ui.Interface with simple command-line driven user actions.
+type Cli struct {
+ ed editor.Editor
+}
+
+func (c *Cli) Init(e editor.Editor) error {
+ c.ed = e
+ return nil
+}
+
+func (c *Cli) Close() {
+}
+
+func (c *Cli) Listen() {
+ scanner := bufio.NewScanner(os.Stdin)
+ var input []string
+outer:
+ for {
+ scanner.Scan()
+ input = strings.Split(scanner.Text(), " ")
+ switch input[0][0] { // first rune in input
+ case 'q': // quit
+ break outer
+ case 'f':
+ ids, bs := c.ed.Buffers()
+ fmt.Printf("%v\n%v\n", ids, bs)
+ case 'b':
+ if len(input) > 1 {
+ id, _ := strconv.ParseInt(input[1], 10, 64)
+ c.ed.Buffer(id)
+ break
+ }
+ c.ed.NewBuffer()
+ case 'a':
+ if len(input) < 2 {
+ break
+ }
+ c.ed.Current().Write([]byte(strings.Join(input[1:], " ")))
+ default:
+ fmt.Println("?")
+ }
+ }
+}
diff --git a/layout.go b/ui/tcell/layout.go
index 605e502..599fb6a 100644
--- a/layout.go
+++ b/ui/tcell/layout.go
@@ -1,4 +1,4 @@
-package main
+package uitcell
type Workspace struct {
x, y, w, h int
@@ -10,7 +10,7 @@ type Column struct {
windows []*Window
}
-// Add adds a new column and resizes. It returns the index of the newly created column.
+// Add adds a new column and resizes.
func (wrk *Workspace) AddCol() {
nx, ny := wrk.x, wrk.y
nw, nh := wrk.w, wrk.h
@@ -120,13 +120,13 @@ func (c *Column) CloseWindow(w *Window) {
CurWin = AllWindows()[0]
} else {
- RunCommand("Exit")
+ ed.Run("Exit")
}
}
// if the only win left is the message win, close all
if len(all) == 1 && CurWin.Name() == FnMessageWin {
- RunCommand("Exit")
+ ed.Run("Exit")
}
}
diff --git a/style.go b/ui/tcell/style.go
index 0e0294f..0dcb565 100644
--- a/style.go
+++ b/ui/tcell/style.go
@@ -1,4 +1,4 @@
-package main
+package uitcell
import "github.com/gdamore/tcell"
@@ -22,8 +22,8 @@ var (
unprintableStyle tcell.Style
)
-// InitStyles initializes the different styles (colors for background/foreground).
-func InitStyles() {
+// initStyles initializes the different styles (colors for background/foreground).
+func initStyles() error {
bodyStyle = tcell.StyleDefault.
Background(tcell.NewHexColor(0xffffea)).
Foreground(tcell.ColorBlack)
@@ -48,4 +48,6 @@ func InitStyles() {
Background(tcell.NewHexColor(0x2222cc))
vertlineStyle = bodyStyle
+
+ return nil
}
diff --git a/ui/tcell/tcell.go b/ui/tcell/tcell.go
new file mode 100644
index 0000000..ff20628
--- /dev/null
+++ b/ui/tcell/tcell.go
@@ -0,0 +1,258 @@
+package uitcell
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/gdamore/tcell"
+ "github.com/prodhe/poe/editor"
+)
+
+const (
+ FnMessageWin = "+poe"
+ FnEmptyWin = ""
+ RuneWidthZero = '?'
+)
+
+var (
+ screen tcell.Screen
+ ed editor.Editor
+ menu *View
+ workspace *Workspace
+ CurWin *Window
+
+ quit chan bool
+ events chan tcell.Event
+)
+
+type Tcell struct{}
+
+func (t *Tcell) Init(e editor.Editor) error {
+ ed = e
+ if err := initScreen(); err != nil {
+ return err
+ }
+
+ if err := initStyles(); err != nil {
+ return err
+ }
+
+ if err := initMenu(); err != nil {
+ return err
+ }
+
+ if err := initWorkspace(); err != nil {
+ return err
+ }
+
+ if err := initWindows(); err != nil {
+ return err
+ }
+
+ quit = make(chan bool, 1)
+ events = make(chan tcell.Event, 100)
+
+ return nil
+}
+
+func (t *Tcell) Close() {
+ if screen == nil {
+ return
+ }
+ screen.DisableMouse()
+ screen.Fini()
+}
+
+func printMsg(format string, a ...interface{}) {
+ // get output window
+ var poewin *Window
+ for _, win := range AllWindows() {
+ poename := win.Dir() + string(filepath.Separator) + FnMessageWin
+ poename = filepath.Clean(poename)
+ if win.Name() == poename && CurWin.Dir() == win.Dir() {
+ poewin = win
+ }
+ }
+
+ if poewin == nil {
+ id, buf := ed.NewBuffer()
+ buf.NewFile(CurWin.Dir() + string(filepath.Separator) + FnMessageWin)
+ poewin = NewWindow(id)
+ poewin.body.what = ViewScratch
+
+ if len(workspace.cols) < 2 {
+ workspace.AddCol()
+ }
+ workspace.LastCol().AddWindow(poewin)
+ }
+
+ poewin.body.SetCursor(poewin.body.text.Len(), 0)
+
+ if a == nil {
+ fmt.Fprintf(poewin.body, format)
+ return
+ }
+ fmt.Fprintf(poewin.body, format, a...)
+}
+
+func initScreen() error {
+ var err error
+ screen, err = tcell.NewScreen()
+ if err != nil {
+ return err
+ }
+ if err = screen.Init(); err != nil {
+ return err
+ }
+ screen.SetStyle(bodyStyle)
+ screen.EnableMouse()
+ screen.Sync()
+ return nil
+}
+
+func initMenu() error {
+ menu = &View{
+ text: &editor.Buffer{},
+ what: ViewMenu,
+ style: bodyStyle,
+ cursorStyle: bodyCursorStyle,
+ hilightStyle: bodyHilightStyle,
+ tabstop: 4,
+ }
+ fmt.Fprintf(menu, "Exit New Newcol")
+ return nil
+}
+
+func initWorkspace() error {
+ workspace = &Workspace{} // first resize event will set proper dimensions
+ workspace.AddCol()
+ return nil
+}
+
+func initWindows() error {
+ ids, _ := ed.Buffers()
+ for _, id := range ids {
+ win := NewWindow(id)
+ workspace.LastCol().AddWindow(win)
+ }
+ return nil
+}
+
+func (t *Tcell) redraw() {
+ menu.Draw()
+ workspace.Draw()
+ screen.Show()
+}
+
+func (t *Tcell) Listen() {
+ go func() {
+ for {
+ events <- screen.PollEvent()
+ }
+ }()
+
+outer:
+ for {
+ // draw
+ t.redraw()
+
+ var event tcell.Event
+
+ select {
+ case <-quit:
+ break outer
+ case event = <-events:
+ }
+
+ for event != nil {
+ switch e := event.(type) {
+ case *tcell.EventResize:
+ w, h := screen.Size()
+ menu.Resize(0, 0, w, 1)
+ workspace.Resize(0, 1, w, h-1)
+ //screen.Clear()
+ screen.Sync()
+ case *tcell.EventKey: // system wide shortcuts
+ switch e.Key() {
+ case tcell.KeyCtrlL: // refresh terminal
+ screen.Clear()
+ screen.Sync()
+ default: // let the focused view handle event
+ if menu.focused {
+ menu.HandleEvent(e)
+ break
+ }
+ if CurWin != nil {
+ CurWin.HandleEvent(e)
+ }
+ }
+ case *tcell.EventMouse:
+ mx, my := e.Position()
+
+ // find which window to send the event to
+ for _, win := range AllWindows() {
+ win.UnFocus()
+ if mx >= win.x && mx < win.x+win.w &&
+ my >= win.y && my < win.y+win.h {
+ CurWin = win
+ }
+ }
+
+ // check if we are in the menu
+ menu.focused = false
+ if my < 1 {
+ menu.focused = true
+ menu.HandleEvent(e)
+ break
+ }
+
+ if CurWin != nil {
+ CurWin.HandleEvent(e)
+ }
+ }
+
+ select {
+ case event = <-events:
+ default:
+ event = nil
+ }
+ }
+ }
+}
+
+func CmdOpen(fn string) {
+ screen.Clear()
+ var win *Window
+ win = FindWindow(fn)
+ if win == nil { //only load windows that do no already exists
+ id, buf := ed.NewBuffer()
+ buf.NewFile(fn)
+ buf.ReadFile()
+ win := NewWindow(id)
+ workspace.LastCol().AddWindow(win)
+ }
+}
+
+func CmdNew(args string) {
+ screen.Clear()
+ id, _ := ed.NewBuffer()
+ win := NewWindow(id)
+ workspace.LastCol().AddWindow(win)
+}
+
+func CmdDel() {
+ CurWin.Close()
+}
+
+func CmdExit() {
+ exit := true
+ wins := AllWindows()
+ for _, win := range wins {
+ if !win.CanClose() {
+ exit = false
+ }
+ }
+ if exit || len(wins) == 0 {
+ quit <- true
+ }
+}
diff --git a/view.go b/ui/tcell/view.go
index 9e26fc2..ef42207 100644
--- a/view.go
+++ b/ui/tcell/view.go
@@ -1,4 +1,4 @@
-package main
+package uitcell
import (
"io"
@@ -10,6 +10,7 @@ import (
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
+ "github.com/prodhe/poe/editor"
)
const (
@@ -26,7 +27,7 @@ type View struct {
style tcell.Style
cursorStyle tcell.Style
hilightStyle tcell.Style
- text *Text
+ text *editor.Buffer
scrollpos int // bytes to skip when drawing content
opos int // overflow offset
tabstop int
@@ -53,7 +54,8 @@ func (v *View) Write(p []byte) (int, error) {
func (v *View) Delete() (int, error) {
// Do not allow deletion beyond what we can see.
// This forces the user to scroll to visible content.
- if v.text.q0 == v.scrollpos && len(v.text.ReadDot()) == 0 {
+ q0, _ := v.text.Dot()
+ if q0 == v.scrollpos && len(v.text.ReadDot()) == 0 {
return 0, nil //silent return
}
n, err := v.text.Delete()
@@ -254,12 +256,13 @@ func (b *View) Draw() {
style := b.style
// highlight cursor
- if (b.text.q0 == b.text.q1 && i == b.text.q0) && b.focused {
+ q0, q1 := b.text.Dot()
+ if (q0 == q1 && i == q0) && b.focused {
style = b.cursorStyle
}
// highlight selection
- if (i >= b.text.q0 && i < b.text.q1) && b.focused {
+ if (i >= q0 && i < q1) && b.focused {
style = b.hilightStyle
}
@@ -323,7 +326,8 @@ func (b *View) Draw() {
}
// show cursor on EOF
- if b.text.q0 == b.text.Len() && b.focused {
+ q0, _ := b.text.Dot()
+ if q0 == b.text.Len() && b.focused {
if x > b.x+b.w {
x = b.x
y++
@@ -381,7 +385,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
// if we clicked inside a current selection, run that one
q0, q1 := v.text.Dot()
if pos >= q0 && pos <= q1 && q0 != q1 {
- RunCommand(v.text.ReadDot())
+ ed.Run(v.text.ReadDot())
return
}
@@ -391,7 +395,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.text.SetDot(p, n)
fn := strings.Trim(v.text.ReadDot(), "\n\t ")
v.text.SetDot(q0, q1)
- RunCommand(fn)
+ ed.Run(fn)
return
}
@@ -441,7 +445,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
// if we clicked inside a current selection, run that one
q0, q1 := v.text.Dot()
if pos >= q0 && pos <= q1 && q0 != q1 {
- RunCommand(v.text.ReadDot())
+ ed.Run(v.text.ReadDot())
return
}
@@ -451,7 +455,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.text.SetDot(p, n)
fn := strings.Trim(v.text.ReadDot(), "\n\t ")
v.text.SetDot(q0, q1)
- RunCommand(fn)
+ ed.Run(fn)
return
case tcell.Button3: // right click
pos := v.XYToOffset(mx, my)
@@ -558,10 +562,10 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.Delete()
return
case tcell.KeyCtrlG: // file info/statistics
- printMsg("0x%.4x %q %d,%d/%d\nbasedir: %s\nwindir: %s\n\nname: %s\nnameabs: %s\ntagname: %s\n",
+ printMsg("0x%.4x %q len %d\nbasedir: %s\nwindir: %s\nname: %s\n",
v.Rune(), v.Rune(),
- v.text.q0, v.text.q1, v.text.Len(),
- baseDir, CurWin.Dir(), CurWin.Name(), CurWin.NameAbs(), CurWin.NameTag())
+ v.text.Len(),
+ ed.WorkDir(), CurWin.Dir(), CurWin.Name())
return
case tcell.KeyCtrlO: // open file/dir
fn := v.text.ReadDot()
@@ -595,7 +599,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
return
}
}
- RunCommand(cmd)
+ ed.Run(cmd)
return
case tcell.KeyCtrlC: // copy to clipboard
str := v.text.ReadDot()
@@ -617,11 +621,11 @@ func (v *View) HandleEvent(ev tcell.Event) {
case tcell.KeyCtrlQ:
// close entire application if we are in the top menu
if v.what == ViewMenu {
- RunCommand("Exit")
+ CmdExit()
return
}
// otherwise, just close this window (CurWin)
- RunCommand("Del")
+ CmdDel()
return
default:
// insert
diff --git a/ui/tcell/window.go b/ui/tcell/window.go
new file mode 100644
index 0000000..fcb146c
--- /dev/null
+++ b/ui/tcell/window.go
@@ -0,0 +1,157 @@
+package uitcell
+
+import (
+ "fmt"
+
+ "github.com/gdamore/tcell"
+ "github.com/prodhe/poe/editor"
+)
+
+// Window is a tagline and a body with an optional underlying file on disk. It is the main component and handles all events apart from the system wide shortcuts.
+type Window struct {
+ x, y, w, h int
+ bufid int64
+ body *View
+ tagline *View
+ col *Column // reference to the column where in
+ hidden bool
+ collapsed bool // not implemented
+ qcnt int // quit count
+}
+
+// NewWindow returns a fresh window associated with the given filename. For special case filenames, look at the package constants.
+func NewWindow(id int64) *Window {
+ win := &Window{
+ bufid: id,
+ body: &View{
+ text: ed.Buffer(id),
+ what: ViewBody,
+ style: bodyStyle,
+ cursorStyle: bodyCursorStyle,
+ hilightStyle: bodyHilightStyle,
+ tabstop: 4,
+ focused: true,
+ },
+ tagline: &View{
+ text: &editor.Buffer{},
+ what: ViewTagline,
+ style: tagStyle,
+ cursorStyle: tagCursorStyle,
+ hilightStyle: tagHilightStyle,
+ tabstop: 4,
+ },
+ }
+
+ fmt.Fprintf(win.tagline, "%s Del ",
+ win.Name(),
+ )
+
+ return win
+}
+
+func AllWindows() []*Window {
+ var ws []*Window
+ for _, col := range workspace.cols {
+ ws = append(ws, col.windows...)
+ }
+ return ws
+}
+
+// FindWindow searches for the given name in current windows and returns a pointer if one already exists. Nil otherwise. Name assumes to be an absolute path.
+func FindWindow(name string) *Window {
+ for _, win := range AllWindows() {
+ if win.Name() == name {
+ return win
+ }
+ }
+ return nil
+}
+
+// Resize will set new values for position and width height. Meant to be used on a resize event for proper recalculation during the Draw().
+func (win *Window) Resize(x, y, w, h int) {
+ win.x, win.y, win.w, win.h = x, y, w, h
+
+ win.tagline.Resize(win.x+3, win.y, win.w-3, 1) // 3 for tagbox
+ win.body.Resize(win.x, win.y+1, win.w, win.h-1)
+}
+
+func (win *Window) UnFocus() {
+ win.body.focused = true
+ win.tagline.focused = false
+}
+
+func (win *Window) Name() string {
+ return win.body.text.Name()
+}
+
+func (win *Window) Dir() string {
+ return win.body.text.WorkDir()
+}
+
+func (win *Window) HandleEvent(ev tcell.Event) {
+ switch ev := ev.(type) {
+ case *tcell.EventMouse:
+ _, my := ev.Position()
+
+ // Set focus to either tagline or body
+ if my > win.tagline.y+win.tagline.h-1 {
+ win.tagline.focused = false
+ } else {
+ win.tagline.focused = true
+ }
+ case *tcell.EventKey:
+ switch ev.Key() {
+ case tcell.KeyCtrlS: // save
+ _, err := win.body.text.SaveFile()
+ if err != nil {
+ printMsg("%s\n", err)
+ }
+ return
+ }
+ }
+
+ // Pass along the event down to current view, if we have not already done something and returned in the switch above.
+ if win.tagline.focused {
+ win.tagline.HandleEvent(ev)
+ } else {
+ win.body.HandleEvent(ev)
+ }
+}
+
+func (win *Window) Draw() {
+ // Draw tag square
+ boxstyle := tagSquareStyle
+ if win.body.dirty {
+ boxstyle = tagSquareModifiedStyle
+ }
+ screen.SetContent(win.x, win.y, ' ', nil, boxstyle)
+ screen.SetContent(win.x+1, win.y, ' ', nil, boxstyle)
+ screen.SetContent(win.x+2, win.y, ' ', nil, win.tagline.style)
+
+ // Tagline
+ win.tagline.Draw()
+
+ // Main text buffer
+ win.body.Draw()
+}
+
+func (win *Window) CanClose() bool {
+ ok := !win.body.dirty || win.qcnt > 0
+ if !ok {
+ name := win.Name()
+ if name == FnEmptyWin {
+ name = "unnamed file"
+ }
+ printMsg("%s modified\n", name)
+ win.qcnt++
+ }
+ return ok
+}
+
+func (win *Window) Close() {
+ if !win.CanClose() {
+ return
+ }
+ ed.CloseBuffer(win.bufid)
+ win.col.CloseWindow(win)
+}
diff --git a/ui/ui.go b/ui/ui.go
new file mode 100644
index 0000000..6dc843a
--- /dev/null
+++ b/ui/ui.go
@@ -0,0 +1,36 @@
+package ui
+
+import (
+ "github.com/prodhe/poe/editor"
+ uitcell "github.com/prodhe/poe/ui/tcell"
+)
+
+const (
+ SignalQuit int = iota
+)
+
+type Messager interface {
+ // Message prints output from the editor, using the Printf signature.
+ Message(format string, a ...interface{})
+}
+
+type Interface interface {
+ //Messager
+
+ // Init initializes the user interface.
+ Init(ed editor.Editor) error
+
+ // Close will close and clean up any resources held by the UI.
+ Close()
+
+ // Listen loops for events and acts upon the editor as it sees fit. It is up to the implementation to decide what events it will look for and how to handle them. This could for example be keyboard input in a terminal implementation updating the buffers or an HTTP server modifing the buffers remotely, depending on the implementation of the UI.
+ Listen()
+}
+
+func NewTcell() Interface {
+ return &uitcell.Tcell{}
+}
+
+func NewCli() Interface {
+ return &Cli{}
+}
diff --git a/window.go b/window.go
deleted file mode 100644
index 0e85bb7..0000000
--- a/window.go
+++ /dev/null
@@ -1,357 +0,0 @@
-package main
-
-import (
- "crypto/sha256"
- "fmt"
- "io"
- "io/ioutil"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/gdamore/tcell"
- "github.com/pkg/errors"
- "github.com/prodhe/poe/gapbuffer"
-)
-
-// File holds information about the file on disk. It is managed by Window and the bytes on disk are optionally loaded into a Text.
-type File struct {
- name string
- read bool // true if file has been read
- mtime time.Time // of file when last read/written
- sha256 string // of file when last read/written
-}
-
-// Window is a tagline and a body with an optional underlying file on disk. It is the main component and handles all events apart from the system wide shortcuts.
-type Window struct {
- x, y, w, h int
- file *File
- isdir bool
- body *View
- tagline *View
- col *Column // reference to the column where in
- hidden bool
- collapsed bool // not implemented
- qcnt int // quit count
-}
-
-// NewWindow returns a fresh window associated with the given filename. For special case filenames, look at the package constants.
-func NewWindow(fn string) *Window {
- fnabs := fn
- if fn != FnEmptyWin {
- var err error
- fnabs, err = filepath.Abs(fn)
- if err != nil {
- printMsg("%s\n", err)
- fnabs = FnEmptyWin
- }
- }
- win := &Window{
- body: &View{
- text: &Text{buf: gapbuffer.New()},
- what: ViewBody,
- style: bodyStyle,
- cursorStyle: bodyCursorStyle,
- hilightStyle: bodyHilightStyle,
- tabstop: 4,
- focused: true,
- },
- tagline: &View{
- text: &Text{buf: gapbuffer.New()},
- what: ViewTagline,
- style: tagStyle,
- cursorStyle: tagCursorStyle,
- hilightStyle: tagHilightStyle,
- tabstop: 4,
- },
- file: &File{name: fnabs},
- }
-
- fmt.Fprintf(win.tagline, "%s%s Del ",
- win.Flags(),
- win.Name(),
- )
-
- return win
-}
-
-func AllWindows() []*Window {
- var ws []*Window
- for _, col := range workspace.cols {
- ws = append(ws, col.windows...)
- }
- return ws
-}
-
-// FindWindow searches for the given name in current windows and returns a pointer if one already exists. Nil otherwise. Name assumes to be an absolute path.
-func FindWindow(name string) *Window {
- for _, win := range AllWindows() {
- if win.NameAbs() == name {
- return win
- }
- }
- return nil
-}
-
-func (win *Window) LoadBuffer() bool {
- if win.file.read || win.Name() == FnEmptyWin || win.body.what == ViewScratch {
- return false
- }
-
- info, err := os.Stat(win.file.name)
- if err != nil {
- // if the file exists, print why we could not open it
- // otherwise just close silently
- if os.IsExist(err) {
- printMsg("%s\n", err)
- }
- win.Close()
- return false
- }
-
- // name is a directory; list it's content into the buffer
- if info.IsDir() {
- files, err := ioutil.ReadDir(win.file.name)
- if err != nil {
- printMsg("%s\n", err)
- return false
- }
-
- win.body.what = ViewScratch
- win.isdir = true
-
- // list files in dir
- for _, f := range files {
- dirchar := ""
- if f.IsDir() {
- dirchar = string(filepath.Separator)
- }
- fmt.Fprintf(win.body.text.buf, "%s%s\n", f.Name(), dirchar)
- }
- return true
- }
-
- // name is a file
- fh, err := os.OpenFile(win.file.name, os.O_RDWR|os.O_CREATE, 0644)
- if err != nil {
- printMsg("%s\n", err)
- return false
- }
- defer fh.Close()
-
- if _, err := io.Copy(win.body.text.buf, fh); err != nil {
- printMsg("%s\n", err)
- return false
- }
- fh.Seek(0, 0)
-
- h := sha256.New()
- if _, err := io.Copy(h, fh); err != nil {
- printMsg("%s\n", err)
- return false
- }
- win.file.sha256 = fmt.Sprintf("%x", h.Sum(nil))
-
- win.file.mtime = info.ModTime()
- win.file.read = true
-
- win.body.SetCursor(0, io.SeekStart)
-
- return true
-}
-
-// SaveFile replaces disk file with buffer content. Returns error if no disk file is set.
-func (win *Window) SaveFile() (int, error) {
- if win.NameTag() == FnEmptyWin {
- return 0, errors.New("no filename")
- }
-
- if win.Name() == FnMessageWin { // can not save +poe window
- return 0, nil
- }
-
- // TODO: check this in real time in case of tag name change...
- if win.isdir { // can not save a directory
- return 0, nil
- }
-
- // check for file existence if we recently changed the file name
- openmasks := os.O_RDWR | os.O_CREATE
- var namechange bool
- if win.Name() != win.NameTag() { // user has changed name
- openmasks |= os.O_EXCL // must not already exist
- namechange = true // to skip sha256 checksum
- }
-
- f, err := os.OpenFile(win.NameTag(), openmasks, 0644)
- if err != nil {
- if os.IsExist(err) {
- printMsg("%s already exists\n", win.NameTag())
- return 0, nil
- }
- return 0, err
- }
- defer f.Close()
-
- h := sha256.New()
- if _, err := io.Copy(h, f); err != nil {
- return 0, errors.Wrap(err, "sha256")
- }
- hhex := fmt.Sprintf("%x", h.Sum(nil))
-
- // verify checksum if the file is not newly created via a namechange
- if !namechange && hhex != win.file.sha256 {
- return 0, errors.Errorf("file has been modified outside of poe")
- }
-
- n, err := f.WriteAt(win.body.text.buf.Bytes(), 0)
- if err != nil {
- return 0, err
- }
- f.Truncate(int64(n))
- f.Sync()
-
- win.file.sha256 = fmt.Sprintf("%x", sha256.Sum256(win.body.text.buf.Bytes()))
-
- info, err := f.Stat()
- if err != nil {
- return n, err
- }
- win.file.mtime = info.ModTime()
-
- win.body.dirty = false
-
- return n, nil
-}
-
-// Name returns either the file from disk name or empty string if the buffer has no disk counterpart.
-func (win *Window) Name() string {
- if win.file.name == FnEmptyWin {
- return FnEmptyWin
- }
- s := win.NameAbs()
- if strings.HasPrefix(s, baseDir) {
- s = "." + strings.TrimPrefix(s, baseDir)
- s = filepath.Clean(s)
- }
- return s
-}
-
-func (win *Window) NameAbs() string {
- if win.file.name == FnEmptyWin {
- return ""
- }
- s, _ := filepath.Abs(win.file.name)
- return s
-}
-
-func (win *Window) NameTag() string {
- tstr := win.tagline.text.String()
- if tstr == "" {
- return ""
- }
- return strings.Split(tstr, " ")[0]
-}
-
-// Dir returns the working directory of current window, without trailing path separator.
-func (win *Window) Dir() string {
- if win.isdir {
- return win.NameAbs()
- }
- if win.Name() == FnEmptyWin {
- return baseDir
- }
- return filepath.Dir(win.NameAbs())
-}
-
-// Flags returns the tagline flags for the window.
-//
-// modified: '
-func (win *Window) Flags() string {
- var flags string
- if win.body.dirty {
- flags += "'"
- }
- return flags
-}
-
-// Resize will set new values for position and width height. Meant to be used on a resize event for proper recalculation during the Draw().
-func (win *Window) Resize(x, y, w, h int) {
- win.x, win.y, win.w, win.h = x, y, w, h
-
- win.tagline.Resize(win.x+3, win.y, win.w-3, 1) // 3 for tagbox
- win.body.Resize(win.x, win.y+1, win.w, win.h-1)
-}
-
-func (win *Window) UnFocus() {
- win.body.focused = true
- win.tagline.focused = false
-}
-
-func (win *Window) HandleEvent(ev tcell.Event) {
- switch ev := ev.(type) {
- case *tcell.EventMouse:
- _, my := ev.Position()
-
- // Set focus to either tagline or body
- if my > win.tagline.y+win.tagline.h-1 {
- win.tagline.focused = false
- } else {
- win.tagline.focused = true
- }
- case *tcell.EventKey:
- switch ev.Key() {
- case tcell.KeyCtrlS: // save
- _, err := win.SaveFile()
- if err != nil {
- printMsg("%s\n", err)
- }
- return
- }
- }
-
- // Pass along the event down to current view, if we have not already done something and returned in the switch above.
- if win.tagline.focused {
- win.tagline.HandleEvent(ev)
- } else {
- win.body.HandleEvent(ev)
- }
-}
-
-func (win *Window) Draw() {
- // Draw tag square
- boxstyle := tagSquareStyle
- if win.body.dirty {
- boxstyle = tagSquareModifiedStyle
- }
- screen.SetContent(win.x, win.y, ' ', nil, boxstyle)
- screen.SetContent(win.x+1, win.y, ' ', nil, boxstyle)
- screen.SetContent(win.x+2, win.y, ' ', nil, win.tagline.style)
-
- // Tagline
- win.tagline.Draw()
-
- // Main text buffer
- win.body.Draw()
-}
-
-func (win *Window) CanClose() bool {
- ok := !win.body.dirty || win.qcnt > 0
- if !ok {
- name := win.Name()
- if name == FnEmptyWin {
- name = "unnamed file"
- }
- printMsg("%s modified\n", name)
- win.qcnt++
- }
- return ok
-}
-
-func (win *Window) Close() {
- if !win.CanClose() {
- return
- }
- win.col.CloseWindow(win)
-}