Subject: Re: [sup-devel] editing messages outside of sup

From: Hamish <[email-masked]>
Date: Sat, 26 Feb 2011 19:23:13 +0000

Excerpts from Hamish's message of Sun Feb 20 22:02:54 +0000 2011:
> For the moment this work is in the async_message_edit branch. If no one
> shouts about this being a terrible idea then I'll merge it into next
> within a week.

No one shouted, so I've merged it into next. At the end of the email is
the diff of the async_message_edit branch against where I started it.

Hamish Downer

diff --git a/lib/sup.rb b/lib/sup.rb
index 74eb950..213823c 100644
--- a/lib/sup.rb
+++ b/lib/sup.rb
@@ -370,6 +370,7 @@ require "sup/horizontal-selector"
 require "sup/modes/line-cursor-mode"
 require "sup/modes/help-mode"
 require "sup/modes/edit-message-mode"
+require "sup/modes/edit-message-async-mode"
 require "sup/modes/compose-mode"
 require "sup/modes/resume-mode"
 require "sup/modes/forward-mode"
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
index 25ea132..444589a 100644
--- a/lib/sup/buffer.rb
+++ b/lib/sup/buffer.rb
@@ -73,7 +73,7 @@ class InputSequenceAborted < StandardError; end
 class Buffer
   attr_reader :mode, :x, :y, :width, :height, :title, :atime
   bool_reader :dirty, :system
-  bool_accessor :force_to_top
+  bool_accessor :force_to_top, :hidden
 
   def initialize window, mode, width, height, opts={}
     @w = window
@@ -82,6 +82,7 @@ class Buffer
     @focus = false
     @title = opts[:title] || ""
     @force_to_top = opts[:force_to_top] || false
+    @hidden = opts[:hidden] || false
     @x, @y, @width, @height = 0, 0, width, height
     @atime = Time.at 0
     @system = opts[:system] || false
@@ -265,7 +266,7 @@ EOS
   end
 
   def rollable_buffers
-    @buffers.select { |b| !b.system? || @buffers.last == b }
+    @buffers.select { |b| !(b.system? || b.hidden?) || @buffers.last == b }
   end
 
   def handle_input c
diff --git a/lib/sup/modes/buffer-list-mode.rb b/lib/sup/modes/buffer-list-mode.rb
index 40f2e76..08bb604 100644
--- a/lib/sup/modes/buffer-list-mode.rb
+++ b/lib/sup/modes/buffer-list-mode.rb
@@ -28,7 +28,7 @@ protected
   end
 
   def regen_text
-    @bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self }.sort_by { |name, buf| buf.atime }.reverse
+    @bufs = BufferManager.buffers.reject { |name, buf| buf.mode == self || buf.hidden? }.sort_by { |name, buf| buf.atime }.reverse
     width = @bufs.max_of { |name, buf| buf.mode.name.length }
     @text = @bufs.map do |name, buf|
       base_color = buf.system? ? :system_buf_color : :regular_buf_color
