changeset 1872:df1ca137b0cb

jp (blog/edit): editor arguments can now be specified on sat.conf, and default on are applied for known editors: - vim and gvim will open content and metadata file in a splitted window - gvim use --nofork option - installed a workaround for shlex.split not handling unicode before Python 2.7.3 - some fixes
author Goffi <goffi@goffi.org>
date Thu, 03 Mar 2016 15:57:06 +0100
parents 64a40adccba4
children 6ec54626610c
files frontends/src/jp/base.py frontends/src/jp/cmd_blog.py
diffstat 2 files changed, 67 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/jp/base.py	Wed Mar 02 19:53:53 2016 +0100
+++ b/frontends/src/jp/base.py	Thu Mar 03 15:57:06 2016 +0100
@@ -38,6 +38,15 @@
 import sat_frontends.jp
 from sat_frontends.jp.constants import Const as C
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
+import shlex
+
+if sys.version_info < (2, 7, 3):
+    # XXX: shlex.split only handle unicode since python 2.7.3
+    # this is a workaround for older versions
+    old_split = shlex.split
+    new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)]
+        if isinstance(s, unicode) else old_split(s, *a, **kw))
+    shlex.split = new_split
 
 try:
     import progressbar
--- a/frontends/src/jp/cmd_blog.py	Wed Mar 02 19:53:53 2016 +0100
+++ b/frontends/src/jp/cmd_blog.py	Thu Mar 03 15:57:06 2016 +0100
@@ -22,20 +22,32 @@
 from sat.core.i18n import _
 from sat.core.constants import Const as C
 from sat.tools import config
+from ConfigParser import NoSectionError, NoOptionError
 import json
 import os.path
 import os
 import time
 import tempfile
 import subprocess
+import shlex
 from sat.tools import common
 
 __commands__ = ["Blog"]
 
-SYNTAX_EXT = { '': 'txt', # used when the syntax is not found
-               "XHTML": "xhtml",
-               "markdown": "md"
-               }
+# extensions to use with known syntaxes
+SYNTAX_EXT = {
+    '': 'txt', # used when the syntax is not found
+    "XHTML": "xhtml",
+    "markdown": "md"
+    }
+
+# defaut arguments used for some known editors
+VIM_SPLIT_ARGS="-c 'vsplit|wincmd w|next|wincmd w'"
+EDITOR_ARGS_MAGIC = {
+    'vim': VIM_SPLIT_ARGS + ' {content_file} {metadata_file}',
+    'gvim': VIM_SPLIT_ARGS + ' --nofork {content_file} {metadata_file}',
+    }
+
 CONF_SYNTAX_EXT = 'syntax_ext_dict'
 BLOG_TMP_DIR="blog"
 # key to remove from metadata tmp file if they exist
@@ -80,11 +92,11 @@
             self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True)
             self.host.quit(1)
 
