7 November 2009 - 23:32Chrome extension: edit Gmail textarea in an external editor
A month or two ago, I switched to using Chrome daily builds (on Debian) as my primary browser. As with most people switching to Chrome, I love the stability and speed, but there are a few extensions from Firefox I sorely miss. A major one is mozex, which allows you to edit textareas in an external editor (It’s All Text also does this). I used this for editing Gmail. I used Gnus as my primary mail and news client for years, and one of the big sticking points to switching to webmail was the anemic textarea editing interface. I’m just used to all of the trappings of Emacs — dict-mode, fill-paragraph, the keybindings, remembrance agent, etc.
Anyway, I decided to see if I could whip up a quick and dirty Chrome extension to substitute for mozex. And when I say quick and dirty, I mean it — this code would make a Perl-happy sysadmin blush. But I hacked something up in a few hours, and I thought I’d put it out there since I find it useful. Maybe someone else can run with it and make it more complete like mozex. With a few one-line edits, this extension can be made to allow the editing of virtually any textarea in any external editor. I limited it to Gmail because I don’t know if there is the possibility of resource leaks or other weird side-effects from running it more extensively. Also, it’s hard-coded to use Emacs, but you could just as easily use Gvim (have it spawn an xterm with an editor or use emacs-client or whatever).
I’ve put the extension source files up here:
http://www.thegibson.org/blog/files/emacs_chrome
Edit: see http://www.thegibson.org/blog/files/emacs_chrome/sync_ver — the original only works right on platforms like Linux due to a serendipitous interaction (see comments).
Edit2: see http://www.thegibson.org/blog/files/emacs_chrome/ba_sync_ver/ — this version uses a Chrome “browser action” rather than a “page action.” The “browser action” extension type is better suited for this. Note that some of the text below is not longer applicable to this new and simpler version, so see comments. I’ll try to make a new post soon with the updated info.
The extension comes with a Python script (you’ll need Python 2.6 to run it). The pycl.py script is a little web server running on port 9292. Run the web server and leave it in the background. Then add the extension to Chrome (go to chrome://extensions). Once you add the extension, you can load up Gmail and either compose or reply to a plain text message (not HTML). Then use the “compose in a new window” icon to open a new window, and you’ll see an Emacs 23 icon appear in the “page action” area (the upper right-hand corner of the address bar, where the SSL lock icon goes). Clicking the icon sends the contents of the text area to the Python web server, which writes it to a temporary file (using tempfile.NamedTemporaryFile) and spawns an editor. Edit your text; when you’re done, save and exit. In the meantime, the background script will start polling the web server to see if the editor has terminated. When the editor finishes, the file contents are read and sent back to Chrome, which modifies the textarea with the new contents.
Anyway, it has several major limitations — first and foremost, it only works consistently if you compose or reply in a new window (click that little diagonal arrow icon in the upper right hand corner of the message frame). Sometimes it works on the main Gmail page, but not always. Due to the hooks it uses, if you open the new window while viewing the message, and then click reply, you’ll have to type something in the textarea or un-focus and re-focus the window before it’ll “notice” the existence of the new textarea. This is a limitation of the way the extension content script is finding the textarea in the page. Maybe the fact that it only works consistently in a new window has to do with Gmail’s crazy dynamic DOM manipulation. Or maybe — perhaps even more likely — I’m missing something obvious. I’m sure someone with more Javascript / DOM / experience could probably fix this. I’ve never used Javascript in any context before, so it was an interesting experience. I basically looked at two Chrome example extensions — 1) the RSS feed subscription and 2) the Gmail checker — and munged their code and techniques together.
Changing editors
To change editors, just edit pycl.py and modify the line:
p = subprocess.Popen(["/usr/bin/emacs", f.name])
Making it work on arbitrary textareas
If you want to change the extension to work on more than just Gmail, first edit majifest.json and change the “matches” line:
"matches": ["http://mail.google.com/*", "https://mail.google.com/*", "file://*/*"],
"matches": ["http://*/*", "https://*/*", "file://*/*"],
The second matches example works on all http and https websites.
Then, in the ta_find.js file, change the getElementsByName(‘body’); line:
result = document.getElementsByName('body');
result = document.getElementsByTagName('textarea');
The second will find any textarea on the page (one limitation of the current incarnation is that it will only deal with the first textarea, however).
Making it not suck
This is my first foray into in-browser Javascript programming. I’m sure there’s a lot of missing exception handling in background.html when dealing with the XmlHttpRequest objects. There might be resource leaks too. And there are corner cases where it won’t get the textarea contents to send to the external editor correct (like if you only use mouse cut and paste to change the contents around without any keypresses). Also, in the Python script, there’s not much error handling either, and using a dedicated private directory (like mozex) rather than tempfile.NamedTemporaryFile is probably advisable on shared systems. Also, it’d be great if some Javascript wizard could help figure out why it only works with Gmail when you open a new window. Ideally I would have liked to add a context menu item to all textareas, but I figured it was easier to use existing Chrome extensions as templates and do it with the “page action” mechanism.
Thoughts on Javascript
Looking at the way that the Gmail checker and the RSS feed subscription work, I see an extensive use of closures and continuation-passing style asynchronous programming. Given JavaScript’s reputation, this is definitely a lot nicer and cleaner than than what I expected. Steve Yegge has also pointed out several times that JavaScript is a nicer language than people actually give it credit for, and now I know firsthand.
20 Comments | Tags: Uncategorized
30 Nov 2009 - 11:36
Thanks for sharing this. The primary reason I have not switched to chrome was the lack of
Firefox extensions that are essential to my work – primarily, the It’s All Text extension, which I use
for editing MediaWiki pages.
Being new to chrome, I tried using this with chrome 3.0.195.33 on Windows and ran into a few snags.
It seemed to almost work, until I found that extensions don’t really work on the production
or even beta versions of chrome; one *must* use the ‘dev’ channel. (If using chrome 3 or beta 4,
extensions seem to work, but parts don’t, such as the “tabs’ permission or “”run_at”: “document_idle”.)
Once I switched to the dev version, I tried the generic version of your extension to edit
textareas in any page
The matches line above is wrong; it requires an extra / on the https: pattern and quotes around the file pattern
“matches”: ["http://*/*", "https://*/*", "file://*/*"],
I also changed the Python to run emacsclient instead of emacs. The extension works, halfway.
I get the Emacs icon in chrome, and when I click on it, the request does get forwarded to Emacs,
which edits the temp file.
But when I close the buffer in Emacs (C-x # or M-x server-edit), the text area back in chrome
does not update with the saved text. After killing the python server, I see messages like
localhost – - [30/Nov/2009 10:37:54] code 404, message Not Found: /2/wpTextbox1
on the console. I debugged further and found I’m getting an exceptions.NameError exception at
self.wfile.write(result)
in do_POST in the Python; ‘result’ is not bound. I’m not sure what this is attempting here.
I removed it and I get the same behavior: clicking the icon in chrome causes an emacsclient
buffer to open on the temp file, but after saving and closing the buffer, the changes do not show
up back in chrome.
Thoughts on how to fix this?
30 Nov 2009 - 14:39
Hi Dave,
First of all, thanks for pointing out those typos. I went ahead and fixed the matches line in the body of my post (missing quotes and one slash) and that self.wfile.write(result) line is spurious. There was a variable called result which was just used for debugging, but I removed the assignment and not the use. The ease of making that silly mistake is one quibble I have with dynamic languages that only check definition at runtime.
Interestingly enough, the fact that it would throw an exception there (due to result being undefined) and return a 404 in the catch clause did not affect its functioning on my setup (Linux + daily Chromium builds). It’s probable that was because the error happened AFTER the 200 response code was sent (so the extra 404 response was seen as message content). In fact, after adding some debugging print statements, it looks like the Javascript/Python interactions definitely don’t work the way I thought they would/should. The fact that it works for me despite that is embarrassing.
The way I had thought it would work was that the POST would return success and the system would poll the web server every once and a while with GETs until the editor is closed and the GET returns the content. Based on the way it is actually functioning, it looks like the POST request might stay blocked until the editor exits and then the GET occurs once (which is actually a nicer way of handling it but I wasn’t sure if I could keep the XMLHttpRequest connection open indefinitely, so I went for the split request/response).
In any event, I have never tried this on Windows, but I’d suspect it doesn’t work the same for one (or both) of two reasons:
1) The way the Python Subprocess module works on Windows
2) The way the XMLHTTPRequest and polling stuff in the Javascript works on Windows Chromium vs. Linux Chromium (it’s possible it relies on unspecified behavior)
To narrow it down, you could try adding a debugging print statement at the top of the do_GET method. If that code never gets executed, it is because the Javascript never gets around to the second part (sending the GET), which would mean there’s something in the background.html callback structure that’s failing. If it does get executed, but returns 404, it’s probably because Python’s Subprocess.poll works differently on Windows. I could also see this stemming from some interaction between Subprocess.Popen and the way the Python HTTPServer works.
I would dig in and debug it a bit more but I’m traveling and don’t have the time this week (or a Windows system to test on).
30 Nov 2009 - 15:48
So after ruminating idly, I think I know why the POST request is hanging on. When I call p = subprocess.Popen, the child process inherits the parent’s file descriptors unless you pass close_fds=True. That means that the child editor process is holding the XMLHTTPRequest connection open until it terminates.
Rather than bother with the two phase split request/response, you could just make a synchronous version where the POST response text contains the edited text content. I hacked up a little PoC here using that method:
http://www.thegibson.org/blog/files/emacs_chrome/sync_ver/
See if that works on Windows for you.
30 Nov 2009 - 16:37
Cool; I’ll try that. I had already put some print statements in there and do_GET was never being called.
I might hack on this (tho I’m no Javascript or Python jockey); the general outline (in your response) of what’s supposed to happen helps.
The behavior I would like ultimately is more like Firefox’s It’s All Text extension (https://addons.mozilla.org/en-US/firefox/addon/4125); each file save from the external editor causes the textarea to refresh; it does not wait for the edit to complete. I find that more convenient to work with, especially with MediaWiki, where there are multiple edit/preview iterations.
01 Dec 2009 - 8:46
Awesome. This works now for general textarea editing via emacsclient (on Windows). Thanks!
06 Dec 2009 - 4:37
Good to hear it works for you. I’m working on a new version that uses a browser action. It’s much simpler codewise, too:
http://www.thegibson.org/blog/files/emacs_chrome/ba_sync_ver/
After I work out the kinks (see below), I may try to make the refresh mods you mentioned.
The reason for moving from a page action to a browser action is to help with dynamically modified pages. The page action script is run after the page loads but maybe before AJAX actions modify the DOM; after that you have to reload the page to get it to run again. The browser action script is run whenever you click the button.
This version works with textareas on normal pages but I’m having trouble getting it to work with GMail. Looking at the console, the basic entrypoint isn’t even getting executed when the page action button is clicked unless I manually reload the whole page and try again (and set allFrames: true in the call to chrome.tabs.executeScript in background.html). It seems that after the GMail Javascript modifies the page some more, the browser action stops going to the right place for some reason (because if I add debugging output to the update.js script, it doesn’t even appear on the page’s console).
12 Dec 2009 - 6:14
Hi,
I’d like to have a hack at this. I was just wondering if your files are backed by a version control I could sync to?
To my mind for emacs it would be simpler to run the equivalent of pycl.py in emacs itself and cut out the middle man. I shall have a go at that once I’ve played with the code.
12 Dec 2009 - 11:55
Alex, I don’t have a public repo but I’ll put something up at Google code if you’d like.
As far as embedding it in Emacs, you make a good point that you could write some elisp that would handle it inside Emacs. However, I personally like the separated Python shim for flexibility, because it allows people to change it to use any editor they want (e.g., gvim, kate, an xterm with pico, etc.), even ones that don’t have the ability to run that functionality internally. Not that I want to discourage you from hacking an elisp version up if you want.
BTW, I’m going to consider the “browser action” version the canonical version because it’s much more appropriate than a “page action” for this kind of thing. The issue with Gmail I mentioned above seems to be a Chromium bug and there is an open ticket on it that someone else reported on the same day I found it:
http://code.google.com/p/chromium/issues/detail?id=29541
13 Dec 2009 - 15:57
Well there is no reason that the python client and a theoretical elisp implementation can’t support the same XmlHTTP API.
I’m currently looking at the Javascript side first though. I’ll post up a link to a git repo if I have anything to share…
14 Dec 2009 - 3:07
Are you going to post to a VCS? This looks like an interesting project, I’ve been having It’s all text and vimperator withdrawal recently.
15 Dec 2009 - 10:03
So I thought I’d post where I’d gotten to with playing around. It’s currently non functional as I need to put your Xml code back in to actually send requests to the “edit server”. However what it does do is create a content script which modifies a page by finding and tagging all it’s textareas in a similar way to Its All Text.
When the user clicks the edit button this fires off a message to the bacground.html code (now in xmlcomms.js, editing JavaScript inside HTML hurts too much). Next steps are to plumb in your XmlHttpReuqest code and set-up the reverse path to send the update the text area with the result. Current known issue, need to pass the edit_id back so we know which text area is going to be tweaked.
Anyway the code can be seen at: http://github.com/stsquad/emacs_chrome. Let me know what you think?
15 Dec 2009 - 12:01
I really like where you’re going with that concept. The idea of putting a functional edit button in the page along with the text area by adding elements with JS actions to the DOM never occurred to me.
I also like the idea of having the content script run on page load and also manually via the browser action button in case unmarked text areas appear due to dynamic content modifying the page. One issue right now is that each time you press the browser action button you get extra edit buttons on the text areas that were unchanged. I’m no JS/DOM guy, but perhaps you can avoid that by looking at text.parentNode’s siblings to see if an img element you created is there before adding (maybe by keeping a table with the image elements you generate somewhere)? Also, one other minor suggestion: I don’t know how hard it would be, but maybe you can find a way to avoid marking hidden textareas? Google search results have a hidden text area, so after I search I get a weird edit button at the top margin of the page attached to that invisible text area.
Looking good though.
15 Dec 2009 - 13:33
Sure, once I have actually plumbed everything together and made an edit I’ll see about the other stuff. Feel free to add any notes on the github wiki.
I noticed you only have a copyright statement on the python code (boilerplate) but I assume your OK with the whole thing being GPLv3? Hopefully it’s a valid license for extensions.
16 Dec 2009 - 1:04
Yeah, GPLv3 is fine with me. I originally had my Javascript BSD licensed because I was editing a BSD licensed Google example extension but then when I switched to browser actions I ended up rewriting all of those Javascript pieces.
30 Dec 2009 - 7:37
OK I’ve done some fixups and restored the behaviour of the toolbar button. I’ve tagged 1.1 and published the extension. It should be showing up on the Google extension gallery once it has been through the approval process.
09 Jan 2010 - 9:22
A formal Google Chrome extension has been released which works basically the same way, although the web server is written in Perl. It’s called TextAid…and it defaults to opening Vim rather than Emacs.
https://chrome.google.com/extensions/detail/ppoadiihggafnhokfkpphojggcdigllp
Mark
05 Feb 2010 - 10:13
Interesting. I hadn’t considered this approach. There are some real advantages to this approach.
The downside is that python (or perl in the case of TextAid) is required, which isn’t a great requirement for Windows. :-/
I’m going to take another look at this and see if I can make this work in a more cross platform way and possibly port the idea back to IAT for FF.
How are you starting the python web server? Is that done externally or from within the extension?
Ciao!
05 Feb 2010 - 10:26
PS: I don’t see how to contact you directly. If there is a link, I don’t see it. You can reach me at http://docwhat.org/mail if you are interested working more on generalizing this concept.
07 Feb 2010 - 20:03
Thanks for your comments, Christian.
Regarding your first question, I’m just starting my Python web server manually. Since this was just a little project for my own uses, and I only restart my web browser or system once every few months at most, starting up the Python part isn’t a big deal for me.
Thanks for pointing out that I have no easily-accessible contact info. I updated my About page to point to my college webpage (which has contact info) and added an email address. As far as working more on the concept, I’d like to but I just graduated and I’m currently in the process of interviewing for jobs, so realistically I don’t think I’ll have any time for it in the near future. I’ve been also meaning to write a few new posts for my blog, but there’s no time for that either.
21 Feb 2011 - 10:58
Shameless self promotion:
I made a bit more general solution for this:
https://chrome.google.com/webstore/detail/ooddekcmdpjicehjkgobdopbkgepmahj