aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md12
-rw-r--r--text.go7
-rw-r--r--todo3
-rw-r--r--view.go77
-rw-r--r--window.go28
5 files changed, 101 insertions, 26 deletions
diff --git a/README.md b/README.md
index 460b8ac..7f42d61 100644
--- a/README.md
+++ b/README.md
@@ -20,14 +20,18 @@ Use the mouse. `^Q` exits.
Everything is text and everything is editable. There are two ways to interact with text, `Run` or `Open`.
-`Open` (`^O` or right-click) will assume the selected text is a file or a directory and will open a new window listing its content. If none is found, it does nothing.
+`Open` (`^O`, right-click or Ctrl+Click) will assume the selected text is a file or a directory and will open a new window listing its content. If none is found, it does nothing.
-`Run` (`^R` or middle-click) interprets the text as a command, which can be an internal poe command like `New` or `Del`. If none is found, it does nothing.
+`Run` (`^R`, middle-click or Alt+Click) interprets the text as a command, which can be an internal poe command like `New` or `Del`. If none is found, it does nothing.
-`^S` saves current buffer to disk.
+`^S` saves current buffer to disk. You can change the name by edit the tagline.
`^Z` undo, `^Y` redo.
+`^W` deletes word backwards.
+
+`^U` deletes to beginning of line.
+
### Commands
`New` opens an empty window.
@@ -38,7 +42,7 @@ Everything is text and everything is editable. There are two ways to interact wi
`Exit` closes all windows and exits the program.
-`!date` executes `date` as a shell command and presents its output in the message window named `+poe`.
+Run command on `date` executes `date` as a shell command and presents its output in the message window named `+poe`. Or `pwd`, or `ls -l`, or `curl google.se`, or... you get the idea.
## Bugs
diff --git a/text.go b/text.go
index b9398ed..fb4090a 100644
--- a/text.go
+++ b/text.go
@@ -82,9 +82,6 @@ func (t *Text) String() string {
// 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)
- if err != nil {
- return 0, 0, err
- }
t.off += size
t.lastRune = r
return
@@ -96,7 +93,7 @@ func (t *Text) UnreadRune() (r rune, size int, err error) {
r, size, err = t.ReadRuneAt(t.off)
t.off++
if err != nil {
- return 0, 0, err
+ return
}
t.off -= size
return
@@ -104,7 +101,7 @@ func (t *Text) UnreadRune() (r rune, size int, err error) {
// ReadRuneAt returns the rune and its size at offset. If the given offset (in byte count) is not a valid rune, it will try to back up until it finds a valid starting point for a rune and return that one.
//
-// This is basically a Seek(offset) followed by a ReadRune(), but does not affect the internal offset for future reads..
+// This is basically a Seek(offset) followed by a ReadRune(), but does not affect the internal offset for future reads.
func (t *Text) ReadRuneAt(offset int) (r rune, size int, err error) {
var c byte
c, err = t.buf.ByteAt(offset)
diff --git a/todo b/todo
index bffdcd4..d0aef98 100644
--- a/todo
+++ b/todo
@@ -1,13 +1,12 @@
jobs / commands
async execution via !<>|
file
- save empty buffer with tag name
- check for overwrite
overwrite when hash-verify fails on save
backup files on disk
window
hide / collapse
command Get (update buffer content)
+ open files with line number, eg poe.go:25
text
int64 as default in case of large files
undvika in-ram buffer - swapfiles?
diff --git a/view.go b/view.go
index 7bf1860..c1cef21 100644
--- a/view.go
+++ b/view.go
@@ -99,7 +99,14 @@ func (v *View) XYToOffset(x, y int) int {
// vertical (number of visual lines)
for y-v.y > 0 {
- r, _, _ := v.text.ReadRuneAt(offset)
+ 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--
@@ -126,7 +133,14 @@ func (v *View) XYToOffset(x, y int) int {
// horizontal
xw := v.x // for tabstop count
for x-v.x > 0 {
- r, n, _ := v.text.ReadRuneAt(offset)
+ 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
}
@@ -154,7 +168,11 @@ func (v *View) Scroll(n int) {
for n > 0 {
r, size, err := v.text.ReadRuneAt(v.scrollpos + offset)
if err != nil {
- break // hit EOF, stop scrolling
+ v.scrollpos = v.text.Len()
+ if err == io.EOF {
+ break // hit EOF, stop scrolling
+ }
+ return
}
offset += size
@@ -181,7 +199,7 @@ func (v *View) Scroll(n int) {
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 an empty new line, back up one more
+ 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
@@ -337,7 +355,6 @@ func (v *View) HandleEvent(ev tcell.Event) {
switch ev := ev.(type) {
case *tcell.EventMouse:
mx, my := ev.Position()
- pos := v.XYToOffset(mx, my)
switch btn := ev.Buttons(); btn {
case tcell.ButtonNone: // on button release
@@ -345,6 +362,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
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)
@@ -358,10 +376,51 @@ func (v *View) HandleEvent(ev tcell.Event) {
v.mpressed = true
v.mclickpos = pos
- if ev.Modifiers()&tcell.ModAlt != 0 {
- RunCommand(v.text.ReadDot())
+ 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 {
+ 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
}
+
+ if ev.Modifiers()&tcell.ModCtrl != 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 {
@@ -377,6 +436,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
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 {
@@ -393,6 +453,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
RunCommand(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 {
@@ -499,7 +560,7 @@ func (v *View) HandleEvent(ev tcell.Event) {
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(),
- baseDir, CurWin.Dir(), CurWin.Name(), CurWin.NameAbs(), CurWin.NameFromTag())
+ baseDir, CurWin.Dir(), CurWin.Name(), CurWin.NameAbs(), CurWin.NameTag())
return
case tcell.KeyCtrlO: // open file/dir
fn := v.text.ReadDot()
diff --git a/window.go b/window.go
index 9560ec6..763acc9 100644
--- a/window.go
+++ b/window.go
@@ -156,27 +156,40 @@ func (win *Window) LoadBuffer() bool {
win.file.mtime = info.ModTime()
win.file.read = true
- win.body.SetCursor(0, 0)
+ win.body.SetCursor(0, io.SeekStart)
return true
}
// SaveFile replaces disk file with buffer content. Returns error if no disk file is set.
func (win *Window) SaveFile() (int, error) {
- if win.file.name == FnEmptyWin {
+ if win.NameTag() == FnEmptyWin {
return 0, errors.New("no filename")
}
- if win.Name() == FnMessageWin {
+ if win.Name() == FnMessageWin { // can not save +poe window
return 0, nil
}
- if win.isdir {
+ // TODO: check this in real time in case of tag name change...
+ if win.isdir { // can not save a directory
return 0, nil
}
- f, err := os.OpenFile(win.NameAbs(), os.O_RDWR|os.O_CREATE, 0644)
+ // check for file existence if we recently changed the file name
+ openmasks := os.O_RDWR | os.O_CREATE
+ var namechange bool
+ if win.Name() != win.NameTag() { // user has changed name
+ openmasks |= os.O_EXCL // must not already exist
+ namechange = true // to skip sha256 checksum
+ }
+
+ f, err := os.OpenFile(win.NameTag(), openmasks, 0644)
if err != nil {
+ if os.IsExist(err) {
+ printMsg("%s already exists\n", win.NameTag())
+ return 0, nil
+ }
return 0, err
}
defer f.Close()
@@ -187,7 +200,8 @@ func (win *Window) SaveFile() (int, error) {
}
hhex := fmt.Sprintf("%x", h.Sum(nil))
- if hhex != win.file.sha256 {
+ // verify checksum if the file is not newly created via a namechange
+ if !namechange && hhex != win.file.sha256 {
return 0, errors.Errorf("file has been modified outside of poe")
}
@@ -232,7 +246,7 @@ func (win *Window) NameAbs() string {
return s
}
-func (win *Window) NameFromTag() string {
+func (win *Window) NameTag() string {
tstr := win.tagline.text.String()
if tstr == "" {
return ""