=?utf-8?q?=5Bfolks=5D_Bug_660236_=E2=80=94_Paging_for_long_text_in_folks-?= =?utf-8?q?inspect?=



commit 19f77e2a6ca09fb74635c5387bd2e9a98aa7bfe3
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sat Jul 21 01:25:01 2012 +0100

    Bug 660236 â Paging for long text in folks-inspect
    
    Use $PAGER to allow paging of the output of really long commands in
    folks-inspect.
    
    Based on work by RaÃl Gutierrez Segales and Jeremy Whiting.
    
    Closes: https://bugzilla.gnome.org/show_bug.cgi?id=660236

 NEWS                       |    1 +
 tools/inspect/Makefile.am  |    1 +
 tools/inspect/inspect.vala |  320 ++++++++++++++++++++++++++++++++++++--------
 tools/inspect/utils.vala   |    6 +-
 4 files changed, 268 insertions(+), 60 deletions(-)
---
diff --git a/NEWS b/NEWS
index 10b41ad..d586eed 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,7 @@ Bugs fixed:
 â Bug 675223 â Shouldn't warn if CM does not implement any contact list
 â Bug 660128 â Most contacts don't have an avatar
 â Bug 652637 â Don't hold locks across async calls
+â Bug 660236 â Paging for long text in folks-inspect
 
 API changes:
 â Add AntiLinkable interface and implement it on Kf.Persona and Edsf.Persona
diff --git a/tools/inspect/Makefile.am b/tools/inspect/Makefile.am
index f341e4a..65a1b79 100644
--- a/tools/inspect/Makefile.am
+++ b/tools/inspect/Makefile.am
@@ -6,6 +6,7 @@ VALAFLAGS = \
 	--pkg=gobject-2.0 \
 	--pkg=gio-2.0 \
 	--pkg=gee-0.8 \
+	--pkg=posix \
 	--pkg=folks \
 	--pkg=build-conf \
 	$(NULL)
diff --git a/tools/inspect/inspect.vala b/tools/inspect/inspect.vala
index 9d66234..c3fe2a2 100644
--- a/tools/inspect/inspect.vala
+++ b/tools/inspect/inspect.vala
@@ -24,6 +24,7 @@ using Folks;
 using Readline;
 using Gee;
 using GLib;
+using Posix;
 
 /* We have to have a static global instance so that the readline callbacks can
  * access its data, since they don't pass closures around. */
@@ -32,11 +33,22 @@ static Inspect.Client main_client = null;
 public class Folks.Inspect.Client : Object
 {
   public HashMap<string, Command> commands;
+  private static bool _is_readline_installed;
   private MainLoop main_loop;
   public IndividualAggregator aggregator { get; private set; }
   public BackendStore backend_store { get; private set; }
   public SignalManager signal_manager { get; private set; }
 
+  /* To page or not to page? */
+  private termios _original_termios_p;
+  private bool _original_termios_p_valid = false;
+  private bool _quit_after_pager_dies = false;
+  private static Pid _pager_pid = 0;
+  private IOChannel? _stdin_channel = null;
+  private static uint _stdin_watch_id = 0;
+  private FileStream? _pager_channel = null;
+  private uint _pager_child_watch_id = 0;
+
   public static int main (string[] args)
     {
       Intl.bindtextdomain (BuildConf.GETTEXT_PACKAGE, BuildConf.LOCALE_DIR);
@@ -52,7 +64,7 @@ public class Folks.Inspect.Client : Object
         }
       catch (OptionError e1)
         {
-          stderr.printf ("Couldnât parse command line options: %s\n",
+          GLib.stderr.printf ("Couldnât parse command line options: %s\n",
               e1.message);
           return 1;
         }
@@ -63,8 +75,17 @@ public class Folks.Inspect.Client : Object
       /* Set up signal handling. */
       Unix.signal_add (Posix.SIGTERM, () =>
         {
-          /* Quit the client and let that exit the process. */
-          main_client.quit ();
+          /* Propagate the signal to our pager process, if it's running. */
+          if (main_client._pager_pid != 0)
+            {
+              main_client._quit_after_pager_dies = true;
+              kill (main_client._pager_pid, Posix.SIGTERM);
+            }
+          else
+            {
+              /* Quit the client and let that exit the process. */
+              main_client.quit ();
+            }
 
           return false;
         });
@@ -76,7 +97,7 @@ public class Folks.Inspect.Client : Object
         }
       else
         {
-          assert (args.length > 1);
+          GLib.assert (args.length > 1);
 
           /* Drop the first argument and parse the rest as a command line. If
            * the first argument is â--â then the command was passed after some
@@ -124,6 +145,23 @@ public class Folks.Inspect.Client : Object
 
   public void quit ()
     {
+      /* Stop paging. */
+      this._stop_paged_output ();
+
+      /* Uninstall readline, if it's installed. */
+      if (this._is_readline_installed)
+        {
+          this._uninstall_readline_and_stdin ();
+        }
+
+      /* Restore the user's original terminal settings, since the pager might've
+       * fiddled with them. */
+      if (this._original_termios_p_valid)
+        {
+          tcsetattr (Posix.STDIN_FILENO, Posix.TCSADRAIN,
+              this._original_termios_p);
+        }
+
       /* Kill the main loop. */
       this.main_loop.quit ();
     }
@@ -153,7 +191,7 @@ public class Folks.Inspect.Client : Object
       finally
         {
           this.aggregator.disconnect (signal_id);
-          assert (this.aggregator.is_quiescent == true);
+          GLib.assert (this.aggregator.is_quiescent == true);
         }
     }
 
