Go

Google ChromeのテキストエリアをEmacsで編集する'Edit with Emacs'から任意のエディタを起動するデーモンをGo 1.4 for Windowsで書いてみたわけだが、エディタがブラウザの後ろに出てしまってダメかもしれない

More than 3 years have passed since last update.

どういう経緯か忘れたのですが

という記事を拝見しました。概要としては

  • Google Chrome は Firefox とは違い、外部プロセスを起動することは禁止されている
  • そこで Chrome Extension の Edit with Emacs は、Emacs のサービスを 127.0.0.1:9292 に立ち上げて、XMLHttpRequest を使って、テキストエリアの内容を Emacs と交換している。
  • 上記の記事では、Emacs のサーバーのかわりに Perl でサービスを立ち上げて、任意のエディターを Chrome で使えるようにしている

という感じです。記事の方法だと Perl 一式が必要だし、Linux でしか動作が検証されていないようでした(別に Windows で動かないとは書いてないが)。が、記事のおかげで原理はだいたい分かったので、勉強がてら Go 1.4 for Windows で「だいたい同じもの」を作ってみました。

editsrv.go
package main

import (
    "bufio"
    "fmt"
    "html"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "os/exec"
    "strings"
)

func HasHtml(headers map[string][]string) bool {
    xurl, ok := headers["X-Url"]
    if !ok || len(xurl) <= 0 {
        return false
    }
    return strings.HasPrefix(xurl[0], "https://twitter.com")
}

func typeHeaders(h map[string][]string, w io.Writer) {
    for key, vals := range h {
        for _, val := range vals {
            fmt.Fprintf(w, "%s: %s\n", key, val)
        }
    }
}

func html2text(out io.Writer, in io.Reader) {
    scanner := bufio.NewScanner(in)
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Fprintln(os.Stderr, line)
        line = strings.TrimSpace(line)
        line = strings.Replace(line, "<div><br></div>", "\n", -1)
        line = strings.Replace(line, "<div>", "", -1)
        line = strings.Replace(line, "</div>", "\n", -1)
        line = html.UnescapeString(line)
        io.WriteString(out, line)
    }
}

func text2html(out io.Writer, in io.Reader) {
    scanner := bufio.NewScanner(in)
    for scanner.Scan() {
        line := scanner.Text()
        line = strings.TrimSpace(line)
        line = html.EscapeString(line)
        if line == "" {
            line = "<br>"
        }
        fmt.Fprintf(out, "<div>%s</div>", line)
    }
}

func handler(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(os.Stderr, "From: %s\n", req.RemoteAddr)
    typeHeaders(req.Header, os.Stderr)
    tmpfd, tmpfdErr := ioutil.TempFile("", "editsrv")
    if tmpfdErr != nil {
        fmt.Fprintln(os.Stderr, tmpfdErr)
        return
    }
    io.WriteString(tmpfd, "\xEF\xBB\xBF")
    tmpName := tmpfd.Name()
    defer os.Remove(tmpName)

    hasHtml := HasHtml(req.Header)
    if hasHtml {
        html2text(tmpfd, req.Body)
    } else {
        io.CopyN(tmpfd, req.Body, req.ContentLength)
    }
    tmpfd.Close()

    var editorName string
    if len(os.Args) >= 2 {
        editorName = os.Args[1]
    } else {
        editorName = "notepad.exe"
    }
    editorArgs := make([]string, 0, len(os.Args))
    for i := 2; i < len(os.Args); i++ {
        editorArgs = append(editorArgs, os.Args[i])
    }
    editorArgs = append(editorArgs, tmpName)
    cmd1 := exec.Command(editorName, editorArgs...)
    fmt.Fprintf(os.Stderr, "Call %s ", editorName)
    for _, arg1 := range editorArgs {
        fmt.Fprintf(os.Stderr, " %s", arg1)
    }
    fmt.Fprint(os.Stderr, "\n")
    if err := cmd1.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    tmpfd, tmpfdErr = os.Open(tmpName)
    if tmpfdErr != nil {
        fmt.Fprintln(os.Stderr, tmpfdErr)
        tmpfd.Close()
        return
    }
    fmt.Fprintf(os.Stderr, "Send '%s' to Chrome\n", tmpName)
    if hasHtml {
        text2html(w, tmpfd)
    } else {
        _, copyErr2 := io.Copy(w, tmpfd)
        if copyErr2 != nil {
            fmt.Fprintln(os.Stderr, copyErr2)
        }
    }
    tmpfd.Close()
    fmt.Fprintln(os.Stderr, "Done")
}

func main() {
    fmt.Println("Any Editor Server for chrome-extension 'Edit with Emacs'")
    http.HandleFunc("/edit", handler)
    http.ListenAndServe(":9292", nil)
}

最初、よー分からんで何故か TCP のレベルからやろうとして挫折したのですが、net/http で書き直したところ、比較的簡単に出来ました。「editsrv.exe gvim.exe」を走らせておくと、テキストエリアの右下に出る edit のマークをクリックした時に gvim.exe が起動するようになります。(なお、UTF8 対応エディターである必要があります)

ただ、twitter の本家サイトについては、テキストエリアの内容が何故か HTML で飛んでくるので、このサイトだけは特別扱いして、HTML ⇔ プレーンテキストの変換を相互にやってます。やりすぎかな

普通、この手のものは「思いついたのはいいが、なかなかうまいこと作れない」ものなんですが、自分にしては、あっさりできて驚きでした。ま、多分、似たようなものの Go 版は先人がとっくの昔に作ってそうではありますが…まぁ、手段が目的みたいなもんですから、そこんところは目をつぶってくださいませ。

しかし…起動したエディターがブラウザの後ろに隠れてしまうのはどうしたもんかな…