aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorPetter Rodhelind <petter.rodhelind@gmail.com>2018-02-26 23:51:03 +0100
committerPetter Rodhelind <petter.rodhelind@gmail.com>2018-02-26 23:51:03 +0100
commit2395485d075f80117fe3ce25ef339bb1ffecf160 (patch)
tree87cabebd5ba1c7210adb5dabe253310f17694931 /ui
parent0023e0929ac7075cd008e0093de58ddc89efd597 (diff)
downloadpoe-2395485d075f80117fe3ce25ef339bb1ffecf160.tar.gz
poe-2395485d075f80117fe3ce25ef339bb1ffecf160.tar.bz2
poe-2395485d075f80117fe3ce25ef339bb1ffecf160.zip
Total redesign. Separating editor parts from UI.
Diffstat (limited to 'ui')
-rw-r--r--ui/cli.go55
-rw-r--r--ui/tcell/layout.go170
-rw-r--r--ui/tcell/style.go53
-rw-r--r--ui/tcell/tcell.go258
-rw-r--r--ui/tcell/view.go649
-rw-r--r--ui/tcell/window.go157
-rw-r--r--ui/ui.go36
7 files changed, 1378 insertions, 0 deletions
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/ui/tcell/layout.go b/ui/tcell/layout.go
new file mode 100644
index 0000000..599fb6a
--- /dev/null
+++ b/ui/tcell/layout.go
@@ -0,0 +1,170 @@
+package uitcell
+
+type Workspace struct {
+ x, y, w, h int
+ cols []*Column
+}
+
+type Column struct {
+ x, y, w, h int
+ windows []*Window
+}
+
+// Add adds a new column and resizes.
+func (wrk *Workspace) AddCol() {
+ nx, ny := wrk.x, wrk.y
+ nw, nh := wrk.w, wrk.h
+
+ if len(wrk.cols) > 0 {
+ nx = wrk.cols[len(wrk.cols)-1].x + wrk.cols[len(wrk.cols)-1].w/2
+ nw = wrk.cols[len(wrk.cols)-1].w / 2
+ wrk.cols[len(wrk.cols)-1].w /= 2
+ }
+
+ newcol := &Column{nx, ny, nw, nh, nil}
+ wrk.cols = append(wrk.cols, newcol)
+
+ wrk.Resize(wrk.x, wrk.y, wrk.w, wrk.h) // for re-arranging side effects
+}
+
+func (wrk *Workspace) CloseCol(c *Column) {
+ var j int
+ for _, col := range wrk.cols {
+ if col != c {
+ wrk.cols[j] = col
+ j++
+ }
+ }
+ wrk.cols = wrk.cols[:j]
+
+ wrk.Resize(wrk.x, wrk.y, wrk.w, wrk.h) // for re-arranging side effects
+}
+
+func (wrk *Workspace) Col(i int) *Column {
+ return wrk.cols[i]
+}
+
+func (wrk *Workspace) LastCol() *Column {
+ return wrk.cols[len(wrk.cols)-1]
+}
+
+func (wrk *Workspace) Resize(x, y, w, h int) {
+ wrk.x, wrk.y, wrk.w, wrk.h = x, y, w, h
+
+ n := len(wrk.cols)
+ if n == 0 {
+ return
+ }
+
+ var remainder int
+ if n > 0 {
+ remainder = w % (n)
+ }
+ for i := range wrk.cols {
+ if i == 0 {
+ var firstvertline int
+ if n > 1 {
+ firstvertline = 1
+ }
+ wrk.cols[i].Resize(x, y, (w/n)+remainder-(n-1)-firstvertline, h)
+ continue
+ }
+ // +i-n-1 on x so we do not draw on last vert line of previous col
+ wrk.cols[i].Resize((w/n)*i+remainder+i-(n-1), y, (w/n)-1, h)
+ }
+}
+
+func (wrk *Workspace) Draw() {
+ for _, col := range wrk.cols {
+ col.Draw()
+
+ // draw vertical lines between cols
+ for x, y := col.x+col.w+1, wrk.y; y < wrk.y+wrk.h; y++ {
+ screen.SetContent(x, y, '|', nil, vertlineStyle)
+ }
+ }
+}
+
+func (c *Column) AddWindow(win *Window) {
+ win.col = c
+ c.windows = append(c.windows, win)
+ c.ResizeWindows()
+}
+
+func (c *Column) CloseWindow(w *Window) {
+ var j int
+ for _, win := range c.windows {
+ if win != w {
+ c.windows[j] = win
+ j++
+ }
+ }
+ c.windows = c.windows[:j]
+
+ // If we deleted the current window (probably), select another
+ if CurWin == w {
+ all := AllWindows()
+
+ // If we are out of windows in our own column, pick another or exit
+ if len(c.windows) > 0 {
+ CurWin = c.windows[j-1]
+ } else {
+ // remove column
+ workspace.CloseCol(c)
+
+ // clear clutter
+ screen.Clear()
+
+ // find another window to focus or exit
+ if len(all) > 0 {
+ CurWin = AllWindows()[0]
+
+ } else {
+ ed.Run("Exit")
+ }
+ }
+
+ // if the only win left is the message win, close all
+ if len(all) == 1 && CurWin.Name() == FnMessageWin {
+ ed.Run("Exit")
+ }
+ }
+
+ c.ResizeWindows()
+}
+
+func (c *Column) Resize(x, y, w, h int) {
+ c.x, c.y = x, y
+ c.w, c.h = w, h
+
+ c.ResizeWindows()
+}
+
+func (c *Column) ResizeWindows() {
+ var n int
+ for _, win := range c.windows {
+ if !win.hidden {
+ n++
+ }
+ }
+
+ var remainder int
+ if n > 0 {
+ 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)
+ }
+}
+
+func (c *Column) Draw() {
+ for _, win := range c.windows {
+ if !win.hidden {
+ win.Draw()
+ }
+ }
+}
diff --git a/ui/tcell/style.go b/ui/tcell/style.go
new file mode 100644
index 0000000..0dcb565
--- /dev/null
+++ b/ui/tcell/style.go
@@ -0,0 +1,53 @@
+package uitcell
+
+import "github.com/gdamore/tcell"
+
+var (
+ // body is the main editing buffer
+ bodyStyle tcell.Style
+ bodyCursorStyle tcell.Style
+ bodyHilightStyle tcell.Style
+
+ // tag is the window tag line above the body
+ tagStyle tcell.Style
+ tagCursorStyle tcell.Style
+ tagHilightStyle tcell.Style
+ tagSquareStyle tcell.Style
+ tagSquareModifiedStyle tcell.Style
+
+ // vertline is the vertical line separating columns
+ vertlineStyle tcell.Style
+
+ // unprintable rune
+ unprintableStyle tcell.Style
+)
+
+// initStyles initializes the different styles (colors for background/foreground).
+func initStyles() error {
+ bodyStyle = tcell.StyleDefault.
+ Background(tcell.NewHexColor(0xffffea)).
+ Foreground(tcell.ColorBlack)
+ bodyCursorStyle = bodyStyle.
+ Background(tcell.NewHexColor(0xeaea9e))
+ bodyHilightStyle = bodyStyle.
+ Background(tcell.NewHexColor(0xa6a65a))
+ unprintableStyle = bodyStyle.
+ Foreground(tcell.ColorRed)
+
+ tagStyle = tcell.StyleDefault.
+ Background(tcell.NewHexColor(0xeaffff)).
+ Foreground(tcell.ColorBlack)
+ tagCursorStyle = tagStyle.
+ Background(tcell.NewHexColor(0x8888cc)).
+ Foreground(tcell.ColorBlack)
+ tagHilightStyle = tagStyle.
+ Background(tcell.NewHexColor(0x8888cc))
+ tagSquareStyle = tagStyle.
+ Background(tcell.NewHexColor(0x8888cc))
+ tagSquareModifiedStyle = tagStyle.
+ 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/ui/tcell/view.go b/ui/tcell/view.go
new file mode 100644
index 0000000..ef42207
--- /dev/null
+++ b/ui/tcell/view.go
@@ -0,0 +1,649 @@
+package uitcell
+
+import (
+ "io"
+ "path/filepath"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/atotto/clipboard"
+ "github.com/gdamore/tcell"
+ runewidth "github.com/mattn/go-runewidth"
+ "github.com/prodhe/poe/editor"
+)
+
+const (
+ ViewMenu int = iota
+ ViewTagline
+ ViewBody
+ ViewScratch
+)
+
+const ClickThreshold = 500 // in milliseconds to count as double click
+
+type View struct {
+ x, y, w, h int
+ style tcell.Style
+ cursorStyle tcell.Style
+ hilightStyle tcell.Style
+ text *editor.Buffer
+ scrollpos int // bytes to skip when drawing content
+ opos int // overflow offset
+ tabstop int
+ focused bool
+ what int
+ dirty bool // modified since last read/write
+ mclicktime time.Time // last mouse click in time
+ mclickpos int // byte offset accounting for runes
+ mpressed bool
+}
+
+func (v *View) Write(p []byte) (int, error) {
+ n, err := v.text.Write(p)
+ if err != nil {
+ return 0, err
+ }
+ if v.what == ViewBody {
+ v.dirty = true
+ }
+ v.SetCursor(0, 1) // force scroll if needed
+ return n, err
+}
+
+func (v *View) Delete() (int, error) {
+ // Do not allow deletion beyond what we can see.
+ // This forces the user to scroll to visible content.
+ q0, _ := v.text.Dot()
+ if q0 == v.scrollpos && len(v.text.ReadDot()) == 0 {
+ return 0, nil //silent return
+ }
+ n, err := v.text.Delete()
+ if err != nil {
+ return n, err
+ }
+ if v.what == ViewBody {
+ v.dirty = true
+ }
+ v.SetCursor(0, 1) // force scroll
+ return n, nil
+}
+
+func (v *View) Resize(x, y, w, h int) {
+ v.x, v.y, v.w, v.h = x, y, w, h
+}
+
+// Byte returns the current byte at start of cursor.
+func (v *View) Rune() rune {
+ r, _, _ := v.text.ReadRuneAt(v.Cursor())
+ return r
+}
+
+func (v *View) Cursor() int {
+ q0, _ := v.text.Dot()
+ return q0
+}
+
+func (v *View) SetCursor(pos, whence int) {
+ v.text.SeekDot(pos, whence)
+
+ // scroll to cursor if out of screen
+ if v.Cursor() < v.scrollpos || v.Cursor() > v.opos {
+ if v.Cursor() != v.text.Len() { // do not autoscroll on +1 last byte
+ v.ScrollTo(v.Cursor())
+ }
+ }
+}
+
+// XYToOffset translates mouse coordinates in a 2D terminal to the correct byte offset in buffer, accounting for rune length, width and tabstops.
+func (v *View) XYToOffset(x, y int) int {
+ offset := v.scrollpos
+
+ // vertical (number of visual lines)
+ for y-v.y > 0 {
+ r, _, err := v.text.ReadRuneAt(offset)
+ if err != nil {
+ if err == io.EOF {
+ return v.text.Len()
+ }
+ printMsg("%s\n", err)
+ return 0
+ }
+ if r == '\n' {
+ offset++
+ y--
+ continue
+ }
+ // loop until next line, either new line or soft wrap at end of window width
+ xw := v.x
+ for r != '\n' && xw <= v.x+v.w {
+ var n int
+ r, n, _ = v.text.ReadRuneAt(offset)
+ offset += n
+ rw := RuneWidth(r)
+ if r == '\t' {
+ rw = v.tabstop - (xw-v.x)%v.tabstop
+ } else if rw == 0 {
+ rw = 1
+ }
+
+ xw += rw
+ }
+ y--
+ }
+
+ // horizontal
+ xw := v.x // for tabstop count
+ for x-v.x > 0 {
+ r, n, err := v.text.ReadRuneAt(offset)
+ if err != nil {
+ if err == io.EOF {
+ return v.text.Len()
+ }
+ printMsg("%s\n", err)
+ return 0
+ }
+ if r == '\n' {
+ break
+ }
+ offset += n
+ rw := RuneWidth(r)
+ if r == '\t' {
+ rw = v.tabstop - (xw-v.x)%v.tabstop
+ } else if rw == 0 {
+ rw = 1
+ }
+ xw += rw // keep track of tabstop modulo
+ x -= rw
+ }
+
+ return offset
+}
+
+// Scroll will move the visible part of the buffer in number of lines, accounting for soft wraps and tabstops. Negative means upwards.
+func (v *View) Scroll(n int) {
+ offset := 0
+
+ xw := v.x // for tabstop count and soft wrap
+ switch {
+ case n > 0: // downwards, next line
+ for n > 0 {
+ r, size, err := v.text.ReadRuneAt(v.scrollpos + offset)
+ if err != nil {
+ v.scrollpos = v.text.Len()
+ if err == io.EOF {
+ break // hit EOF, stop scrolling
+ }
+ return
+ }
+ offset += size
+
+ rw := RuneWidth(r)
+ if r == '\t' {
+ rw = v.tabstop - (xw-v.x)%v.tabstop
+ } else if rw == 0 {
+ rw = 1
+ }
+ xw += rw
+
+ if r == '\n' || xw > v.x+v.w { // new line or soft wrap
+ n-- // move down
+ xw = v.x // reset soft wrap
+ }
+ }
+ v.scrollpos += offset
+ case n < 0: // upwards, previous line
+ // This is kind of ugly, but it relies on the soft wrap
+ // counting in positive scrolling. It will scroll back to the
+ // nearest new line character and then scroll forward again
+ // until the very last iteration, which is the offset for the previous
+ // softwrap/nl.
+ for n < 0 {
+ start := v.scrollpos // save current offset
+ v.scrollpos -= v.text.PrevDelim('\n', v.scrollpos) // scroll back
+ if start-v.scrollpos == 1 { // if it was just a new line, back up one more
+ v.scrollpos -= v.text.PrevDelim('\n', v.scrollpos)
+ }
+ prevlineoffset := v.scrollpos // previous (or one more) new line, may be way back
+
+ for v.scrollpos < start { // scroll one line forward until we're back at current
+ prevlineoffset = v.scrollpos // save offset just before we jump forward again
+ v.Scroll(1) // used for the side effect of setting v.scrollpos
+ }
+ v.scrollpos = prevlineoffset
+ n++
+ }
+ }
+
+ // boundaries
+ if v.scrollpos < 0 {
+ v.scrollpos = 0
+ }
+ if v.scrollpos > v.text.Len() {
+ v.scrollpos = v.text.Len()
+ }
+}
+
+// ScrollTo will scroll to an absolute byte offset in the buffer and backwards to the nearest previous newline.
+func (v *View) ScrollTo(offset int) {
+ offset -= v.text.PrevDelim('\n', offset)
+ if offset > 0 {
+ offset += 1
+ }
+ v.scrollpos = offset
+ v.Scroll(-(v.h / 3)) // scroll a third page more for context
+}
+
+func (b *View) Draw() {
+ x, y := b.x, b.y
+
+ if b.text.Len() > 0 {
+ b.opos = b.scrollpos // keep track of last visible char/overflow
+ b.text.Seek(b.scrollpos, io.SeekStart)
+ for i := b.scrollpos; i < b.text.Len(); { // i gets incremented after reading of the rune, to know how many bytes we need to skip
+ // line wrap
+ if x > b.x+b.w {
+ y += 1
+ x = b.x
+ }
+
+ // stop at visual bottom of view
+ if y >= b.y+b.h {
+ break
+ }
+
+ // default style
+ style := b.style
+
+ // highlight cursor
+ q0, q1 := b.text.Dot()
+ if (q0 == q1 && i == q0) && b.focused {
+ style = b.cursorStyle
+ }
+
+ // highlight selection
+ if (i >= q0 && i < q1) && b.focused {
+ style = b.hilightStyle
+ }
+
+ // draw rune from buffer
+ r, n, err := b.text.ReadRune()
+ if err != nil {
+ screen.SetContent(x, y, '?', nil, style)
+ printMsg("rune [%d]: %s\n", i, err)
+ break
+ }
+ b.opos += n // increment last visible char/overflow
+ i += n // jump past bytes for next run
+
+ // color the entire line if we are in selection
+ fillstyle := b.style
+ if style == b.hilightStyle {
+ fillstyle = b.hilightStyle
+ }
+
+ switch r {
+ case '\n': // linebreak
+ screen.SetContent(x, y, '\n', nil, style)
+ for j := x + 1; j <= b.x+b.w; j++ {
+ screen.SetContent(j, y, ' ', nil, fillstyle) // fill rest of line
+ }
+ y += 1
+ x = b.x
+ case '\t': // show tab until next even tabstop width
+ screen.SetContent(x, y, '\t', nil, style)
+ x++
+ for (x-b.x)%b.tabstop != 0 {
+ screen.SetContent(x, y, ' ', nil, fillstyle)
+ x++
+ }
+ default: // print rune
+ screen.SetContent(x, y, r, nil, style)
+ rw := RuneWidth(r)
+ if rw == 2 { // wide runes
+ screen.SetContent(x+1, y, ' ', nil, fillstyle)
+ }
+ if rw == 0 { // control characters
+ rw = 1
+ screen.SetContent(x, y, RuneWidthZero, nil, unprintableStyle)
+ }
+ x += rw
+ }
+ }
+ }
+
+ if b.opos != b.text.Len() {
+ b.opos--
+ }
+
+ // fill out last line if we did not end on a newline
+ //c, _ := b.text.buf.ByteAt(b.opos)
+ c := b.text.LastRune()
+ if c != '\n' && y < b.y+b.h {
+ for w := b.x + b.w; w >= x; w-- {
+ screen.SetContent(w, y, ' ', nil, b.style)
+ }
+ }
+
+ // show cursor on EOF
+ q0, _ := b.text.Dot()
+ if q0 == b.text.Len() && b.focused {
+ if x > b.x+b.w {
+ x = b.x
+ y++
+ }
+ if y < b.y+b.h {
+ screen.SetContent(x, y, ' ', nil, b.cursorStyle)
+ x++
+ }
+ }
+
+ // clear the rest and optionally show a special char as empty line
+ for w := b.x + b.w; w >= x; w-- {
+ screen.SetContent(w, y, ' ', nil, b.style)
+ }
+ y++
+ if y < b.y+b.h {
+ for ; y < b.y+b.h; y++ {
+ screen.SetContent(b.x, y, ' ', nil, b.style) // special char
+ x = b.x + b.w
+ for x >= b.x+1 {
+ screen.SetContent(x, y, ' ', nil, bodyStyle)
+ x--
+ }
+ }
+ }
+}
+
+func (v *View) HandleEvent(ev tcell.Event) {
+ switch ev := ev.(type) {
+ case *tcell.EventMouse:
+ mx, my := ev.Position()
+
+ switch btn := ev.Buttons(); btn {
+ case tcell.ButtonNone: // on button release
+ if v.mpressed {
+ v.mpressed = false
+ }
+ case tcell.Button1:
+ pos := v.XYToOffset(mx, my)
+ if v.mpressed { // select text via click-n-drag
+ if pos > v.mclickpos {
+ v.text.SetDot(v.mclickpos, pos)
+ } else {
+ // switch q0 and q1
+ v.text.SetDot(pos, v.mclickpos)
+ }
+ return
+ }
+
+ v.mpressed = true
+ v.mclickpos = pos
+
+ if ev.Modifiers()&tcell.ModAlt != 0 { // identic code to Btn2
+ pos := v.XYToOffset(mx, my)
+ // if we clicked inside a current selection, run that one
+ q0, q1 := v.text.Dot()
+ if pos >= q0 && pos <= q1 && q0 != q1 {
+ ed.Run(v.text.ReadDot())
+ return
+ }
+
+ // otherwise, select non-space chars under mouse and run that
+ p := pos - v.text.PrevSpace(pos)
+ n := pos + v.text.NextSpace(pos)
+ v.text.SetDot(p, n)
+ fn := strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.text.SetDot(q0, q1)
+ ed.Run(fn)
+ return
+ }
+
+ if ev.Modifiers()&tcell.ModShift != 0 { // identic code to Btn3
+ pos := v.XYToOffset(mx, my)
+ // if we clicked inside a current selection, open that one
+ q0, q1 := v.text.Dot()
+ if pos >= q0 && pos <= q1 && q0 != q1 {
+ CmdOpen(v.text.ReadDot())
+ return
+ }
+
+ // otherwise, select everything inside surround spaces and open that
+ p := pos - v.text.PrevSpace(pos)
+ n := pos + v.text.NextSpace(pos)
+ v.text.SetDot(p, n)
+ fn := strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.text.SetDot(q0, q1)
+ if fn == "" { // if it is still blank, abort
+ return
+ }
+ if fn != "" && fn[0] != filepath.Separator {
+ fn = CurWin.Dir() + string(filepath.Separator) + fn
+ fn = filepath.Clean(fn)
+ }
+ CmdOpen(fn)
+ return
+ }
+
+ elapsed := ev.When().Sub(v.mclicktime) / time.Millisecond
+
+ if elapsed < ClickThreshold {
+ // double click
+ v.text.Select(pos)
+ } else {
+ // single click
+ v.SetCursor(pos, 0)
+ //screen.ShowCursor(3, 3)
+ }
+ v.mclicktime = ev.When()
+ case tcell.WheelUp: // scrollup
+ v.Scroll(-1)
+ case tcell.WheelDown: // scrolldown
+ v.Scroll(1)
+ case tcell.Button2: // middle click
+ pos := v.XYToOffset(mx, my)
+ // if we clicked inside a current selection, run that one
+ q0, q1 := v.text.Dot()
+ if pos >= q0 && pos <= q1 && q0 != q1 {
+ ed.Run(v.text.ReadDot())
+ return
+ }
+
+ // otherwise, select non-space chars under mouse and run that
+ p := pos - v.text.PrevSpace(pos)
+ n := pos + v.text.NextSpace(pos)
+ v.text.SetDot(p, n)
+ fn := strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.text.SetDot(q0, q1)
+ ed.Run(fn)
+ return
+ case tcell.Button3: // right click
+ pos := v.XYToOffset(mx, my)
+ // if we clicked inside a current selection, open that one
+ q0, q1 := v.text.Dot()
+ if pos >= q0 && pos <= q1 && q0 != q1 {
+ CmdOpen(v.text.ReadDot())
+ return
+ }
+
+ // otherwise, select everything inside surround spaces and open that
+ p := pos - v.text.PrevSpace(pos)
+ n := pos + v.text.NextSpace(pos)
+ v.text.SetDot(p, n)
+ fn := strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.text.SetDot(q0, q1)
+ if fn == "" { // if it is still blank, abort
+ return
+ }
+ if fn != "" && fn[0] != filepath.Separator {
+ fn = CurWin.Dir() + string(filepath.Separator) + fn
+ fn = filepath.Clean(fn)
+ }
+ CmdOpen(fn)
+ return
+ default:
+ printMsg("%#v", btn)
+ }
+ case *tcell.EventKey:
+ key := ev.Key()
+ switch key {
+ case tcell.KeyCR: // use unix style 0x0A (\n) for new lines
+ key = tcell.KeyLF
+ case tcell.KeyRight:
+ _, q1 := v.text.Dot()
+ v.SetCursor(q1, io.SeekStart)
+ v.SetCursor(utf8.RuneLen(v.Rune()), io.SeekCurrent)
+ return
+ case tcell.KeyLeft:
+ v.SetCursor(-1, io.SeekCurrent)
+ return
+ case tcell.KeyDown:
+ fallthrough
+ case tcell.KeyPgDn:
+ v.Scroll(v.h / 3)
+ return
+ case tcell.KeyUp:
+ fallthrough
+ case tcell.KeyPgUp:
+ v.Scroll(-(v.h / 3))
+ return
+ case tcell.KeyCtrlA: // line start
+ offset := v.text.PrevDelim('\n', v.Cursor())
+ if offset > 1 && v.Cursor()-offset != 0 {
+ offset -= 1
+ }
+ v.SetCursor(-offset, io.SeekCurrent)
+ return
+ case tcell.KeyCtrlE: // line end
+ if v.Rune() == '\n' {
+ v.SetCursor(1, io.SeekCurrent)
+ return
+ }
+ offset := v.text.NextDelim('\n', v.Cursor())
+ v.SetCursor(offset, io.SeekCurrent)
+ return
+ case tcell.KeyCtrlU: // delete line backwards
+ if v.text.ReadDot() != "" {
+ v.Delete() // delete current selection first
+ }
+ offset := v.text.PrevDelim('\n', v.Cursor())
+ if offset > 1 && v.Cursor()-offset != 0 {
+ offset -= 1
+ }
+ r, _, _ := v.text.ReadRuneAt(v.Cursor() - offset)
+ if r == '\n' && offset > 1 {
+ offset -= 1
+ }
+ v.text.SetDot(v.Cursor()-offset, v.Cursor())
+ v.Delete()
+ return
+ case tcell.KeyCtrlW: // delete word backwards
+ if v.text.ReadDot() != "" {
+ v.Delete() // delete current selection first
+ }
+ startpos := v.Cursor()
+ offset := v.text.PrevWord(v.Cursor())
+ if offset == 0 {
+ v.SetCursor(-1, io.SeekCurrent)
+ offset = v.text.PrevWord(v.Cursor())
+ }
+ v.text.SetDot(v.Cursor()-offset, startpos)
+ v.Delete()
+ return
+ case tcell.KeyCtrlZ:
+ v.text.Undo()
+ return
+ case tcell.KeyCtrlY:
+ v.text.Redo()
+ return
+ case tcell.KeyBackspace2: // delete
+ fallthrough
+ case tcell.KeyCtrlH:
+ v.Delete()
+ return
+ case tcell.KeyCtrlG: // file info/statistics
+ printMsg("0x%.4x %q len %d\nbasedir: %s\nwindir: %s\nname: %s\n",
+ v.Rune(), v.Rune(),
+ v.text.Len(),
+ ed.WorkDir(), CurWin.Dir(), CurWin.Name())
+ return
+ case tcell.KeyCtrlO: // open file/dir
+ fn := v.text.ReadDot()
+ if fn == "" { // select all non-space characters
+ curpos := v.Cursor()
+ p := curpos - v.text.PrevSpace(curpos)
+ n := curpos + v.text.NextSpace(curpos)
+ v.text.SetDot(p, n)
+ fn = strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.SetCursor(curpos, io.SeekStart)
+ if fn == "" { // if it is still blank, abort
+ return
+ }
+ }
+ if fn != "" && fn[0] != filepath.Separator {
+ fn = CurWin.Dir() + string(filepath.Separator) + fn
+ fn = filepath.Clean(fn)
+ }
+ CmdOpen(fn)
+ return
+ case tcell.KeyCtrlR: // run command in dot
+ cmd := v.text.ReadDot()
+ if cmd == "" { // select all non-space characters
+ curpos := v.Cursor()
+ p := curpos - v.text.PrevSpace(curpos)
+ n := curpos + v.text.NextSpace(curpos)
+ v.text.SetDot(p, n)
+ cmd = strings.Trim(v.text.ReadDot(), "\n\t ")
+ v.SetCursor(curpos, io.SeekStart)
+ if cmd == "" { // if it is still blank, abort
+ return
+ }
+ }
+ ed.Run(cmd)
+ return
+ case tcell.KeyCtrlC: // copy to clipboard
+ str := v.text.ReadDot()
+ if str == "" {
+ return
+ }
+ if err := clipboard.WriteAll(str); err != nil {
+ printMsg("%s\n", err)
+ }
+ return
+ case tcell.KeyCtrlV: // paste from clipboard
+ s, err := clipboard.ReadAll()
+ if err != nil {
+ printMsg("%s\n", err)
+ return
+ }
+ v.text.Write([]byte(s))
+ return
+ case tcell.KeyCtrlQ:
+ // close entire application if we are in the top menu
+ if v.what == ViewMenu {
+ CmdExit()
+ return
+ }
+ // otherwise, just close this window (CurWin)
+ CmdDel()
+ return
+ default:
+ // insert
+ }
+
+ // insert if no early return
+ if key == tcell.KeyRune {
+ v.Write([]byte(string(ev.Rune())))
+ } else {
+ v.Write([]byte{byte(key)})
+ }
+ }
+}
+
+func RuneWidth(r rune) int {
+ rw := runewidth.RuneWidth(r)
+ if r == '⌘' {
+ rw = 2
+ }
+ return rw
+}
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{}
+}