diff --git a/lib/sup/modes/edit-message-async-mode.rb b/lib/sup/modes/edit-message-async-mode.rb
new file mode 100644
index 0000000..385ba6a
--- /dev/null
+++ b/lib/sup/modes/edit-message-async-mode.rb
@@ -0,0 +1,89 @@
+module Redwood
+
+class EditMessageAsyncMode < LineCursorMode
+
+  register_keymap do |k|
+    k.add :edit_finished, "Finished editing message", 'E'
+    k.add :path_to_clipboard, "Copy file path to the clipboard", :enter
+  end
+
+  def initialize parent_edit_mode, file_path, msg_subject
+    @parent_edit_mode = parent_edit_mode
+    @file_path = file_path
+    @orig_mtime = File.mtime @file_path
+
+    @text = ["ASYNC MESSAGE EDIT",
+             "", "Your message with subject:",  msg_subject, "is saved in a file:", "", @file_path, "", 
+             "You can edit your message in the editor of your choice and continue to",
+             "use sup while you edit your message.", "",
+             "Press <Enter> to have the file path copied to the clipboard.", "",
+             "When you have finished editing, select this buffer and press 'E'.",]
+    super()
+  end
+
+  def lines; @text.length end
+
+  def [] i
+    @text[i]
+  end
+
+  def killable?
+    if file_being_edited?
+      if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
+        return false
+      end
+    end
+
+    @parent_edit_mode.edit_message_async_resume true
+    true
+  end
+
+  def unsaved?
+    !file_being_edited? && !file_has_been_edited?
+  end
+
+protected
+
+  def edit_finished
+    if file_being_edited?
+      if !BufferManager.ask_yes_or_no("It appears the file is still being edited. Are you sure?")
+        return false
+      end
+    end
+
+    @parent_edit_mode.edit_message_async_resume
+    BufferManager.kill_buffer buffer
+    true
+  end
+
+  def path_to_clipboard
+    if system("which xsel > /dev/null 2>&1")
+      # linux/unix path
+      IO.popen('xsel --clipboard --input', 'r+') { |clipboard| clipboard.puts(@file_path) }
+      BufferManager.flash "Copied file path to clipboard."
+    elsif system("which pbcopy > /dev/null 2>&1")
+      # mac path
+      IO.popen('pbcopy', 'r+') { |clipboard| clipboard.puts(@file_path) }
+      BufferManager.flash "Copied file path to clipboard."
+    else
+      BufferManager.flash "No way to copy text to clipboard - try installing xsel."
+    end
+  end
+
+  def file_being_edited?
+    # check for common editor lock files
+    vim_lock_file = File.join(File.dirname(@file_path), '.'+File.basename(@file_path)+'.swp')
+    emacs_lock_file = File.join(File.dirname(@file_path), '.#'+File.basename(@file_path))
+
+    return true if File.exist?(vim_lock_file) || File.exist?(emacs_lock_file)
+
+    false
+  end
+
+  def file_has_been_edited?
+    File.mtime(@file_path) > @orig_mtime
+  end
+
+end
+
+end
diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb
index 734a879..01f3653 100644
--- a/lib/sup/modes/edit-message-mode.rb
+++ b/lib/sup/modes/edit-message-mode.rb
@@ -80,6 +80,7 @@ EOS
     k.add :edit_cc, "Edit Cc:", 'c'
     k.add :edit_subject, "Edit Subject", 's'
     k.add :edit_message, "Edit message", :enter
+    k.add :edit_message_async, "Edit message asynchronously", 'E'
     k.add :save_as_draft, "Save as draft", 'P'
     k.add :attach_file, "Attach a file", 'a'
     k.add :delete_attachment, "Delete an attachment", 'd'
@@ -184,7 +185,51 @@ EOS
     @edited
   end
 
+  def edit_message_async
+    @file = Tempfile.new ["sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}", ".eml"]
+    @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
+    @file.puts
+    @file.puts @body.join("\n")
+    @file.close
+
+    @mtime = File.mtime @file.path
+
+    # put up buffer saying you can now edit the message in another
+    # terminal or app, and continue to use sup in the meantime.
+    subject = @header["Subject"] || ""
+    @async_mode = EditMessageAsyncMode.new self, @file.path, subject
+    BufferManager.spawn "Waiting for message \"#{subject}\" to be finished", @async_mode
+
+    # hide ourselves, and wait for signal to resume from async mode ...
+    buffer.hidden = true
+  end
+
+  def edit_message_async_resume being_killed=false
+    buffer.hidden = false
+    @async_mode = nil
+    BufferManager.raise_to_front buffer if !being_killed
+
+    @edited = true if File.mtime(@file.path) > @mtime
+
+    header, @body = parse_file @file.path
+    @header = header - NON_EDITABLE_HEADERS
+    handle_new_text @header, @body
+    update
+
+    true
+  end
+
   def killable?
+    if [email-masked]?
+      return false if [email-masked]?
+      if File.mtime(@file.path) > @mtime
+        @edited = true
+        header, @body = parse_file @file.path
+        @header = header - NON_EDITABLE_HEADERS
+        handle_new_text @header, @body
+        update
+      end
+    end
     !edited? || BufferManager.ask_yes_or_no("Discard message?")
   end
_______________________________________________
Sup-devel mailing list
[email-masked]
http://rubyforge.org/mailman/listinfo/sup-devel

---

From: Hamish <[email-masked]>
Date: Sun, 20 Feb 2011 22:02:54 +0000

Hello

