diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33be763..365c135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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.) \ No newline at end of file +either. diff --git a/PLUMBING.md b/PLUMBING.md new file mode 100644 index 0000000..517afc0 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/README.md b/README.md index f422bca..726504e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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]` diff --git a/actions/execute.go b/actions/execute.go index 14a7cd9..ba83dc3 100644 --- a/actions/execute.go +++ b/actions/execute.go @@ -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) { @@ -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] == '!' { @@ -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 @@ -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 @@ -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 { @@ -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 diff --git a/actions/plumb.go b/actions/plumb.go new file mode 100644 index 0000000..860a43b --- /dev/null +++ b/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 + } + } +} diff --git a/deplumber/main.go b/deplumber/main.go new file mode 100644 index 0000000..4b58c9e --- /dev/null +++ b/deplumber/main.go @@ -0,0 +1,217 @@ +// The deplumber is a service which coordinates the running de processes +// to ensure that they don't act on the same plumber message. When it reads +// a plumbing message from the edit port, it will open it in the active de +// window if the buffer is clean, and open a new de window if it's not. +// +// Messages that don't come from de always spawn a new window. +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "os/signal" + "os/user" + "strconv" + "strings" + "sync" + "syscall" + + "9fans.net/go/plan9" + "9fans.net/go/plumb" +) + +// We keep track of the most recent de process that we got a message from, +// and make the assumption that that's the window that should be used for +// plumbing messages. Since plumbing messages are generated from the user +// doing something, we can assume that the user did something in that window +// in order to generate the message. +var activeProcess struct { + Conn net.Conn + PID uint64 + Dirty bool +} + +// A mutex for any modifications to the activeProcess. +var mu sync.Mutex + +var listener *net.UnixListener + +// Deletes the ~/.de/deplumber file just before dying. This should be called +// before log.Fatal() or from any signal handler. +func cleanupBeforeDeath() { + // Delete our ~/.de/deplumber file, since we're no longer using it. + if u, err := user.Current(); err == nil { + os.Remove(u.HomeDir + "/.de/deplumber") + } + + if listener != nil { + listener.Close() + } +} + +func handleConnection(conn net.Conn) { + defer conn.Close() + buffReader := bufio.NewReader(conn) + val, err := buffReader.ReadString('\n') + if err != nil { + return + } + val = strings.TrimSpace(val) + pid, err := strconv.ParseUint(val, 10, 64) + if err != nil { + log.Printf("Invalid PID %s\n", val) + return + } + activeProcess.PID = pid + activeProcess.Conn = conn + + for { + val, err := buffReader.ReadString('\n') + if err != nil { + return + } + val = strings.TrimSpace(val) + + mu.Lock() + activeProcess.PID = pid + activeProcess.Conn = conn + switch val { + case "Clean": + activeProcess.Dirty = false + case "Dirty": + activeProcess.Dirty = true + default: + log.Printf("Unknown status ", val) + + } + mu.Unlock() + } +} + +func plumbListener() { + f, err := plumb.Open("edit", plan9.OREAD) + if err != nil { + cleanupBeforeDeath() + log.Fatal("Could not connect to p9p plumber. Is it running?") + return + } + var m *plumb.Message = &plumb.Message{} + for { + err := m.Recv(f) + if err != nil { + cleanupBeforeDeath() + log.Fatal(err) + return + } + mu.Lock() + + // If we've received a plumbing message from de, we determine + // whether or not to receive a new de window by checking if the + // active window (which is the one that sent it) has a dirty + // buffer. + // + // If the message didn't come from de, we always spawn a new + // window. + if !activeProcess.Dirty && activeProcess.PID != 0 && m.Src == "de" { + fmt.Fprintf(activeProcess.Conn, "%s\n", m.Data) + } else { + cmd := exec.Command("de", string(m.Data)) + cmd.Dir = m.Dir + err := cmd.Start() + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + } + } + mu.Unlock() + } + +} + +func main() { + if u, err := user.Current(); err == nil { + if data, err := ioutil.ReadFile(u.HomeDir + "/.de/deplumber"); err == nil && string(data) != "" { + log.Fatalf("Another deplumber instance appears to already be running at %v. If this is not correct, please delete the file ~/.de/deplumber\n", string(data)) + } + } + + // Get a temporary filename to use as the unix domain socket. + // We don't actually use it, we just delete it and then create a + // UDS socket at the same address. + f, err := ioutil.TempFile("", "delistener") + if err != nil { + log.Fatal(err) + } + f.Close() + udsFile := f.Name() + os.Remove(udsFile) + + // Listen on the temporary file name we just created. + addr, err := net.ResolveUnixAddr("unix", udsFile) + if err != nil { + log.Fatal(err) + } + ln, err := net.ListenUnix( + "unix", + addr, + ) + if err != nil { + log.Fatal(err) + return + } + listener = ln + + // Write the temporary filename to ~/.de/deplumber, so that new de + // instances know where to send their plumbing status. + if u, err := user.Current(); err == nil { + ioutil.WriteFile(u.HomeDir+"/.de/deplumber", []byte(udsFile), 0600) + // This isn't done in a defer, because there's no code path + // where we exit normally. We call cleanupBeforeDeath() to do + // this instead. + //defer os.Remove(u.HomeDir + "/.de/deplumber") + } + + // Start a goroutine to listen for plumbing events + go plumbListener() + + // Start a thread to catch signals. We don't want to do it in the main + // thread, because ln.Accept() may be blocking. + go func() { + c := make(chan os.Signal) + signal.Notify(c, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGABRT, + syscall.SIGQUIT, + ) + s := <-c + // If we get past here, we caught a signal telling us to die. + cleanupBeforeDeath() + log.Fatalf("Killed by signal %s", s) + }() + + // Wait for dead child processes instead of leaving zombies around + go func() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGCHLD) + for { + <-c + syscall.Wait4(-1, nil, 0, nil) + } + }() + + for { + // Just keep accepting connections on our socket. + conn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + go handleConnection(conn) + } +} diff --git a/issues/Keyboard-plumbing-should-include-click-attribute/Description b/issues/Keyboard-plumbing-should-include-click-attribute/Description new file mode 100644 index 0000000..c3b7b01 --- /dev/null +++ b/issues/Keyboard-plumbing-should-include-click-attribute/Description @@ -0,0 +1,5 @@ +When a mouse event generates a click, the message plumbed includes a click +attribute in order to make it easier for the plumber to get things right +from context. + +The keyboard should do this too, when "enter" generates a plumbing event. \ No newline at end of file diff --git a/kbmap/normalmode.go b/kbmap/normalmode.go index 09631da..92cf443 100644 --- a/kbmap/normalmode.go +++ b/kbmap/normalmode.go @@ -350,9 +350,17 @@ func normalMap(e key.Event, buff *demodel.CharBuffer, v demodel.Viewport) (demod return NormalMode, demodel.DirectionDown, nil case key.CodeReturnEnter: if buff.Dot.Start == buff.Dot.End { - actions.OpenOrPerformAction(position.CurExecutionWordStart, position.CurExecutionWordEnd, buff, v) + // If the plumbing is ready to go, Plumb will add a click + // attribute. Otherwise, fall back on our executionword + // heuristic. + if actions.PlumbingReady { + actions.PlumbExecuteOrFindNext(position.DotStart, position.DotEnd, buff, v) + } else { + actions.OpenOrPerformAction(position.CurExecutionWordStart, position.CurExecutionWordEnd, buff, v) + //actions.PlumbExecuteOrFindNext(position.CurExecutionWordStart, position.CurExecutionWordEnd, buff, v) + } } else { - actions.OpenOrPerformAction(position.DotStart, position.DotEnd, buff, v) + actions.PlumbExecuteOrFindNext(position.DotStart, position.DotEnd, buff, v) } // There's a possibility OpenOrPerformAction opened a new file, in which case // we should scroll to the top, or inserted text, in which case we should scroll diff --git a/main.go b/main.go index 7a9aaf5..7f6cf7c 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/driusan/de/actions" "github.com/driusan/de/demodel" @@ -76,11 +77,11 @@ func runStartupCommands(b *demodel.CharBuffer, v demodel.Viewport) { } func main() { - /* - f, _ := os.Create("test.profile") - pprof.StartCPUProfile(f) - defer pprof.StopCPUProfile() - */ + dirtyChan := make(chan bool) + + plumber := &plumbService{} + plumber.Connect(dirtyChan) + var sz size.Event var filename string @@ -124,6 +125,7 @@ func main() { buff.LoadSnarfBuffer() defer buff.SaveSnarfBuffer() + var MouseButtonMask [6]bool viewport := &viewer.Viewport{ @@ -164,12 +166,84 @@ func main() { // the tagline, we should stay in tagmode. runStartupCommands(&buff, viewport) + + // cache the last dirty status, so that we only send a message when + // it changes, instead of every single event + lastDirty := buff.Dirty + + var w screen.Window + + // Monitor the plumber service, and send it the buffer status + // as soon as it's available. + go func() { + // If it's not ready yet, wait for either an error or it to + // become available. + for !plumber.Available() { + // Select on the different channels that the plumbService + // may have sent on, and convert them to. + select { + case err := <-plumber.ErrorChan: + // If there was an error connecting before + // it became available, print it and abort + // the goroutine. + buff.AppendTag("\n" + err.Error()) + return + default: + // Wait 200 milliseconds before trying again, + // there's no reason to be too greedy. + time.Sleep(200 * time.Millisecond) + } + } + + // Buffer must be available if we got here, so make sure the buffer + // dirty status is up to date. + dirtyChan <- buff.Dirty + lastDirty = buff.Dirty + + actions.PlumbingReady = true + + // All that's left to do is continually wait for messages + // on either OpenChan or ErrorChan and act on them. + for { + select { + case err := <-plumber.ErrorChan: + // Print any errors that come in to the tagline. + buff.AppendTag("\n" + err.Error()) + case filename := <-plumber.OpenChan: + // Open the requested file if we got something on openChan. + if err := actions.OpenFile(filename, &buff, viewport); err != nil { + buff.AppendTag("\n" + err.Error()) + continue + } + + // Reset the viewport to 0,0, get a new + // renderer for the correct content type, + // and request a new render of the + // current window. + viewport.Location.X = 0 + viewport.Location.Y = 0 + + viewport.SetRenderer( + renderer.GetRenderer(&buff), + ) + if w != nil { + w.Send(viewer.RequestRerender{}) + } + + } + } + }() + + // Main shiny event loop driver.Main(func(s screen.Screen) { - w, err := s.NewWindow(nil) + win, err := s.NewWindow(nil) if err != nil { return } + // We need to store the window as w, so that it's available + // for the plumbService to use. + w = win defer w.Release() window := dewindow{ Window: w, @@ -177,9 +251,24 @@ func main() { viewport.Window = w for { + if plumber.Available() { + // Send the dirty status if it changes. + if buff.Dirty != lastDirty { + dirtyChan <- buff.Dirty + lastDirty = buff.Dirty + } + } + + // Handle the next actual event switch e := w.NextEvent().(type) { case lifecycle.Event: - if e.To == lifecycle.StageDead { + switch e.To { + case lifecycle.StageFocused: + if plumber.Available() { + dirtyChan <- buff.Dirty + lastDirty = buff.Dirty + } + case lifecycle.StageDead: return } case key.Event: @@ -441,17 +530,29 @@ func main() { if e.Direction == mouse.DirRelease && e.Button == mouse.ButtonRight { oldFilename := buff.Filename if evtBuff == buff.Tagline { - if eDot.Start == eDot.End { - actions.FindNextOrOpenTag(position.CurTagWordStart, position.CurTagWordEnd, &buff, viewport) + if plumber.Available() { + if eDot.Start == eDot.End { + actions.TagPlumbOrFindNext(position.CurTagWordStart, position.CurTagWordEnd, &buff, viewport) + } else { + actions.TagPlumbOrFindNext(position.TagDotStart, position.TagDotEnd, &buff, viewport) + } } else { - actions.FindNextOrOpenTag(position.TagDotStart, position.TagDotEnd, &buff, viewport) + if eDot.Start == eDot.End { + actions.FindNextOrOpenTag(position.CurTagWordStart, position.CurTagWordEnd, &buff, viewport) + } else { + actions.FindNextOrOpenTag(position.TagDotStart, position.TagDotEnd, &buff, viewport) + } } } else { - if eDot.Start == eDot.End { - actions.FindNextOrOpen(position.CurWordStart, position.CurWordEnd, evtBuff, viewport) - + if plumber.Available() { + actions.PlumbOrFindNext(position.DotStart, position.DotEnd, evtBuff, viewport) } else { - actions.FindNextOrOpen(position.DotStart, position.DotEnd, evtBuff, viewport) + if eDot.Start == eDot.End { + actions.FindNextOrOpen(position.CurWordStart, position.CurWordEnd, evtBuff, viewport) + + } else { + actions.FindNextOrOpen(position.DotStart, position.DotEnd, evtBuff, viewport) + } } } if oldFilename != buff.Filename { @@ -551,5 +652,6 @@ func main() { } } + }) } diff --git a/plumber.go b/plumber.go new file mode 100644 index 0000000..e32b76d --- /dev/null +++ b/plumber.go @@ -0,0 +1,131 @@ +package main + +import ( + "bufio" + "fmt" + "io/ioutil" + "net" + "os" + "os/user" + "strings" + "time" +) + +// a plumbService coordinates communication between a de process, and a deplumber +// process. +// +// (The deplumber process communicates with the p9p plumber and receives messages. +// It decides whether or not to spawn a new window or re-use the existing one as +// best as it can.) +type plumbService struct { + + // A channel which communicates that a file named + // string should be opened. + OpenChan chan string + + // A channel on which the main thread communicates the + // clean/dirty status of its buffer to us. + DirtyChan chan bool + + // A channel which the plumbService communicates errors over. + ErrorChan chan error + + // The Unix Domain Socket connection to the deplumber service. + conn net.Conn + + // Set to true at the end of initialization, so that Connect() + // doesn't need to block and Available() will work. + ready bool +} + +// Connect connects the plumbService to a deplumber instance, and returns +// channels that it may asynchronously communicate with the main thread +// over. +// +// In particular, if there's any errors they will be sent over the errors +// channel instead of returned directly, to avoid having to block when calling +// connect. +// +// dirtyChan is a channel that de communicates the buffer dirty status to +// the plumbService over. +func (p *plumbService) Connect(dirtyChan chan bool) { + p.ErrorChan = make(chan error, 1) + p.OpenChan = make(chan string) + p.DirtyChan = dirtyChan + // Read the ~/.de/deplumber file to see where we should connect + u, err := user.Current() + socket, err := ioutil.ReadFile(u.HomeDir + "/.de/deplumber") + if err != nil { + p.ErrorChan <- fmt.Errorf("deplumber not started. Plumbing not available.") + close(p.ErrorChan) + return + } + + // Connect. + // It's a Unix Domain socket. If it takes longer than a second to connect, there's a problem + p.conn, err = net.DialTimeout("unix", string(socket), time.Second) + if err != nil { + p.ErrorChan <- fmt.Errorf("Could not connect to deplumber at %v: %v", string(socket), err) + close(p.ErrorChan) + return + } + + // Monitor the dirtyChan for messages from the main thread saying + // our dirty bit has changed, and inform the deplumber as appropriate. + go p.dirtyMonitor() + + // We're now ready to receive messages and monitor the connection for + // new files that we should open. + go p.monitorOpenChan() + return +} + +// Returns whether the plumbing service is available and ready +// to plumb messages +func (p *plumbService) Available() bool { + return p.ready && p.conn != nil +} + +// Goes into an infinite loop monitoring the dirtyChan +// for changes in buffer status, and forwards them to the +// deplumber connection. +// +// This should only be called from a goroutine after the +// connection is initialized. It communicates from de to +// the deplumber. +func (p *plumbService) dirtyMonitor() { + for { + dirty := <-p.DirtyChan + + if dirty { + fmt.Fprintf(p.conn, "Dirty\n") + } else { + fmt.Fprintf(p.conn, "Clean\n") + } + } +} + +// Goes into an infinite loop, reading messages from the socket connection +// and sending them across the OpenChan +// +// This should also only be called from a goroutine after the connection is +// initialized. It communicates from the deplumber to de. +func (p *plumbService) monitorOpenChan() { + // We've connected to the deplumber service, so send it our PID and tell + // it we have a clean buffer + fmt.Fprintf(p.conn, "%d\nClean\n", os.Getpid()) + r := bufio.NewReader(p.conn) + + p.ready = true + + for { + file, err := r.ReadString('\n') + if err != nil { + p.ErrorChan <- err + p.ready = false + return + } + file = strings.TrimSpace(file) + p.OpenChan <- file + } +}