diff options
-rw-r--r-- | README.md | 12 | ||||
-rw-r--r-- | text.go | 7 | ||||
-rw-r--r-- | todo | 3 | ||||
-rw-r--r-- | view.go | 77 | ||||
-rw-r--r-- | window.go | 28 |
5 files changed, 101 insertions, 26 deletions
@@ -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 @@ -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) @@ -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? @@ -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() @@ -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 "" |