It is often useful to be able to look at messages while writing a message.
While I can exit my editor and look at other messages, and then come back to 
my editor, it would be nice to have both open at the same time. So I've
written some code that allows you to do that. When you are in
compose-mode/reply-mode/etc you can press 'E' and it will transform into
a buffer showing a file path of the file to edit, and a key to press ('E'
again) to say you've finished.

So you can open the file in another editor, whether on your desktop, in 
another terminal, in a screen session split so you can see both, or whatever.
While editing you can still navigate around sup's buffers and copy text,
email addresses etc into your editor and so on.

Then when you're done, you can exit the editor, navigate back to the edit
buffer and press 'E' to go back to normal edit-message-mode and carry on as
normal. 

As part of this, I've created a way to hide the original buffer. I think
I handle buffer killing properly in the various states (file still being
edited, file saved etc), but someone will doubtless find an edge case
...

Hamish Downer

---

From: Hamish <[email-masked]>
Date: Tue, 05 Apr 2011 00:01:18 +0100

Excerpts from Ico Doornekamp's message of Sun Feb 27 19:44:15 +0000 2011:
> > Excerpts from Ico Doornekamp's message of Sun Feb 27 15:31:14 +0000 2011:
> > > > Hamish, definitely thank you for doing something about this.  I
> > > > actually had a general question about this problem a while back - if
> > > > it is possible to edit the message independently via the approach you
> > > > took in this patch, why can't the editor be fired up in a
> > > > "nonblocking" kind of mode in the first place?  Something like a
> > > > fork()/exec() to start the editor, and then handling SIGCHLD rather
> > > > than calling a variant of wait() immediately.  The SIGCHLD handling
> > > > could check the return code from the editor, and then update the
> > > > message sending buffer appropriately.
> 
> When the infrastructure for background-editor hooks is there, I'm ready
> to try to make the hook!

OK, I've pushed a change to allow hooks for this functionality. For now
it is just on the async_message_edit branch (in the main gitorious
repo).

To quote from the hook text:

  Runs when 'H' is pressed in async edit mode. You can run whatever code
  you want here - though the default case would be launching a text
  editor. Your hook is assumed to not block, so you should use exec() or
  fork() to launch the editor.

  Once the hook has returned then sup will be responsive as usual. You will
  still need to press 'E' to exit this buffer and send the message.

  Variables:
  file_path: The full path to the file containing the message to be edited.

  Return value: None

If the hook did block then sup would be unresponsive, which would kind
of ruin the point of the exercise.

So this should work fine to launch a GUI text editor - your hook could
just be:

  fork { system "/usr/bin/gedit #{file_path} > /dev/null 2>&1" }

(Note that if you don't redirect all output to /dev/null then you can end
up with the screen being written all over, which gets a bit messy ...)

The next step would be to have the async mode exit as soon as the forked
hook code has finished. The best way I've thought of to do this is to
trap SIGUSR1. As long as there was only one async mode active this would
work fine, but if there were two there would be trouble. I guess I could
add a return value to the hook about whether to pay attention to
SIGUSR1. Then the BufferManager could know which buffer was waiting for
SIGUSR1. If a second buffer called the same hook while the first was
still waiting for the signal, then it should refuse to honour it. It
does feel a bit messy ... I might have a go at it at some point, but if
anyone has other ideas, then please shout out.

Hamish

---

From: Roni Choudhury <[email-masked]>
Date: Sat, 26 Feb 2011 19:51:05 -0700

Excerpts from Alvaro Herrera's message of 2011-02-26 18:15:31 -0700:
> Excerpts from Hamish's message of sáb feb 26 16:23:13 -0300 2011:
> I like this general idea very much.  The bit about having to copy/paste
> the file name is a bit of a pain, though.  I gave it a spin, and
> excepting that one infelicity, I found it pretty neat.  I noticed no
> problems at all.
> 
> The way I envision this working is with a "vim --servername foo" window
> being open at the time sup starts, and then "vim --remote" would open
> the file in that server each time I edit an email.  Most likely, the
> command to edit a file should be specified via a hook.
> 
> Thanks for your work on this.
> 

I promise, this is not to denigrate on your work at all... I have just
always been confused what would be wrong with the general approach I
just described.

roni

---

From: Ico Doornekamp <[email-masked]>
Date: Sun, 27 Feb 2011 16:31:14 +0100

> message sending buffer appropriately.