@@ -171,7 +209,7 @@ public class Folks.Inspect.Client : Object
 
       if (command == null)
         {
-          stdout.printf ("Unrecognised command â%sâ.\n", command_name);
+          GLib.stdout.printf ("Unrecognised command â%sâ.\n", command_name);
           return;
         }
 
@@ -185,14 +223,14 @@ public class Folks.Inspect.Client : Object
             }
           catch (GLib.Error e1)
             {
-              stderr.printf ("Error preparing aggregator: %s\n", e1.message);
+              GLib.stderr.printf ("Error preparing aggregator: %s\n", e1.message);
               Process.exit (1);
             }
 
           /* Run the command */
           command.run (subcommand);
 
-          this.main_loop.quit ();
+          this.quit ();
         });
 
       this.main_loop.run ();
@@ -205,30 +243,28 @@ public class Folks.Inspect.Client : Object
        * stdin in our main loop, and passing character notifications to
        * readline. The main loop also processes all the folks events, thus
        * preventing us having to run a second thread. */
-      var stdin_channel = new IOChannel.unix_new (stdin.fileno ());
-      stdin_channel.add_watch (IOCondition.IN, (source, cond) =>
-        {
-          /* At least a single character is available on stdin, so let readline
-           * consume it. */
-          if ((cond & IOCondition.IN) != 0)
-            {
-              Readline.callback_read_char ();
-              return true;
-            }
 
-          assert_not_reached ();
-        });
+      /* Copy the user's original terminal settings. */
+      if (tcgetattr (Posix.STDIN_FILENO, out this._original_termios_p) == 0)
+        {
+          this._original_termios_p_valid = true;
+        }
 
       /* Handle SIGINT. */
       Unix.signal_add (Posix.SIGINT, () =>
         {
+          if (Client._is_readline_installed == false)
+            {
+              return true;
+            }
+
           /* Tidy up. */
           Readline.free_line_state ();
           Readline.cleanup_after_signal ();
           Readline.reset_after_signal ();
 
           /* Display a fresh prompt. */
-          stdout.printf ("^C");
+          GLib.stdout.printf ("^C");
           Readline.crlf ();
           Readline.reset_line_state ();
           Readline.replace_line ("", 0);
@@ -243,60 +279,226 @@ public class Folks.Inspect.Client : Object
       Readline.attempted_completion_function = Client.completion_cb;
       Readline.catch_signals = 0; /* go away, readline */
 
+      /* Install readline and the stdin handler. */
+      this._stdin_channel = new IOChannel.unix_new (GLib.stdin.fileno ());
+      this._install_readline_and_stdin ();
+
+      /* Run the aggregator and the main loop. */
+      this.aggregator.prepare ();
+
+      this.main_loop.run ();
+    }
+
+  private void _install_readline_and_stdin ()
+    {
+      /* stdin handler. */
+      this._stdin_watch_id = this._stdin_channel.add_watch (IOCondition.IN,
+          this._stdin_handler_cb);
+
       /* Callback for each character appearing on stdin. */
-      Readline.callback_handler_install ("> ", (_command_line) =>
-        {
-          if (_command_line == null)
-            {
-              /* EOF. If we've entered some text, don't do anything. Otherwise,
-               * quit. */
-              if (Readline.line_buffer != "")
-                {
-                  Readline.ding ();
-                  return;
-                }
+      Readline.callback_handler_install ("> ", Client._readline_handler_cb);
+      Client._is_readline_installed = true;
+    }
 
-              /* Quit. */
-              Readline.crlf ();
-              Readline.callback_handler_remove ();
-              main_client.quit ();
+  private void _uninstall_readline_and_stdin ()
+    {
+      Readline.callback_handler_remove ();
+      Client._is_readline_installed = false;
 
-              return;
-            }
+      Source.remove (this._stdin_watch_id);
+      this._stdin_watch_id = 0;
+    }
+
+  /* This should only ever be called while readline is installed. */
+  private bool _stdin_handler_cb (IOChannel source, IOCondition cond)
+    {
+      /* At least a single character is available on stdin, so let readline
+       * consume it. */
+      if ((cond & IOCondition.IN) != 0)
+        {
+          Readline.callback_read_char ();
+          return true;
+        }
 
-          var command_line = (!) _command_line;
+      assert_not_reached ();
+    }
 
-          command_line = command_line.strip ();
-          if (command_line == "")
+  private static void _readline_handler_cb (string? _command_line)
+    {
+      if (_command_line == null)
+        {
+          /* EOF. If we've entered some text, don't do anything. Otherwise,
+           * quit. */
+          if (Readline.line_buffer != "")
             {
-              /* If the user's entered a blank line, just display a new prompt
-               * without doing anything else. */
+              Readline.ding ();
               return;
             }
 
-          string subcommand;
-          string command_name;
-          Command command = main_client.parse_command_line (command_line,
-              out command_name, out subcommand);
+          /* Quit. */
+          main_client.quit ();
 
-          /* Run the command */
-          if (command != null)
+          return;
+        }
+
+      var command_line = (!) _command_line;
+
+      command_line = command_line.strip ();
+      if (command_line == "")
+        {
+          /* If the user's entered a blank line, just display a new prompt
+           * without doing anything else. */
+          return;
+        }
+
+      string subcommand;
+      string command_name;
+      Command command = main_client.parse_command_line (command_line,
+          out command_name, out subcommand);
+
+      /* Run the command */
+      if (command != null)
+        {
+          if (command_name != "quit")
             {
-              command.run (subcommand);
+              /* Start paging output. This is stopped when the pager dies. */
+              main_client._start_paged_output ();
             }
-          else
+
+          command.run (subcommand);
+
+          /* Close the stream to the pager so it knows it's reached EOF. */
+          main_client._pager_channel = null;
+          Utils.output_filestream = GLib.stdout;
+        }
+      else
+        {
+          GLib.stdout.printf ("Unrecognised command â%sâ.\n", command_name);
+        }
+
+      /* Store the command in the history, even if it failed */
+      Readline.History.add (command_line);
+    }
+
+  private void _start_paged_output ()
+    {
+      /* If the output is not a TTY (because it's a pipe or a file or a
+       * toaster) we don't page. */
+      if (!isatty (1))
+        {
+          return;
+        }
+
+      var pager = Environment.get_variable ("PAGER");
+      if (pager != null && pager == "")
+        {
+          return;
+        }
+
+      if (pager == null)
+        {
+          pager = "less -FRSX";
+        }
+
+      /* Convert command to null terminated array */
+      string[] args;
+      try
+        {
+          GLib.Shell.parse_argv (pager, out args);
+        }
+      catch (GLib.ShellError e)
+        {
+          warning ("Error parsing pager arguments: %s", e.message);
+          return;
+        }
+
+      /* Remove the readline and stdin handlers while the pager is running. */
+      this._uninstall_readline_and_stdin ();
+
+      /* Store the readline terminal state so that we can restore them
+       * after the pager has exited. */
+      Readline.prep_terminal (1);
+
+      /* Spawn the pager. */
+      int pager_fd = 0;
+
+      try
+        {
+          GLib.Process.spawn_async_with_pipes (null,
+              args,
+              null,
+              GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN |
+                  GLib.SpawnFlags.SEARCH_PATH |
+                  GLib.SpawnFlags.DO_NOT_REAP_CHILD /* we use a ChildWatch */,
+              null,
+              out this._pager_pid,
+              out pager_fd, // Std input
+              null, // Std out
+              null); // Std error
+        }
+      catch (SpawnError e2)
+        {
+          warning ("Error spawning pager: %s", e2.message);
+          return;
+        }
+
+      /* Redirect our output to the pager. */
+      this._pager_channel = FileStream.fdopen (pager_fd, "w");
+      Utils.output_filestream = this._pager_channel;
+
+      /* Watch for when the pager exits. */
+      this._pager_child_watch_id = ChildWatch.add (this._pager_pid,
+          (pid, status) =>
             {
-              stdout.printf ("Unrecognised command â%sâ.\n", command_name);
-            }
+              /* $PAGER died or was killed. */
+              this._stop_paged_output ();
+
+              /* Reset the readline state ready to display a new prompt. If the
+               * pager exited as the result of a signal, it probably didn't
+               * tidy up after itself (e.g. `less` leaves a colon prompt behind
+               * on the current line), so move to a new line. Doing this
+               * normally just looks a bit weird. */
+              if (Process.if_signaled (status))
+                {
+                  Readline.crlf ();
+                }
 
-          /* Store the command in the history, even if it failed */
-          Readline.History.add (command_line);
-        });
+              Readline.reset_line_state ();
+              Readline.replace_line ("", 0);
 
-      /* Run the aggregator and the main loop. */
-      this.aggregator.prepare ();
+              /* Are we supposed to quit (e.g. due to receiving a SIGTERM)? */
+              if (this._quit_after_pager_dies)
+                {
+                  main_client.quit ();
+                  return;
+                }
 
-      this.main_loop.run ();
+              /* Reinstall the readline handler and stdin handler. */
+              this._install_readline_and_stdin ();
+            });
+    }
+
+  private void _stop_paged_output ()
+    {
+      if (this._pager_pid == 0)
+        {
+          return;
+        }
+
+      Process.close_pid (this._pager_pid);
+      Source.remove (this._pager_child_watch_id);
+
+      this._pager_channel = null;
+      Utils.output_filestream = GLib.stdout;
+      this._pager_pid = 0;
+      this._pager_child_watch_id = 0;
+
+      /* Reset the terminal state (e.g. ECHO, which can get left turned
+       * off if the pager was killed uncleanly). */
+      Readline.deprep_terminal ();
+      Readline.free_line_state ();
+      Readline.cleanup_after_signal ();
+      Readline.reset_after_signal ();
     }
 
   private static Command? parse_command_line (string command_line,
diff --git a/tools/inspect/utils.vala b/tools/inspect/utils.vala
index 3774e63..1aeccc2 100644
--- a/tools/inspect/utils.vala
+++ b/tools/inspect/utils.vala
@@ -28,6 +28,9 @@ private class Folks.Inspect.Utils
   private static uint indentation = 0;
   private static string indentation_string = "";
 
+  /* The FILE we're printing output to. */
+  public static unowned FileStream output_filestream = GLib.stdout;
+
   public static void init ()
     {
       Utils.indentation_string = "";
@@ -107,7 +110,8 @@ private class Folks.Inspect.Utils
       /* FIXME: store the va_list temporarily to work around bgo#638308 */
       var valist = va_list ();
       string output = format.vprintf (valist);
-      stdout.printf ("%s%s\n", Utils.indentation_string, output);
+      var str = "%s%s\n".printf (Utils.indentation_string, output);
+      Utils.output_filestream.printf (str);
     }
 
   public static void print_individual (Individual individual,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]