aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--commands.go12
-rw-r--r--style.go12
-rw-r--r--text.go9
-rw-r--r--todo13
-rw-r--r--view.go176
-rw-r--r--window.go20
6 files changed, 182 insertions, 60 deletions
diff --git a/commands.go b/commands.go
index 023a112..9d31614 100644
--- a/commands.go
+++ b/commands.go
@@ -37,15 +37,11 @@ func RunCommand(input string) {
// Edit shortcuts for external commands and piping
switch input[0] {
- case '!':
- fallthrough
- case '<':
- fallthrough
- case '>':
- fallthrough
- case '|':
+ case '!', '<', '>', '|':
CmdEdit(input)
}
+
+ CmdEdit("!" + input)
}
func CmdExit(args string) {
@@ -116,7 +112,7 @@ func CmdEdit(args string) {
// if command produced output, print it
outstr := string(out)
if outstr != "" {
- printMsg("%s\n", outstr)
+ printMsg("%s", outstr)
}
default:
printMsg("?\n")
diff --git a/style.go b/style.go
index 57debbf..eaff897 100644
--- a/style.go
+++ b/style.go
@@ -9,9 +9,11 @@ var (
bodyHilightStyle tcell.Style
// tag is the window tag line above the body
- tagStyle tcell.Style
- tagCursorStyle tcell.Style
- tagHilightStyle tcell.Style
+ 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
@@ -40,6 +42,10 @@ func InitStyles() {
Foreground(tcell.ColorBlack)
tagHilightStyle = tagStyle.
Background(tcell.NewHexColor(0x8888cc))
+ tagSquareStyle = tagStyle.
+ Background(tcell.NewHexColor(0x8888cc))
+ tagSquareModifiedStyle = tagStyle.
+ Background(tcell.NewHexColor(0x0))
vertlineStyle = bodyStyle
}
diff --git a/text.go b/text.go
index 5b50be9..b9398ed 100644
--- a/text.go
+++ b/text.go
@@ -74,6 +74,11 @@ func (t *Text) Len() int {
return t.buf.Len()
}
+// String returns the entire text buffer as a string.
+func (t *Text) String() string {
+ return string(t.buf.Bytes())
+}
+
// ReadRune reads a rune from buffer and advances the internal offset. This could be called in sequence to get all runes from buffer. This populates LastRune().
func (t *Text) ReadRune() (r rune, size int, err error) {
r, size, err = t.ReadRuneAt(t.off)
@@ -261,6 +266,10 @@ func (t *Text) Select(offset int) {
offset, _ = t.Seek(offset, io.SeekStart)
start, end := offset, offset
+ // space
+ //start -= t.PrevSpace(start)
+ //end += t.NextSpace(end)
+
// word
start -= t.PrevWord(start)
end += t.NextWord(end)
diff --git a/todo b/todo
index b79f2f5..bffdcd4 100644
--- a/todo
+++ b/todo
@@ -1,15 +1,13 @@
jobs / commands
- async extern exekvering via !<>|
+ async execution via !<>|
file
- spara tom buffer med nytt filnamn
+ save empty buffer with tag name
check for overwrite
overwrite when hash-verify fails on save
backup files on disk
window
- hide
- command Get
-view
- scroll must account for soft wraps
+ hide / collapse
+ command Get (update buffer content)
text
int64 as default in case of large files
undvika in-ram buffer - swapfiles?
@@ -18,6 +16,7 @@ text
undo
unlimited for real (undo undo)
cache write Change{} until next action
+ refactor all Next/Prev-funcs
---
visual
calculate view percentage on screen
@@ -29,4 +28,4 @@ visual
another style for top menu
do not change CurWin on mousepressed
scroll on mpressed at bottom line
- unprintableChar styling \ No newline at end of file
+ unprintableChar styling
diff --git a/view.go b/view.go
index d0e6bba..7bf1860 100644
--- a/view.go
+++ b/view.go
@@ -3,6 +3,7 @@ package main
import (
"io"
"path/filepath"
+ "strings"
"time"
"unicode/utf8"
@@ -92,79 +93,106 @@ func (v *View) SetCursor(pos, whence int) {
}
}
-// XYToOffset translates mouse coordinates in a 2D terminal to the correct byte offset in buffer, accounting for rune length and width.
-func (b *View) XYToOffset(x, y int) int {
- offset := b.scrollpos
+// 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 lines)
- for y-b.y > 0 {
- r, _, _ := b.text.ReadRuneAt(offset)
+ // vertical (number of visual lines)
+ for y-v.y > 0 {
+ r, _, _ := v.text.ReadRuneAt(offset)
if r == '\n' {
offset++
y--
continue
}
- // loop until next line
- xw := 0
- for r != '\n' && xw < b.x+b.w {
+ // 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, _ = b.text.ReadRuneAt(offset)
+ r, n, _ = v.text.ReadRuneAt(offset)
offset += n
rw := RuneWidth(r)
- if rw == 0 {
+ if r == '\t' {
+ rw = v.tabstop - (xw-v.x)%v.tabstop
+ } else if rw == 0 {
rw = 1
}
+
xw += rw
}
y--
}
// horizontal
- for x-b.x > 0 {
- r, n, _ := b.text.ReadRuneAt(offset)
+ xw := v.x // for tabstop count
+ for x-v.x > 0 {
+ r, n, _ := v.text.ReadRuneAt(offset)
if r == '\n' {
break
}
- if r == '\t' {
- x -= b.tabstop - 1 // TODO: modulo count
- }
offset += n
rw := RuneWidth(r)
- if rw == 0 {
+ 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. Negative means upwards.
+// 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
- if n > 0 {
+
+ xw := v.x // for tabstop count and soft wrap
+ switch {
+ case n > 0: // downwards, next line
for n > 0 {
- if r, _, _ := v.text.ReadRuneAt(v.scrollpos + offset); r == '\n' {
- offset++
- } else {
- offset += v.text.NextDelim('\n', v.scrollpos+offset) + 1
+ r, size, err := v.text.ReadRuneAt(v.scrollpos + offset)
+ if err != nil {
+ break // hit EOF, stop scrolling
+ }
+ 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
}
- n--
}
v.scrollpos += offset
- }
-
- if n < 0 {
- offset += v.text.PrevDelim('\n', v.scrollpos)
+ 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 {
- offset += v.text.PrevDelim('\n', v.scrollpos-offset)
+ start := v.scrollpos // save current offset
+ v.scrollpos -= v.text.PrevDelim('\n', v.scrollpos) // scroll back
+ if start-v.scrollpos == 1 { // if it was an empty 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++
}
- if v.scrollpos-offset > 0 {
- v.scrollpos -= offset - 1
- } else {
- v.scrollpos = 0
- }
}
// boundaries
@@ -317,7 +345,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.mpressed = false
}
case tcell.Button1:
- if v.mpressed {
+ if v.mpressed { // select text via click-n-drag
if pos > v.mclickpos {
v.text.SetDot(v.mclickpos, pos)
} else {
@@ -348,6 +376,45 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.Scroll(-1)
case tcell.WheelDown: // scrolldown
v.Scroll(1)
+ case tcell.Button2: // middle click
+ // if we clicked inside a current selection, run that one
+ q0, q1 := v.text.Dot()
+ if pos >= q0 && pos <= q1 && q0 != q1 {
+ RunCommand(v.text.ReadDot())
+ 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)
+ RunCommand(fn)
+ return
+ case tcell.Button3: // right click
+ // 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)
}
@@ -390,6 +457,9 @@ func (v *View) HandleEvent(ev tcell.Event) {
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
@@ -402,6 +472,9 @@ func (v *View) HandleEvent(ev tcell.Event) {
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 {
@@ -423,23 +496,44 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.Delete()
return
case tcell.KeyCtrlG: // file info/statistics
- printMsg("0x%.4x %q %d,%d/%d\noverflow: %d scroll: %d\ndot: %q\nprev nl: %d\n",
+ printMsg("0x%.4x %q %d,%d/%d\nbasedir: %s\nwindir: %s\n\nname: %s\nnameabs: %s\ntagname: %s\n",
v.Rune(), v.Rune(),
v.text.q0, v.text.q1, v.text.Len(),
- v.opos, v.scrollpos,
- v.text.ReadDot(),
- v.text.PrevDelim('\n', v.Cursor()))
+ baseDir, CurWin.Dir(), CurWin.Name(), CurWin.NameAbs(), CurWin.NameFromTag())
return
case tcell.KeyCtrlO: // open file/dir
fn := v.text.ReadDot()
- if fn != FnEmptyWin && fn[0] != filepath.Separator {
+ 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
- RunCommand(v.text.ReadDot())
+ 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
+ }
+ }
+ RunCommand(cmd)
return
case tcell.KeyCtrlC: // copy to clipboard
if err := clipboard.WriteAll(v.text.ReadDot()); err != nil {
diff --git a/window.go b/window.go
index c3a5c72..9560ec6 100644
--- a/window.go
+++ b/window.go
@@ -68,7 +68,7 @@ func NewWindow(fn string) *Window {
file: &File{name: fnabs},
}
- fmt.Fprintf(win.tagline, "⌘ %s%s Del",
+ fmt.Fprintf(win.tagline, "%s%s Del ",
win.Flags(),
win.Name(),
)
@@ -171,6 +171,10 @@ func (win *Window) SaveFile() (int, error) {
return 0, nil
}
+ if win.isdir {
+ return 0, nil
+ }
+
f, err := os.OpenFile(win.NameAbs(), os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return 0, err
@@ -221,15 +225,29 @@ func (win *Window) Name() string {
}
func (win *Window) NameAbs() string {
+ if win.file.name == FnEmptyWin {
+ return ""
+ }
s, _ := filepath.Abs(win.file.name)
return s
}
+func (win *Window) NameFromTag() string {
+ tstr := win.tagline.text.String()
+ if tstr == "" {
+ return ""
+ }
+ return strings.Split(tstr, " ")[0]
+}
+
// Dir returns the working directory of current window, without trailing path separator.
func (win *Window) Dir() string {
if win.isdir {
return win.NameAbs()
}
+ if win.Name() == FnEmptyWin {
+ return baseDir
+ }
return filepath.Dir(win.NameAbs())
}