But in what tty should the forked editor run then? I guess the above
idea would work for spawning an editor under X, but can not be used when
running on a (remote) console. I guess the 'vim --remote' solution would
be feasable, bug I guess this is not simple enough to add as an
out-of-the-box solution.

I guess one solution could be to add master/slave pty support to sup,
allowing one or more regular console based editors instances (vim, nano,
joe, etc) inside sup, passing all keyboard input to the slave process
except for a few (configurable) keystrokes for switching sup buffers.

(I'd make an exception for running emacs inside sup, because emacs users
would probably prefer running their own lisp version of sup inside emacs
instead of emacs in sup)

A quick proto would probably be not very hard to do, it is mostly a
matter of passing keyboard commands from the master sup to the editor
running in the slave pty, passing pty output back to the terminal and
handling the proper signals and ioctls() for keeping track of the window
size. This will work as long as all terminal control characters are
simple passed transparantly from the slave editor to the terminal, but
things quickly get nasty when you would want to do the terminal
emulation inside sup, and this is probably necessary for properly
handling screen redrawing and resizing.

It's hard to guess how much work this would really be. Part of me says
it should not be too hard to find a simple terminal emulation
implentation in another piece of (L)GPL software and simply steal as
much as possible. Another part of my fears one would likely end up
reimplementing at least half of GNU screen, which is probably not a very
pleasant adventure.

Ico

-- 
:wq
^X^Cy^K^X^C^C^C^C

---

From: Ico Doornekamp <[email-masked]>
Date: Sun, 27 Feb 2011 20:44:15 +0100

* On Sun Feb 27 18:20:55 +0100 2011, Hamish wrote:
 
> If you have ssh'ed to a remote machine then I don't think there is a
> good way to manage this automatically without turning sup into screen
> (which I think would be a bad idea), though I wouldn't want to get in
> the way of imaginative hooks of course.

I've just hooked up a few lines of code (standalone, not part of sup)
that forks two editors (one joe, one vim) in ptys and handling both
terminal outputs with select(), allowing me to 'switch' between
processes on-the-fly using a magic keystroke. Redrawing is forced before
'switching' to a process by doing a TIOCSWINSZ ioctl and sending a
SIGWINCH twice, first with a fake, then with the true window size. This
is not quite a complete rewrite of screen, but seems to work quite well.
(Although I'm sure there are plenty of cases not covered by this crude
method of switching)

---

From: Alvaro Herrera <[email-masked]>
Date: Sat, 26 Feb 2011 22:15:31 -0300

-- 
Álvaro Herrera <[email-masked]>
_______________________________________________
Sup-devel mailing list
[email-masked]
http://rubyforge.org/mailman/listinfo/sup-devel

---

From: Ico Doornekamp <[email-masked]>
Date: Tue, 05 Apr 2011 06:38:06 +0200

>   still need to press 'E' to exit this buffer and send the message.

Sounds very useful to me!

> anyone has other ideas, then please shout out.

I might have understood the details of your implementation wrong, but
why would it not just be possible to act on a SIGCHLD and use wait() to
retreive the pid and exitstatus of the finished process ? The pid would
distinguish the different async handlers, resolving the ambiguity ?

Thanks!

---

From: Hamish <[email-masked]>
Date: Sun, 27 Feb 2011 17:20:55 +0000

> be feasable, bug I guess this is not simple enough to add as an

Now I've got the basic stuff working, I'd be up for adding to it.
Probably the best way would be to use a hook that could fire up
something using a fork()/exec().

The way I'd see this working is that it would only be useful on a
machine running X (or Wayland, or whatever the Mac GUI is called).

Basically I would say that if you want to edit your message outside of
sup on a remote machine (via ssh) you should use screen/tmux in split
mode, or have 2 terminals that ssh to the same server, or something like
that. Opening the file tends to involve typing:

$ vim /tmp/sup

and then pressing tab, so I don't think it's too onerous. And if you
turned on X forwarding and had xsel installed you could just press
<Enter> (to copy the path to the clipboard) and then paste that into the
other terminal.

Anyway, I'll have a think about how best to do a hook for this. I might
end up doing a blocking and a non-blocking variant - the blocking
variant could drop you straight back to reply-mode when it detects the
editor has exited, while the non-blocking mode would make it simpler to
write a hook, but you would have to manually exit async mode once
editing was done.

Hamish