Skip to content

Commit

Permalink
Added support for Plan9Ports plumber
Browse files Browse the repository at this point in the history
Instead of poorly imitating the plumbing service, de can now
use the real p9p plumber on unix-like systems.

Since de itself is single-buffer, the new service deplumber must
be run running in order to receive plumbing messages and coordinate
whether a new de window should be created, or the existing one re-used.

If the plumbing message came from de and the buffer is clean, it will
tell that window to open the new file. Otherwise, it'll open a new one.

This frees de from having to do window management (leaving that job
to the window manager) while still getting the benefits of using
the real plumber.
  • Loading branch information
Dave MacFarlane authored and driusan committed Jan 15, 2017
1 parent 5d15bf6 commit 9b6e1f7
Show file tree
Hide file tree
Showing 10 changed files with 740 additions and 29 deletions.
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Expand Up @@ -36,5 +36,4 @@ I also wouldn't be opposed to anyone doing work that makes progress on GitHub Is
faithful vi key bindings.) The more I use de instead of vi, the more I get used to the idiosyncrasies,
and the less likely I am to remember how it's supposed to work. (Similarly, if you're a long time
acme user, I'm not opposed to any changes that make the mouse usage less surprising to acme users,
although since de is cross-platform and can't assume p9p is installed, we can't assume the plumber
is installed.)
either.
36 changes: 36 additions & 0 deletions PLUMBING.md
@@ -0,0 +1,36 @@
# Plumbing

