diff options
author | Petter Rodhelind <petter.rodhelind@gmail.com> | 2018-02-26 23:51:03 +0100 |
---|---|---|
committer | Petter Rodhelind <petter.rodhelind@gmail.com> | 2018-02-26 23:51:03 +0100 |
commit | 2395485d075f80117fe3ce25ef339bb1ffecf160 (patch) | |
tree | 87cabebd5ba1c7210adb5dabe253310f17694931 /ui/tcell | |
parent | 0023e0929ac7075cd008e0093de58ddc89efd597 (diff) | |
download | poe-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.go | 170 | ||||
-rw-r--r-- | ui/tcell/style.go | 53 | ||||
-rw-r--r-- | ui/tcell/tcell.go | 258 | ||||
-rw-r--r-- | ui/tcell/view.go | 649 | ||||
-rw-r--r-- | ui/tcell/window.go | 157 |
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) +} |