aboutsummaryrefslogtreecommitdiff
path: root/ui/tcell
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/tcell
parent0023e0929ac7075cd008e0093de58ddc89efd597 (diff)
downloadpoe-2395485d075f80117fe3ce25ef339bb1ffecf160.tar.gz
poe-2395485d075f80117fe3ce25ef339bb1ffecf160.tar.bz2
poe-2395485d075f80117fe3ce25ef339bb1ffecf160.zip
Total redesign. Separating editor parts from UI.
Diffstat (limited to 'ui/tcell')
-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
5 files changed, 1287 insertions, 0 deletions
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)
+}