(If you're unfamiliar with the Plan 9 concept of "plumbing", you may want
to start by reading [the paper](http://doc.cat-v.org/plan_9/4th_edition/papers/plumb))

de now supports directly using the plan9ports plumber for interpreting
interactions, if it's available, with a couple caveats.

1. Since de is a single-buffer editor, we need another process (the deplumber)
to handle the incoming messages. (If de handled the edit port itself either
either every window would process each edit message and the windows
would multiply like rabbits, or the plumbing would stop working after the
first window closed.)
2. The default p9p plumbing rules don't plumb directories to $editor. The file
plumbing.sample is a sample config that you can put in $HOME/lib/plumbing
in order to use deplumber to listen on the edit port, and also plumb
directories to edit.

So to use de with plumbing the steps are:
1. Run "plumber" (from p9p)
2. Run "deplumber" (from de, you may need to `go get github.com/driusan/de/...` first) in the background)
3. Run de, or plumb a message some other way to test it.

You can plumb from de either by hitting the Enter key or right clicking
somewhere. If the message couldn't be successfully plumbed or deplumber isn't
running, de will fall back on the old behaviour (find next for right-click, and
execute for enter.)

de is a text editor, not a window manager. If you're running under X11, you may
want to use a tiling window manager in order to have your windows managed in a
way that makes de more closely resemble acme. Otherwise, de doesn't pretend
that it knows how you like your windows arranged better than you and your window
manager.

The integration of plumbing directly into de is relatively new, so if you have
any problems, please file a bug report.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -20,6 +20,7 @@ See [USAGE.md](USAGE.md) for usage instructions.
* vi-like keybindings and philosophy.
* acme-like mouse bindings and philosophy.
* Ability to write plugins in Go. See [PLUGINS.md](PLUGINS.md).
* Ability to plumb with p9p plumber. See [PLUMBING.md](PLUMBING.md)

![de screenshot](https://driusan.github.io/de/descreenshot_code.png)

Expand All @@ -36,7 +37,7 @@ See [USAGE.md](USAGE.md) for usage instructions.
It should be installable with the standard go tools:

```
go get -u github.com/driusan/de
go get -u github.com/driusan/de/...
```

Then as long as $GOPATH/bin is in your path, you can launch with `de [filename]`
Expand Down
23 changes: 13 additions & 10 deletions actions/execute.go
Expand Up @@ -148,7 +148,7 @@ const (
replaceDot
)

func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {
func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) error {
// replace aliases before doing anything else
for name, value := range aliases {
if strings.HasPrefix(cmd, name) {
Expand Down Expand Up @@ -180,10 +180,10 @@ func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {

// it was an internal command, so run it.
f(string(newArgs), buff, v)
return
return nil
}
if len(cmd) <= 0 || buff == nil {
return
return fmt.Errorf("Could not run command")
}
var ignoreReturnCode bool
if cmd[0] == '!' {
Expand Down Expand Up @@ -220,21 +220,21 @@ func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {
stdout, err := gocmd.StdoutPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return
return fmt.Errorf("Could not open STDOUT pipe")
}
stdin, err := gocmd.StdinPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return
return fmt.Errorf("Could not open STDIN pipe")
}
stderr, err := gocmd.StderrPipe()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return
return fmt.Errorf("Could not open STDERR pipe")
}
if err := gocmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return
return fmt.Errorf("Could not execute command")
}

// send the selection to the processes's stdin, or the file if nothing
Expand All @@ -249,7 +249,7 @@ func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {
output, err := ioutil.ReadAll(stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
return
return fmt.Errorf("Could not read output of command.")
}

// print stderr to the tagline if anything was printed to stderr. This needs to be
Expand All @@ -270,12 +270,14 @@ func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {
// Something went wrong, so log it and return without modifying the real
// buffer.
fmt.Fprintf(os.Stderr, "%s\n", exiterr)
return
// The command exited with an error, but running the command
// was a success, so we don't error out.
return nil
}

if len(output) <= 0 {
// there was no output, so we don't need to do anything
return
return nil
}

switch mode {
Expand Down Expand Up @@ -321,6 +323,7 @@ func RunOrExec(cmd string, buff *demodel.CharBuffer, v demodel.Viewport) {
buff.Buffer = newBuffer
}
buff.Dirty = true
return nil
}

var aliases map[string]string
Expand Down
209 changes: 209 additions & 0 deletions actions/plumb.go
@@ -0,0 +1,209 @@
package actions

import (
"fmt"
"os"
"strconv"

"9fans.net/go/plan9"
plumblib "9fans.net/go/plumb"

"github.com/driusan/de/demodel"
)

// This boolean indicates whether plumbing is ready to be used. It should
// generally only be called by the main thread to flag that plumb messages
// are fine to use.
//
// Until the main thread does that, plumb will fail, assuming that it hasn't
// been properly initialized.
var PlumbingReady bool

func plumb(content []byte, buff *demodel.CharBuffer, v demodel.Viewport, click int) error {
if !PlumbingReady {
return fmt.Errorf("Plumbing unavailable")
}
fid, err := plumblib.Open("send", plan9.OWRITE)
if err != nil {
fmt.Printf("%v", err)
return err
}

wd, _ := os.Getwd()
m := plumblib.Message{
Src: "de",
Dst: "",
Dir: wd,
Type: "text",
Data: content,
}
if click != 0 {
m.Attr = &plumblib.Attribute{Name: "click", Value: strconv.Itoa(click)}
}
return m.Send(fid)
}

func PlumbExecuteOrFindNext(From, To demodel.Position, buff *demodel.CharBuffer, v demodel.Viewport) {
if buff == nil {
return
}
dot := demodel.Dot{}
i, err := From(*buff)
if err != nil {
return
}
dot.Start = i

i, err = To(*buff)
if err != nil {
return
}
dot.End = i + 1

word := string(buff.Buffer[dot.Start:dot.End])

// Don't bother trying if there's nothing listening yet.
if PlumbingReady {
// Create a new []byte for the plumber, because if nothing
// was selected we want to send a message with a click
// attribute, and if plumbing fails we don't want to have
// touched the word for the fallbacks.
var plumbword []byte
var click int
var pdot demodel.Dot

if dot.Start == dot.End-1 {
// Nothing was selected, to add a "click" attribute
if dot.Start < 100 {
click = int(dot.Start)
pdot.Start = 0
} else {
click = 100
pdot.Start = dot.Start - 100
}
if dot.End+100 < uint(len(buff.Buffer)) {
pdot.End = pdot.End + 100
} else {
pdot.End = uint(len(buff.Buffer))
}
plumbword = buff.Buffer[pdot.Start:pdot.End]
} else {
// Default to "word" (the selected text)
// with no click attribute
plumbword = []byte(word)
}

// If the message was successfully plumbed, we're done.
if err := plumb(plumbword, buff, v, click); err == nil {
return
}
}
// Try executing the command. If it works, we're done.
if err := RunOrExec(word, buff, v); err == nil {
return
}

// We couldn't plumb it, we couldn't execute it, so give up and search for
// the word
lenword := dot.End - dot.Start
for i := dot.End; i < uint(len(buff.Buffer))-lenword; i++ {
if string(buff.Buffer[i:i+lenword]) == word {
buff.Dot.Start = i
buff.Dot.End = i + lenword - 1
return
}
}

}
func PlumbOrFindNext(From, To demodel.Position, buff *demodel.CharBuffer, v demodel.Viewport) {
if buff == nil {
return
}
dot := demodel.Dot{}
i, err := From(*buff)
if err != nil {
return
}
dot.Start = i

i, err = To(*buff)
if err != nil {
return
}
dot.End = i + 1

var word string

// If nothing is selected, instead send 100 characters before and after
// and include a "click" attribute in the plumbing message.
var click int
if dot.Start == dot.End-1 {
if dot.Start < 100 {
click = int(dot.Start)
dot.Start = 0
} else {
click = 100
dot.Start -= 100
}
if dot.End+100 < uint(len(buff.Buffer)) {
dot.End += 100
} else {
dot.End = uint(len(buff.Buffer))
}
}
word = string(buff.Buffer[dot.Start:dot.End])

if PlumbingReady {
if err := plumb([]byte(word), buff, v, click); err == nil {
return
}
}

// the file doesn't exist, so find the next instance of word.
lenword := dot.End - dot.Start
for i := dot.End; i < uint(len(buff.Buffer))-lenword; i++ {
if string(buff.Buffer[i:i+lenword]) == word {
buff.Dot.Start = i
buff.Dot.End = i + lenword - 1
return
}
}
}

func TagPlumbOrFindNext(From, To demodel.Position, buff *demodel.CharBuffer, v demodel.Viewport) {
if buff == nil || buff.Tagline == nil {
return
}
dot := demodel.Dot{}
i, err := From(*buff)
if err != nil {
return
}
dot.Start = i

i, err = To(*buff)
if err != nil {
return
}
dot.End = i + 1

// find the word between From and To in the tagline
word := string(buff.Tagline.Buffer[dot.Start:dot.End])

if PlumbingReady {
if err := plumb([]byte(word), buff, v, 0); err == nil {
return
}
}

// the file doesn't exist, so find the next instance of word inside
// the *non-tag* buffer.
lenword := dot.End - dot.Start
for i := buff.Dot.End; i < uint(len(buff.Buffer))-lenword; i++ {
if string(buff.Buffer[i:i+lenword]) == word {
buff.Dot.Start = i
buff.Dot.End = i + lenword - 1
return
}
}
}

0 comments on commit 9b6e1f7

Please sign in to comment.