diff options
author | Petter Rodhelind <petter.rodhelind@gmail.com> | 2018-02-22 23:15:13 +0100 |
---|---|---|
committer | Petter Rodhelind <petter.rodhelind@gmail.com> | 2018-02-22 23:15:13 +0100 |
commit | 4bca49f807544bd948a5f5f78e3787411252650f (patch) | |
tree | 5014acfd25b349488fd8116dccccac714bedb65d /testfiles | |
download | poe-4bca49f807544bd948a5f5f78e3787411252650f.tar.gz poe-4bca49f807544bd948a5f5f78e3787411252650f.tar.bz2 poe-4bca49f807544bd948a5f5f78e3787411252650f.zip |
first commit
Diffstat (limited to 'testfiles')
-rw-r--r-- | testfiles/column.txt | 58 | ||||
-rw-r--r-- | testfiles/commands.txt | 48 | ||||
-rw-r--r-- | testfiles/date.txt | 3 | ||||
-rw-r--r-- | testfiles/file.txt | 12 | ||||
-rw-r--r-- | testfiles/hej | 1 | ||||
-rw-r--r-- | testfiles/hejsan | 1 | ||||
-rw-r--r-- | testfiles/itworks.tm | 0 | ||||
-rw-r--r-- | testfiles/lipsum.txt | 19 | ||||
-rw-r--r-- | testfiles/poe.txt | 287 | ||||
-rw-r--r-- | testfiles/text.txt | 229 | ||||
-rw-r--r-- | testfiles/utf8.txt | 14 | ||||
-rw-r--r-- | testfiles/view.txt | 272 | ||||
-rw-r--r-- | testfiles/window.txt | 304 |
13 files changed, 1248 insertions, 0 deletions
diff --git a/testfiles/column.txt b/testfiles/column.txt new file mode 100644 index 0000000..d7f24cf --- /dev/null +++ b/testfiles/column.txt @@ -0,0 +1,58 @@ +package main + +type Columns []*Column + +type Column struct { + x, y int + w, h int + windows []*Window +} + +// Add adds a new column and resizes. It returns the index of the newly created column. +func (cs Columns) Add() Columns { + nw, nh := screen.Size() + + var nx, ny int + if len(cs) > 0 { + nx = cs[len(cs)-1].x + cs[len(cs)-1].w/2 + nw = cs[len(cs)-1].w / 2 + cs[len(cs)-1].w /= 2 + } + + newcol := &Column{nx, ny, nw, nh, nil} + newwin := NewWindow("") + newcol.windows = append(newcol.windows, newwin) + cs = append(cs, newcol) + + return cs +} + +func (c *Column) AddWindow(win *Window) { + c.windows = append(c.windows, win) + c.ResizeInternal() +} + +func (c *Column) Resize(x, y, w, h int) { + c.x, c.y = x, y + c.w, c.h = w, h + + c.ResizeInternal() +} + +func (c *Column) ResizeInternal() { + var n int + for i := range c.windows { + if c.windows[i].visible { + n++ + } + } + + remainder := c.h % n + for i, win := range c.windows { + if i == 0 { + win.Resize(c.x, c.y, c.w, c.h/n+remainder) + continue + } + win.Resize(c.x, c.y+(c.h/n)*(i)+remainder, c.w, c.h/n) + } +} diff --git a/testfiles/commands.txt b/testfiles/commands.txt new file mode 100644 index 0000000..96b49c0 --- /dev/null +++ b/testfiles/commands.txt @@ -0,0 +1,48 @@ +assadfsadfpackage main + +import ( + "fmt" + "os/exec" + "strings" +) + +func toggleCmdline() { + if !ctrlWin.focused { + ctrlWin.visible = true + ctrlWin.SetFocus(true) + CurWin().SetFocus(false) + return + } + ctrlWin.visible = false + ctrlWin.SetFocus(false) + CurWin().SetFocus(true) + + // TODO: should call some sort of ResizeAll here... + screen.Clear() + screen.Sync() +} + +func parseCmd(input []byte) { + if len(input) < 1 { + return + } + + switch input[0] { + case 'f': + var names []string + for _, win := range AllWindows() { + names = append(names, fmt.Sprintf(" %3s %s", win.Flags(), win.Name())) + } + printMsg(strings.Join(names, "\n")) + case '!': + cmd := strings.Split(string(input[1:]), " ") + out, err := exec.Command(cmd[0], cmd[1:]...).Output() + if err != nil { + printMsg("error: %s", err) + break + } + printMsg("%s", out) + default: + printMsg("?") + } +} diff --git a/testfiles/date.txt b/testfiles/date.txt new file mode 100644 index 0000000..f89f298 --- /dev/null +++ b/testfiles/date.txt @@ -0,0 +1,3 @@ +Mån 19 Feb 2018 15:55:51 CET + +åäö diff --git a/testfiles/file.txt b/testfiles/file.txt new file mode 100644 index 0000000..f09a2cd --- /dev/null +++ b/testfiles/file.txt @@ -0,0 +1,12 @@ +package main + +import ( + "time" +) + +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/testfiles/hej b/testfiles/hej new file mode 100644 index 0000000..1df007c --- /dev/null +++ b/testfiles/hej @@ -0,0 +1 @@ +hejtva diff --git a/testfiles/hejsan b/testfiles/hejsan new file mode 100644 index 0000000..dcca51e --- /dev/null +++ b/testfiles/hejsan @@ -0,0 +1 @@ +hejsan new file diff --git a/testfiles/itworks.tm b/testfiles/itworks.tm new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/testfiles/itworks.tm diff --git a/testfiles/lipsum.txt b/testfiles/lipsum.txt new file mode 100644 index 0000000..fd40771 --- /dev/null +++ b/testfiles/lipsum.txt @@ -0,0 +1,19 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec facilisis elit sem, at egestas orci convallis et. Suspendisse non leo vitae orci lobortis eleifend. Donec tempus sapien vel risus porta ultricies. Nunc quis bibendum sem, eget tempus velit. Fusce vel purus nec nisl molestie mattis. Donec lorem turpis, accumsan id odio eu, interdum varius augue. Donec id aliquam metus. Donec pharetra dui magna, eu faucibus arcu posuere a. Praesent pharetra, enim sit amet faucibus posuere, dolor tellus ullamcorper quam, sit amet pellentesque diam massa vitae dui. Nulla nec faucibus justo. Etiam dignissim, tellus quis rhoncus sagittis, ipsum urna interdum nunc, at maximus diam magna non ligula. + +Fusce vitae molestie tortor. Fusce congue ornare risus vitae dignissim. Praesent volutpat erat sit amet posuere varius. Fusce id fermentum risus. In ac eros varius, fringilla erat ac, cursus odio. Nunc consectetur vitae dolor non cursus. Sed eleifend imperdiet sem sit amet rutrum. Nulla pretium et ante eu lobortis. Suspendisse porta sodales fermentum. + +Proin dignissim lorem sed leo aliquam rutrum. Donec vel lorem vitae dui mollis lobortis. Nam ac ornare tellus, ac venenatis nulla. Curabitur sagittis at nulla id blandit. Curabitur porta sit amet orci sed aliquam. Duis fermentum tincidunt rutrum. Morbi varius blandit velit in interdum. Cras congue pretium nisl nec interdum. + +Sed rutrum risus sed mauris cursus, nec viverra dolor blandit. Vestibulum commodo malesuada felis vitae varius. Nulla euismod id felis eu pulvinar. Fusce nisl mauris, pretium quis dignissim sit amet, consequat sed tortor. Vestibulum sollicitudin mi risus, vitae porta dolor semper vitae. Vestibulum tristique libero ac mauris tristique, at blandit ipsum facilisis. In mollis diam a dapibus fermentum. Phasellus laoreet diam id pretium faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse ornare velit eget lorem tincidunt vehicula. Duis purus augue, finibus ut dolor nec, tempor vulputate ipsum. Ut pellentesque id sem quis luctus. Integer tristique facilisis eleifend. Sed at egestas risus. + +Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque felis leo, lacinia id placerat eu, lobortis nec sapien. Pellentesque finibus accumsan ultrices. Integer vel rhoncus nisi. Vestibulum vitae tincidunt enim. Suspendisse molestie vitae lacus sed aliquam. Donec egestas sed ante eu dignissim. In ac neque facilisis, ullamcorper odio volutpat, auctor massa. Nulla tortor ante, interdum sit amet massa vel, cursus feugiat metus. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec facilisis elit sem, at egestas orci convallis et. Suspendisse non leo vitae orci lobortis eleifend. Donec tempus sapien vel risus porta ultricies. Nunc quis bibendum sem, eget tempus velit. Fusce vel purus nec nisl molestie mattis. Donec lorem turpis, accumsan id odio eu, interdum varius augue. Donec id aliquam metus. Donec pharetra dui magna, eu faucibus arcu posuere a. Praesent pharetra, enim sit amet faucibus posuere, dolor tellus ullamcorper quam, sit amet pellentesque diam massa vitae dui. Nulla nec faucibus justo. Etiam dignissim, tellus quis rhoncus sagittis, ipsum urna interdum nunc, at maximus diam magna non ligula. + +Fusce vitae molestie tortor. Fusce congue ornare risus vitae dignissim. Praesent volutpat erat sit amet posuere varius. Fusce id fermentum risus. In ac eros varius, fringilla erat ac, cursus odio. Nunc consectetur vitae dolor non cursus. Sed eleifend imperdiet sem sit amet rutrum. Nulla pretium et ante eu lobortis. Suspendisse porta sodales fermentum. + +Proin dignissim lorem sed leo aliquam rutrum. Donec vel lorem vitae dui mollis lobortis. Nam ac ornare tellus, ac venenatis nulla. Curabitur sagittis at nulla id blandit. Curabitur porta sit amet orci sed aliquam. Duis fermentum tincidunt rutrum. Morbi varius blandit velit in interdum. Cras congue pretium nisl nec interdum. + +Sed rutrum risus sed mauris cursus, nec viverra dolor blandit. Vestibulum commodo malesuada felis vitae varius. Nulla euismod id felis eu pulvinar. Fusce nisl mauris, pretium quis dignissim sit amet, consequat sed tortor. Vestibulum sollicitudin mi risus, vitae porta dolor semper vitae. Vestibulum tristique libero ac mauris tristique, at blandit ipsum facilisis. In mollis diam a dapibus fermentum. Phasellus laoreet diam id pretium faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse ornare velit eget lorem tincidunt vehicula. Duis purus augue, finibus ut dolor nec, tempor vulputate ipsum. Ut pellentesque id sem quis luctus. Integer tristique facilisis eleifend. Sed at egestas risus. + +Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque felis leo, lacinia id placerat eu, lobortis nec sapien. Pellentesque finibus accumsan ultrices. Integer vel rhoncus nisi. Vestibulum vitae tincidunt enim. Suspendisse molestie vitae lacus sed aliquam. Donec egestas sed ante eu dignissim. In ac neque facilisis, ullamcorper odio volutpat, auctor massa. Nulla tortor ante, interdum sit amet massa vel, cursus feugiat metus. diff --git a/testfiles/poe.txt b/testfiles/poe.txt new file mode 100644 index 0000000..ea84a03 --- /dev/null +++ b/testfiles/poe.txt @@ -0,0 +1,287 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + "strings" + + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/encoding" + "github.com/prodhe/poe/gapbuffer" +) + +var ( + // Main screen terminal + screen tcell.Screen + + // Styles + defStyle tcell.Style + cursorStyle tcell.Style + tagStyle tcell.Style + separatorStyle tcell.Style + + // cols contains windows. + cols Columns + + // Control window: commands and output + ctrlWin *Window + + // curWin index points to the currently focused window from AllWindows(). + curWin int + + // redrawCount makes sure the garbage collector is run regularly + redrawCount uint +) + +// InitStyles initializes the different styles (colors for background/foreground). +func InitStyles() { + defStyle = tcell.StyleDefault. + Background(tcell.NewHexColor(0xffffea)). + Foreground(tcell.ColorBlack) + cursorStyle = defStyle.Background(tcell.NewHexColor(0x99994c)) + tagStyle = tcell.StyleDefault. + Background(tcell.NewHexColor(0xeaffff)). + Foreground(tcell.ColorBlack) + separatorStyle = defStyle +} + +// InitSceen initializes the tcell terminal. +func InitScreen() { + // setup tcell terminal + encoding.Register() + 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(defStyle) + screen.EnableMouse() + screen.Clear() +} + +func InitControlWindow() { + w, h := screen.Size() + + fn := "+poe" + ctrlWin = &Window{ + body: &View{text: &Text{buf: gapbuffer.New()}}, + tagline: &View{text: &Text{buf: gapbuffer.New()}}, + file: &File{name: fn}, + } + ctrlWin.body.SetStyle(defStyle) + ctrlWin.tagline.SetStyle(tagStyle) + ctrlWin.Resize(w-w/3+1, 0, w/3-1, h) + ctrlWin.body.tabstop = 4 + ctrlWin.visible = true + + fmt.Fprintf(ctrlWin.tagline, "Exit Save Clear h %d", ctrlWin.body.h) +} + +// 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, "") + } + + //w, h := screen.Size() + + // create a first column + // if cols == nil { + // cols = append(cols, &Column{0, 0, w / 2, h, nil}) + // } + cols = cols.Add() + + // setup windows + for i, fn := range fns { + win := NewWindow(fn) + win.LoadBuffer() + if i == 0 { // first window gets focus + win.SetFocus(true) + } + cols[len(cols)-1].AddWindow(win) + } + + cols = cols.Add() + cols[len(cols)-1].AddWindow(NewWindow("")) +} + +func printMsg(format string, a ...interface{}) { + if a == nil { + fmt.Fprintf(ctrlWin.body, format) + return + } + fmt.Fprintf(ctrlWin.body, format, a...) +} + +func cleanExit() { + if screen != nil { + screen.Fini() + } + if err := recover(); err != nil { + buf := make([]byte, 1<<16) + n := runtime.Stack(buf, true) + fmt.Println("poe error:", err) + fmt.Printf("%s", buf[:n+1]) + os.Exit(1) + } +} + +func main() { + flag.Parse() + + // Init + InitStyles() + InitScreen() + InitControlWindow() + + // proper closing and terminal cleanup on exit and error message on a possible panic + defer cleanExit() + + // This loads all buffers reading file names from command line. Each file handler must be closed manually later on. + LoadBuffers(flag.Args()) + + var qcnt int + //var w, h int + var ev tcell.Event + // main loop +outer: + for { + //w, h = screen.Size() + ev = screen.PollEvent() + + var mx, my, mscroll int + var btn1clk bool + + switch ev := ev.(type) { + case *tcell.EventResize: + evw, evh := ev.Size() + //printMsg("old %d %d, new %d %d\n", + //w, h, evw, evh) + for _, col := range cols { + //newx := win.x + (evw - w) + //newy := win.y + (evh - h) + //neww := win.w + (evw - w) + //newh := win.h + (evh - h) + + col.Resize(col.x, col.y, evw, evh) + } + screen.Clear() + screen.Sync() + case *tcell.EventKey: + // system wide shortcuts + key := ev.Key() + switch key { + case tcell.KeyCtrlL: // refresh terminal + screen.Clear() + screen.Sync() + case tcell.KeyCtrlQ: + //cmdline.focused = false + var unsaved []string + for _, win := range AllWindows() { + if win.body.dirty { + unsaved = append(unsaved, fmt.Sprintf(" %3s %s", win.Flags(), win.Name())) + } + } + qcnt++ + if qcnt > 1 || len(unsaved) == 0 { + break outer // leaves main loop and deferred exit will cleanup + } + printMsg("changed files\n%s\n", strings.Join(unsaved, "\n")) + case tcell.KeyCtrlN: // cycle through buffers + CurWin().SetFocus(false) + curWin++ + if curWin >= len(AllWindows()) { + curWin = 0 + } + if !CurWin().file.read && CurWin().file.name != "" { + CurWin().LoadBuffer() + } + CurWin().SetFocus(true) + CurWin().visible = true + cols[0].ResizeInternal() + case tcell.KeyCtrlD: // open command prompt + toggleCmdline() + default: // let the focused window or cmdline take over keyboard input + qcnt = 0 + if ctrlWin.focused { + ctrlWin.HandleEvent(ev) + } else { + CurWin().HandleEvent(ev) + } + } + case *tcell.EventMouse: + mx, my = ev.Position() + switch ev.Buttons() { + case tcell.Button1: + btn1clk = true + case tcell.WheelUp: // scrollup + mscroll = -1 + case tcell.WheelDown: // scrolldown + mscroll = 1 + } + } + + // Draw command line + if ctrlWin.visible { + ctrlWin.tagline.Draw() + ctrlWin.body.Draw() + } + + // Scroll message line if that is where the mouse is + // x, y, w, h := msgline.Size() + // if mx >= x && mx <= x+w && my >= y && my <= y+h { + // msgline.Scroll(mscroll) + // mscroll = 0 // stop other windows from scrolling + // } + + // Draw windows + for _, win := range AllWindows() { + if !win.visible { + continue + } + + if win.focused && mscroll != 0 { + win.body.Scroll(mscroll) + } + + if btn1clk { + var v *View + v = win.body + offset := v.XYToOffset(mx, my) + printMsg("x: %d, y: %d, offset: %d\n", mx, my, offset) + v.SetCursor(offset, 0) + } + + // Tagline + win.tagline.text.buf.Destroy() + fmt.Fprintf(win.tagline, "%3s %s x %d y %d w %d h %d", + win.Flags(), + win.Name(), + win.x, + win.y, + win.w, + win.h, + ) + win.tagline.Draw() + + // Main text buffer + win.body.Draw() + } + + // Update screen with newly drawed content + screen.Show() + + // Garbage collect regularly + if redrawCount%50 == 0 { + runtime.GC() + } + redrawCount++ + } +} diff --git a/testfiles/text.txt b/testfiles/text.txt new file mode 100644 index 0000000..c2dc4af --- /dev/null +++ b/testfiles/text.txt @@ -0,0 +1,229 @@ +package main + +import ( + "unicode" + + "github.com/pkg/errors" + "github.com/prodhe/poe/gapbuffer" +) + +type Text struct { + buf *gapbuffer.Buffer + cursorpos int + p0, p1 int // not implemented + history History +} + +// Sync synchronizes the underlying buffer with the cursor position. This should be called before every call that manipulates data in the buffer. +func (t *Text) Sync() { + t.buf.Seek(t.cursorpos) +} + +func (t *Text) Write(p []byte) (int, error) { + c := Change{t.cursorpos, ActionInsert, p} + n, err := t.commit(c) + if err != nil { + return n, err + } + t.history.Do(c) + return n, nil +} + +func (t *Text) Delete() (int, error) { + if t.cursorpos <= 0 { + return 0, errors.New("out of range") + } + b, _ := t.buf.ByteAt(t.cursorpos - 1) + c := Change{t.cursorpos, ActionDelete, []byte{b}} + n, err := t.commit(c) + if err != nil { + return n, err + } + t.history.Do(c) + return n, nil +} + +func (t *Text) Undo() error { + change, err := t.history.Undo() + if err != nil { + return errors.Wrap(err, "undo") + } + t.commit(change) + return nil +} + +func (t *Text) Redo() error { + change, err := t.history.Redo() + if err != nil { + return errors.Wrap(err, "redo") + } + t.commit(change) + return nil +} + +func (t *Text) commit(c Change) (int, error) { + switch c.action { + case ActionInsert: + t.SetCursor(c.offset, 0) + t.Sync() + n, err := t.buf.Write(c.content) + if err != nil { + return 0, err + } + t.SetCursor(n, 1) + return n, err + case ActionDelete: + t.SetCursor(c.offset, 0) + t.Sync() + var ds []byte + for i := c.offset; i <= c.offset; i++ { + ds = append(ds, t.buf.Delete()) + } + t.SetCursor(len(ds), -1) + return len(ds), nil + default: + return 0, errors.New("invalid action in change") + } +} + +// WordOffset returns the absolute offset for the beginning of the word and the end. A word is a letter/digit sequence of bytes separated by anything else. It scans from the current cursor position. +func (t *Text) WordOffset() (start, end int) { + start, end = t.cursorpos, t.cursorpos + c, _ := t.buf.ByteAt(start) + for unicode.IsLetter(rune(c)) || unicode.IsDigit(rune(c)) { + start-- + c, _ = t.buf.ByteAt(start) + } + if start < t.cursorpos { + start++ + } + c, _ = t.buf.ByteAt(end) + for unicode.IsLetter(rune(c)) || unicode.IsDigit(rune(c)) { + end++ + c, _ = t.buf.ByteAt(end) + } + return start, end +} + +// NextNL returns number of bytes from given position to the nearest next new line. +func (t *Text) NextNL(start int) int { + if start >= t.buf.Len() { + return 0 + } + n := start + offset := 0 + if c, _ := t.buf.ByteAt(n); c == '\n' { + n++ + offset++ + } + for { + c, _ := t.buf.ByteAt(n) + if c == '\n' || n >= t.buf.Len() { + break + } + n++ + offset++ + } + return offset +} + +// PrevNL returns number of bytes from given position to the nearest new line backwards. +func (t *Text) PrevNL(start int) int { + if start == 0 { + return 0 + } + if start > t.buf.Len() { + start = t.buf.Len() - 1 + } + n := start - 1 + offset := 1 + if c, _ := t.buf.ByteAt(n); c == '\n' { + return offset + } + for { + c, _ := t.buf.ByteAt(n) + if c == '\n' || n == 0 { + break + } + n-- + offset++ + } + return offset +} + +// SetCursor moves the cursor to different offsets in the text buffer. +// +// A negative whence is backwards from current position, a positive whence is forward and a zero whence sets the absolute position from the start of the buffer. +func (t *Text) SetCursor(pos, whence int) { + switch whence { + case -1: + t.cursorpos -= pos + case 0: + t.cursorpos = pos + case 1: + t.cursorpos += pos + } + + // check out of range + if t.cursorpos < 0 { + t.cursorpos = 0 + } + if t.cursorpos >= t.buf.Len() { + t.cursorpos = t.buf.Len() + } +} + +/* History */ +type Action int + +const ( + ActionInsert Action = iota + ActionDelete +) + +type Change struct { + offset int + action Action + content []byte +} + +type History struct { + done []Change + recall []Change +} + +func (h *History) Do(c Change) { + h.done = append(h.done, c) + h.recall = nil // clear old recall stack on new do +} + +func (h *History) Undo() (Change, error) { + if len(h.done) == 0 { + return Change{}, errors.New("no history") + } + lastdone := h.done[len(h.done)-1] + h.recall = append(h.recall, lastdone) + h.done = h.done[:len(h.done)-1] // remove last one + + // Reverse the done action so the returned change can be applied directly. + switch lastdone.action { + case ActionInsert: + lastdone.action = ActionDelete + lastdone.offset += len(lastdone.content) + case ActionDelete: + lastdone.action = ActionInsert + lastdone.offset -= len(lastdone.content) + } + + return lastdone, nil +} + +func (h *History) Redo() (Change, error) { + if len(h.recall) == 0 { + return Change{}, errors.New("no recall history") + } + lastrecall := h.recall[len(h.recall)-1] + h.done = append(h.done, lastrecall) + h.recall = h.recall[:len(h.recall)-1] //remove last one + return lastrecall, nil +} diff --git a/testfiles/utf8.txt b/testfiles/utf8.txt new file mode 100644 index 0000000..14aa094 --- /dev/null +++ b/testfiles/utf8.txt @@ -0,0 +1,14 @@ +åäö +ÅÄÖ + +😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏 + +😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟 + +😠😡😢😣😥😦😧😨😩😪😫😭😮😯😰😱 + +😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀☠ + +--- + +noooo diff --git a/testfiles/view.txt b/testfiles/view.txt new file mode 100644 index 0000000..7cb4000 --- /dev/null +++ b/testfiles/view.txt @@ -0,0 +1,272 @@ +package main + +import ( + "github.com/gdamore/tcell" +) + +type View struct { + x, y int // position + w, h int + rows []int // cursor offset for beginning of each row + style tcell.Style + text *Text + scrollpos int // bytes to skip when drawing content + opos int // overflow + tabstop int + history History + focused bool + dirty bool +} + +func (b *View) Write(p []byte) (int, error) { + n, err := b.text.Write(p) + if err != nil { + return 0, err + } + b.dirty = true + return n, err +} + +func (b *View) Delete() (int, error) { + // Do not allow deletion beyond what we can see. + // This forces the user to scroll to visible content. + if b.text.cursorpos == b.scrollpos { + return 0, nil //silent return + } + n, err := b.text.Delete() + if err != nil { + return n, err + } + + b.dirty = true + + return n, nil +} + +func (b *View) SetStyle(style tcell.Style) { + b.style = style +} + +func (b *View) Resize(x, y, w, h int) { + b.x = x + b.y = y + b.w = w + b.h = h +} + +func (b *View) Size() (x, y, w, h int) { + return b.x, b.y, b.w, b.h +} + +// Byte returns the current byte under the cursor. +func (b *View) Byte() byte { + c, _ := b.text.buf.ByteAt(b.text.cursorpos) + return c +} + +func (b *View) Overflow() int { + return b.opos +} + +func (b *View) Cursor() int { + return b.text.cursorpos +} + +func (b *View) SetCursor(pos, whence int) { + b.text.SetCursor(pos, whence) + + // // scroll to cursor if out of screen + // if b.Cursor() < b.scrollpos || b.Cursor() >= b.Overflow() { + // //if b.Cursor() != b.buf.Len() { // do not autoscroll on +1 last byte + // b.ScrollTo(b.Cursor()) + // //} + // } +} + +// XYToOffset translates mouse coordinates in a 2D terminal to the correct byte offset in buffer. +func (b *View) XYToOffset(x, y int) int { + offset := b.scrollpos + + for y-b.y > 0 { + c, _ := b.text.buf.ByteAt(offset) + if c == '\n' { + offset++ + y-- + continue + } + xw := 0 + for ; c != '\n' && xw < b.x+b.w; offset++ { + c, _ = b.text.buf.ByteAt(offset) + xw++ + } + y-- + } + + for x-b.x > 0 { + c, _ := b.text.buf.ByteAt(offset) + if c == '\n' { + break + } + if c == '\t' { + x -= b.tabstop - 1 + } + offset++ + x-- + } + + return offset +} + +// Scroll will move the visible part of the buffer in number of lines. Negative means upwards. +func (b *View) Scroll(n int) { + offset := 0 + if n > 0 { + for n > 0 { + if c, _ := b.text.buf.ByteAt(b.scrollpos + offset); c == '\n' { + offset++ + } else { + offset += b.text.NextNL(b.scrollpos+offset) + 1 + } + n-- + } + b.scrollpos += offset + } + + if n < 0 { + offset += b.text.PrevNL(b.scrollpos) + for n < 0 { + offset += b.text.PrevNL(b.scrollpos - offset) + n++ + } + if b.scrollpos-offset > 0 { + b.scrollpos -= offset - 1 + } else { + b.scrollpos = 0 + } + } + + // boundaries + if b.scrollpos < 0 { + b.scrollpos = 0 + } + if b.scrollpos > b.text.buf.Len() { + b.scrollpos = b.text.buf.Len() + } +} + +// ScrollTo will scroll to an absolute byte offset in the buffer and backwards to the nearest previous newline. +func (b *View) ScrollTo(offset int) { + offset -= b.text.PrevNL(offset) + if offset > 0 { + offset += 1 + } + b.scrollpos = offset + b.Scroll(-(b.h / 3)) // scroll a third page more for context +} + +func (b *View) Draw() { + x, y := b.x, b.y + + // clear + for y := b.y; y <= b.h; y++ { + for x := b.x; x < b.w; x++ { + //draw vertical line separator + if b.x > 0 && x == b.x { + screen.SetContent(x, y, '|', nil, separatorStyle) + } else { + screen.SetContent(x, y, ' ', nil, b.style) + } + } + } + + if b.text.buf.Len() > 0 { + b.opos = b.scrollpos // keep track of last visible char/overflow + + for i := b.scrollpos; i < b.text.buf.Len(); i++ { + // line wrap + if x == b.x+b.w { + y += 1 + x = b.x + } + + // jump past separator if needed + // if b.x > 0 && x == b.x { + // x++ + // } + + // default style + style := b.style + + // highlight cursor + if i == b.text.cursorpos && b.focused { + style = cursorStyle + } + + // draw byte from buffer + c, err := b.text.buf.ByteAt(i) + if err != nil { + screen.SetContent(x, y, '?', nil, style) + printMsg("index out of range [%d]\n", i) + break + } + switch c { + case '\n': // linebreak + screen.SetContent(x, y, '\n', nil, style) + for j := x + 1; j < b.w; j++ { + screen.SetContent(j, y, ' ', nil, defStyle) // fill rest of line with blanks + } + + y += 1 + if y >= b.y+b.h { + break + } + x = b.x + b.w + for x > b.x { + screen.SetContent(x, y, ' ', nil, b.style) + x-- + } + case '\t': // show tab in tabstop width + screen.SetContent(x, y, '\t', nil, style) + x++ + for j := 0; j < b.tabstop-1; j++ { + screen.SetContent(x, y, ' ', nil, b.style) + x++ + } + default: + screen.SetContent(x, y, rune(c), nil, style) + x += 1 + } + + b.opos++ // increment last visible char/overflow + + // stop at bottom of box + if y >= b.y+b.h { + break + } + } + } + + // remove visual cursor clutter and fill out last line + for w := b.w; w >= x; w-- { + screen.SetContent(w, y, ' ', nil, b.style) + } + + // show cursor on EOF + if b.text.cursorpos == b.text.buf.Len() && b.focused { + screen.SetContent(x, y, ' ', nil, cursorStyle) + } + + // //begin empty lines with tilde, if more than one + // if b.h > 1 { + // y++ + // for ; y <= b.y+b.h; y++ { + // screen.SetContent(0, y, '~', nil, defStyle) + // //remove visual trailing clutter on line below + // x = b.x + b.w + // for x > 0 { + // screen.SetContent(x, y, ' ', nil, defStyle) + // x-- + // } + // } + // } +} diff --git a/testfiles/window.txt b/testfiles/window.txt new file mode 100644 index 0000000..d76c579 --- /dev/null +++ b/testfiles/window.txt @@ -0,0 +1,304 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "io" + "os" + "unicode" + + "github.com/gdamore/tcell" + "github.com/pkg/errors" + "github.com/prodhe/poe/gapbuffer" +) + +type Window struct { + x, y, w, h int + body *View + tagline *View + file *File + focused bool + visible bool +} + +func NewWindow(fn string) *Window { + win := &Window{ + body: &View{text: &Text{buf: gapbuffer.New()}}, + tagline: &View{text: &Text{buf: gapbuffer.New()}}, + file: &File{name: fn}, + } + win.body.SetStyle(defStyle) + win.tagline.SetStyle(tagStyle) + win.body.tabstop = 4 + win.visible = true + + return win +} + +func (win *Window) LoadBuffer() { + if win.file.read { + return + } + fh, err := os.OpenFile(win.file.name, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + panic(err) + } + defer fh.Close() + + if _, err := io.Copy(win.body.text.buf, fh); err != nil { + panic(err) + } + + fh.Seek(0, 0) + + h := sha256.New() + if _, err := io.Copy(h, fh); err != nil { + panic(err) + } + win.file.sha256 = fmt.Sprintf("%x", h.Sum(nil)) + + info, err := fh.Stat() + if err != nil { + panic(err) + } + win.file.mtime = info.ModTime() + + win.file.read = true + + win.body.text.SetCursor(0, 0) + +} + +// SaveFile replaces disk file with buffer content. Returns error if no disk file is set. +func (win *Window) SaveFile() (int, error) { + if win.file.name == "" { + return 0, errors.New("no filename") + } + + f, err := os.OpenFile(win.file.name, os.O_RDWR|os.O_CREATE, 0644) + if err != 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)) + + if 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 { + return win.file.name +} + +// Flags returns the 3 byte tagline flags for the window. +// +// 0: modified, ' or blank +// 1: visible, + or - +// 2: focused, . or blank +func (win *Window) Flags() [3]byte { + flags := [3]byte{' ', '+', ' '} + if win.body.dirty { + flags[0] = '\'' + } + if !win.visible { + flags[1] = '-' + } + if win.focused { + flags[2] = '.' + } + 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, win.y, win.w, 1) + win.body.Resize(win.x, win.y+1, win.w, win.h-2) +} + +// Insert inserts the given byte into the buffer. +func (win *Window) Insert(b byte) { + if _, err := win.body.Write([]byte{b}); err != nil { + printMsg("insertion error: %s", err) + } +} + +// Remove removes a byte from the buffer. +func (win *Window) Delete() { + if _, err := win.body.Delete(); err != nil { + printMsg("deletion error: %s", err) + } +} + +func (win *Window) SetFocus(flag bool) { + win.focused = flag + win.body.focused = flag + win.tagline.focused = false +} + +func (win *Window) HandleEvent(ev tcell.Event) { + switch ev := ev.(type) { + case *tcell.EventKey: + key := ev.Key() + switch key { + case tcell.KeyCR: + key = tcell.KeyLF + case tcell.KeyRight: + win.body.SetCursor(1, 1) + return + case tcell.KeyLeft: + win.body.SetCursor(1, -1) + return + case tcell.KeyDown: + fallthrough + case tcell.KeyPgDn: + _, _, _, h := win.body.Size() + win.body.Scroll(h / 3) + return + case tcell.KeyUp: + fallthrough + case tcell.KeyPgUp: + _, _, _, h := win.body.Size() + win.body.Scroll(-(h / 3)) + return + case tcell.KeyCtrlA: // line start + offset := win.body.text.PrevNL(win.body.text.cursorpos) + if offset > 1 && win.body.text.cursorpos-offset != 0 { + offset -= 1 + } + c, _ := win.body.text.buf.ByteAt(win.body.text.cursorpos - offset) + if c == '\n' && offset > 1 { + offset -= 1 + } + win.body.text.SetCursor(offset, -1) + return + case tcell.KeyCtrlE: // line end + if win.body.Byte() == '\n' { + win.body.text.SetCursor(1, 1) + return + } + offset := win.body.text.NextNL(win.body.text.cursorpos) + win.body.text.SetCursor(offset, 1) + return + case tcell.KeyCtrlU: // delete line backwards + offset := win.body.text.PrevNL(win.body.text.cursorpos) + if offset > 1 && win.body.text.cursorpos-offset != 0 { + offset -= 1 + } + c, _ := win.body.text.buf.ByteAt(win.body.text.cursorpos - offset) + if c == '\n' && offset > 1 { + offset -= 1 + } + for offset > 0 { + win.Delete() + offset-- + } + return + case tcell.KeyCtrlW: // delete word backwards + offset := win.body.text.cursorpos - 1 + c, _ := win.body.text.buf.ByteAt(offset) + if unicode.IsSpace(rune(c)) { + if c == '\n' { + win.Delete() + return + } + for unicode.IsSpace(rune(c)) && c != '\n' { + win.Delete() + if win.body.text.cursorpos <= 0 { + break + } + offset-- + c, _ = win.body.text.buf.ByteAt(offset) + } + } + for !unicode.IsSpace(rune(c)) { + win.Delete() + if win.body.text.cursorpos <= 0 { + break + } + offset-- + c, _ = win.body.text.buf.ByteAt(offset) + } + return + case tcell.KeyCtrlSpace: + return + case tcell.KeyCtrlS: // save + _, err := win.SaveFile() + if err != nil { + printMsg("%s\n", err) + return + } + return + case tcell.KeyCtrlG: // file info/statistics + printMsg("[0x%.4x %q] [gbuf: %d/%d cap: %d] [cursor: %d overflow: %d] [scroll: %d]\n%v", win.body.Byte(), + win.body.Byte(), + win.body.text.buf.Pos(), + win.body.text.buf.Len(), + win.body.text.buf.Cap(), + win.body.text.cursorpos, + win.body.Overflow(), + win.body.scrollpos, + win.body.text.history) + return + case tcell.KeyCtrlZ: + win.body.text.Undo() + return + case tcell.KeyCtrlY: + win.body.text.Redo() + return + case tcell.KeyBackspace2: + fallthrough + case tcell.KeyCtrlH: + win.Delete() + return + default: + // continue + } + + // insert + if key == tcell.KeyRune { + win.Insert(byte(ev.Rune())) + } else { + win.Insert(byte(key)) + } + } +} + +func AllWindows() []*Window { + var ws []*Window + for _, col := range cols { + ws = append(ws, col.windows...) + } + return ws +} + +func CurWin() *Window { + return AllWindows()[curWin] +} |