-    def buildMetadataFile(self, tmp_file_path, mb_data=None):
+    def buildMetadataFile(self, content_file_path, mb_data=None):
         """Build a metadata file using json
 
-        The file is named after tmp_file_path, with extension replaced by _metadata.json
-        @param tmp_file_path(str): path to the temporary file which will contain the body
+        The file is named after content_file_path, with extension replaced by _metadata.json
+        @param content_file_path(str): path to the temporary file which will contain the body
         @param mb_data(dict, None): microblog metadata (for existing items)
         @return (tuple[dict, str]): merged metadata put originaly in metadata file
             and path to temporary metadata file
@@ -103,7 +115,7 @@
             mb_data['title'] = self.args.title
 
         # the we create the file and write metadata there, as JSON dict
-        meta_file_path = os.path.splitext(tmp_file_path)[0] + '_metadata.json'
+        meta_file_path = os.path.splitext(content_file_path)[0] + '_metadata.json'
         # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
         if os.path.exists(meta_file_path):
             self.disp(u"metadata file {} already exists, this should not happen! Cancelling...", error=True)
@@ -114,46 +126,61 @@
 
         return mb_data, meta_file_path
 
-    def edit(self, sat_conf, tmp_file_path, tmp_file_obj, mb_data=None):
+    def edit(self, sat_conf, content_file_path, content_file_obj, mb_data=None):
         """Edit the file contening the content using editor, and publish it"""
         item_ori_mb_data = mb_data
         # we first create metadata file
-        meta_ori, meta_file_path = self.buildMetadataFile(tmp_file_path, item_ori_mb_data)
+        meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, item_ori_mb_data)
 
         # then we calculate hashes to check for modifications
         import hashlib
-        tmp_file_obj.seek(0)
-        tmp_ori_hash = hashlib.sha1(tmp_file_obj.read()).digest()
-        tmp_file_obj.close()
+        content_file_obj.seek(0)
+        tmp_ori_hash = hashlib.sha1(content_file_obj.read()).digest()
+        content_file_obj.close()
 
         # then we launch editor
         editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi')
-        editor_exit = subprocess.call([editor, tmp_file_path])
+        try:
+            # is there custom arguments in sat.conf ?
+            editor_args = config.getConfig(sat_conf, 'jp', 'blog_editor_args', Exception)
+        except (NoOptionError, NoSectionError):
+            # no, we check if we know the editor and have special arguments
+            editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), '')
+        try:
+            # we split the arguments and add the known fields
+            # we split arguments first to avoid escaping issues in file names
+            args = [a.format(content_file=content_file_path, metadata_file=meta_file_path) for a in shlex.split(editor_args)]
+        except ValueError as e:
+            self.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=editor_args, reason=e))
+            args = []
+        if not args:
+            args = [content_file_path]
+        editor_exit = subprocess.call([editor] + args)
 
         # we send the file if edition was a success
         if editor_exit != 0:
             self.disp(u"Editor exited with an error code, so temporary file has not be deleted, and blog item is not published.\nTou can find temporary file at {path}".format(
-                path=tmp_file_path), error=True)
+                path=content_file_path), error=True)
         else:
             try:
-                with open(tmp_file_path, 'rb') as f:
+                with open(content_file_path, 'rb') as f:
                     content = f.read()
                 with open(meta_file_path, 'rb') as f:
                     mb_data = json.load(f)
-            except OSError:
-                self.disp(u"Can write files at {file_path} and/or {meta_path}, have they been deleted? Cancelling edition".format(
-                    file_path=tmp_file_path, meta_path=meta_file_path), error=True)
+            except (OSError, IOError):
+                self.disp(u"Can read files at {content_path} and/or {meta_path}, have they been deleted?\nCancelling edition".format(
+                    content_path=content_file_path, meta_path=meta_file_path), error=True)
                 self.host.quit(1)
             except ValueError:
                 self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" +
-                    "You can find tmp file at {file_path} and temporary meta file at {meta_path}.".format(
-                    file_path=tmp_file_path, meta_path=meta_file_path), error=True)
+                    "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format(
+                    content_path=content_file_path, meta_path=meta_file_path), error=True)
                 self.host.quit(1)
 
             if not C.bool(mb_data.get('publish', "true")):
                 self.disp(u'Publication blocked by "publish" key in metadata, cancelling edition.\n\n' +
-                    "temporary file path:\t{file_path}\nmetadata file path:\t{meta_path}".format(
-                    file_path=tmp_file_path, meta_path=meta_file_path), error=True)
+                    "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format(
+                    content_path=content_file_path, meta_path=meta_file_path), error=True)
                 self.host.quit(0)
 
             if len(content) == 0:
@@ -174,11 +201,11 @@
                 try:
                     self.host.bridge.mbSend('', '', mb_data, self.profile)
                 except Exception as e:
-                    self.disp(u"Error while sending your blog, the temporary files have been kept at {file_path} and {meta_path}: {reason}".format(
-                        file_path=tmp_file_path, meta_path=meta_file_path, reason=e), error=True)
+                    self.disp(u"Error while sending your blog, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format(
+                        content_path=content_file_path, meta_path=meta_file_path, reason=e), error=True)
                     self.host.quit(1)
 
-            os.unlink(tmp_file_path)
+            os.unlink(content_file_path)
             os.unlink(meta_file_path)
 
     def start(self):
@@ -191,12 +218,12 @@
 
         # we now create a temporary file
         tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT[''])
-        tmp_file_obj, tmp_file_path = self.getTmpFile(sat_conf, tmp_suff)
+        content_file_obj, content_file_path = self.getTmpFile(sat_conf, tmp_suff)
 
         item_lower = self.args.item.lower()
         if item_lower == 'new':
             self.disp(u'Editing a new blog item', 2)
-            self.edit(sat_conf, tmp_file_path, tmp_file_obj)
+            self.edit(sat_conf, content_file_path, content_file_obj)
         elif item_lower == 'last':
             self.disp(u'Editing last published item', 2)
             try:
@@ -208,9 +235,9 @@
             content = mb_data['content_xhtml']
             if current_syntax != 'XHTML':
                 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile)
-            tmp_file_obj.write(content.encode('utf-8'))
-            tmp_file_obj.seek(0)
-            self.edit(sat_conf, tmp_file_path, tmp_file_obj, mb_data=mb_data)
+            content_file_obj.write(content.encode('utf-8'))
+            content_file_obj.seek(0)
+            self.edit(sat_conf, content_file_path, content_file_obj, mb_data=mb_data)
 
 
 class Import(base.CommandAnswering):