From 6dcff6ae0e1430bf8712b572beb66e301dc9c0c9 Mon Sep 17 00:00:00 2001
From: Vitaly Minko <vitaly.minko@gmail.com>
Date: Thu, 3 Oct 2019 08:26:56 +0300
Subject: [PATCH] Implemented 'stop_hidden' configuration option for
 applications.

This option lets user to stop application processes automatically when all
the application windows are hidden from the user. A window is considered hidden
when it's iconified or shaded or does not belong to the current desktop.

This option in intended for minimization of CPU usage and power consumption.
Some modern applications (like Web browsers) tend to consume significant amount
of CPU resources even when hidden.

There are three possible values of the option:
   'no'      - do not stop application when it hides (default value)
   'process' - stop just the process the hidden window belongs to
   'group'   - stop whole process group the application belongs to; some complex
               applications (like web browsers) emerge multiple child processed,
               so in order to stop such applications it's not enough just to
               stop one process.

Example:

  <application class="Firefox">
    <stop_hidden>group</stop_hidden>
  </application>

Please note that Firefox does not create a new group for its child
processes. You need to run it via `setsid firefox` in order to have a separate
group for all Firefox processes.
---
 data/rc.xml            |  7 ++++
 data/rc.xsd            |  8 ++++
 doc/rc-mouse-focus.xml |  7 ++++
 openbox/client.c       | 91 +++++++++++++++++++++++++++++++++++++++++-
 openbox/client.h       | 16 ++++++++
 openbox/config.c       | 14 +++++++
 openbox/config.h       |  1 +
 openbox/session.c      |  4 ++
 openbox/session.h      |  2 +-
 9 files changed, 148 insertions(+), 2 deletions(-)

diff --git a/data/rc.xml b/data/rc.xml
index 3e5554ba..c06d3839 100644
--- a/data/rc.xml
+++ b/data/rc.xml
@@ -732,6 +732,13 @@
     <fullscreen>yes</fullscreen>
     # make the window in fullscreen mode when it appears
 
+    <stop_hidden>process</stop_hidden>
+    # stop the application when it hides in order to minimize CPU usage
+    # and power consumption
+    #   'no'      - do not stop application when it hides (default value)
+    #   'process' - stop just the process the window belongs to
+    #   'group'   - stop whole process group the application belongs to
+
     <maximized>true</maximized>
     # 'Horizontal', 'Vertical' or boolean (yes/no)
   </application>
diff --git a/data/rc.xsd b/data/rc.xsd
index c8f5638b..a7c23bd4 100644
--- a/data/rc.xsd
+++ b/data/rc.xsd
@@ -254,6 +254,7 @@
             <xsd:element minOccurs="0" name="skip_pager" type="ob:bool"/>
             <xsd:element minOccurs="0" name="skip_taskbar" type="ob:bool"/>
             <xsd:element minOccurs="0" name="fullscreen" type="ob:bool"/>
+            <xsd:element minOccurs="0" name="stop_hidden" type="ob:stop_mode"/>
             <xsd:element minOccurs="0" name="maximized" type="ob:maximization"/>
         </xsd:all>
         <!-- at least one of these must be present -->
@@ -485,6 +486,13 @@
             <xsd:enumeration value="Floating"/>
         </xsd:restriction>
     </xsd:simpleType>
+    <xsd:simpleType name="stop_mode">
+        <xsd:restriction base="xsd:string">
+            <xsd:enumeration value="no"/>
+            <xsd:enumeration value="process"/>
+            <xsd:enumeration value="group"/>
+        </xsd:restriction>
+    </xsd:simpleType>
     <xsd:simpleType name="keyname">
         <xsd:restriction base="xsd:string">
             <!-- FIXME: M, Mod2, Mod5 in addition to S, A, C -->
diff --git a/doc/rc-mouse-focus.xml b/doc/rc-mouse-focus.xml
index dc7f2e98..3e29f73b 100644
--- a/doc/rc-mouse-focus.xml
+++ b/doc/rc-mouse-focus.xml
@@ -625,6 +625,13 @@
     <fullscreen>yes</fullscreen>
     # make the window in fullscreen mode when it appears
 
+    <stop_hidden>process</stop_hidden>
+    # stop the application when it hides in order to minimize CPU usage
+    # and power consumption
+    #   'no'      - do not stop application when it hides (default value)
+    #   'process' - stop just the process the window belongs to
+    #   'group'   - stop whole process group the application belongs to
+
     <maximized>true</maximized>
     # 'Horizontal', 'Vertical' or boolean (yes/no)
   </application>
diff --git a/openbox/client.c b/openbox/client.c
index 3ff278ae..04b66c08 100644
--- a/openbox/client.c
+++ b/openbox/client.c
@@ -625,6 +625,9 @@ void client_unmanage(ObClient *self)
     prompt_unref(self->kill_prompt);
     self->kill_prompt = NULL;
 
+    /* if client was stopped, continue executing */
+    client_continue_hidden(self);
+
     client_list = g_list_remove(client_list, self);
     stacking_remove(self);
     window_remove(self->window);
@@ -968,6 +971,8 @@ static ObAppSettings *client_get_settings_state(ObClient *self)
         self->skip_pager = !!settings->skip_pager;
     if (settings->skip_taskbar != -1)
         self->skip_taskbar = !!settings->skip_taskbar;
+    if (settings->stop_hidden != OB_CLIENT_STOP_MODE_NONE)
+        self->stop_hidden = settings->stop_hidden;
 
     if (settings->max_vert != -1)
         self->max_vert = !!settings->max_vert;
@@ -1043,6 +1048,7 @@ static void client_restore_session_state(ObClient *self)
     self->max_horz = self->session->max_horz;
     self->max_vert = self->session->max_vert;
     self->undecorated = self->session->undecorated;
+    self->stop_hidden = self->session->stop_hidden;
 }
 
 static gboolean client_restore_session_stacking(ObClient *self)
@@ -2510,8 +2516,11 @@ static void client_change_wm_state(ObClient *self)
         (self->desktop != DESKTOP_ALL && self->desktop != screen_desktop))
     {
         self->wmstate = IconicState;
-    } else
+        client_stop_hidden(self);
+    } else {
         self->wmstate = NormalState;
+        client_continue_hidden(self);
+    }
 
     if (old != self->wmstate) {
         OBT_PROP_MSG(ob_screen, self->window, KDE_WM_CHANGE_STATE,
@@ -2608,6 +2617,24 @@ ObClient *client_search_focus_group_full(ObClient *self)
     return NULL;
 }
 
+gboolean client_is_hidden(ObClient *self)
+{
+    return (self->wmstate == IconicState);
+}
+
+gboolean client_is_group_hidden(ObClient *self)
+{
+    GSList *it;
+
+    if (self->group) {
+        for (it = self->group->members; it; it = g_slist_next(it)) {
+            ObClient *c = it->data;
+            if (!client_is_hidden(c)) return FALSE;
+        }
+    }
+    return TRUE;
+}
+
 gboolean client_has_parent(ObClient *self)
 {
     return self->parents != NULL;
@@ -3706,6 +3733,68 @@ void client_kill(ObClient *self)
     }
 }
 
+void client_stop_hidden(ObClient *self)
+{
+    if (self->stop_hidden == OB_CLIENT_STOP_MODE_NONE)
+        return;
+
+    if (client_on_localhost(self) && self->pid) {
+        /* running on the local host */
+
+        if (!client_is_group_hidden(self)) {
+            return;
+        }
+
+        if (self->stop_hidden == OB_CLIENT_STOP_MODE_PROCESS) {
+            ob_debug("client_stop_hidden is stopping process with pid %lu",
+                     self->pid);
+            kill(self->pid, SIGSTOP);
+        } else if (self->stop_hidden == OB_CLIENT_STOP_MODE_GROUP) {
+            ob_debug("client_stop_hidden is stopping group with pid %lu",
+                     self->pid);
+            pid_t pgid = getpgid(self->pid);
+            if (pgid != -1) {
+                killpg(self->pid, SIGSTOP);
+            }
+        } else {
+            ob_debug("client_stop_hidden: unknown value of stop_hidden");
+            return;
+        }
+    }
+    else {
+        /* do nothing, running on a remote host */
+    }
+}
+
+void client_continue_hidden(ObClient *self)
+{
+    if (self->stop_hidden == OB_CLIENT_STOP_MODE_NONE)
+        return;
+
+    if (client_on_localhost(self) && self->pid) {
+        /* running on the local host */
+        if (self->stop_hidden == OB_CLIENT_STOP_MODE_PROCESS) {
+            ob_debug("client_continue_hidden is continuing process with pid %lu",
+                      self->pid);
+            kill(self->pid, SIGCONT);
+        } else if (self->stop_hidden == OB_CLIENT_STOP_MODE_GROUP) {
+            ob_debug("client_continue_hidden is continuing group with pid %lu",
+                      self->pid);
+            pid_t pgid = getpgid(self->pid);
+            if (pgid != -1) {
+                killpg(self->pid, SIGCONT);
+            }
+        } else {
+            ob_debug("client_continue_hidden: unknown value"
+                     " of stop_hidden");
+            return;
+        }
+    }
+    else {
+        /* do nothing, running on a remote host */
+    }
+}
+
 void client_hilite(ObClient *self, gboolean hilite)
 {
     if (self->demands_attention == hilite)
diff --git a/openbox/client.h b/openbox/client.h
index 11a01400..d574740a 100644
--- a/openbox/client.h
+++ b/openbox/client.h
@@ -69,6 +69,14 @@ typedef enum
     OB_CLIENT_FUNC_UNDECORATE = 1 << 9  /*!< Allow to be undecorated */
 } ObFunctions;
 
+/*! The way to stop client when it hides */
+typedef enum
+{
+    OB_CLIENT_STOP_MODE_NONE,    /*!< Do not stop client */
+    OB_CLIENT_STOP_MODE_PROCESS, /*!< Stop the client process only */
+    OB_CLIENT_STOP_MODE_GROUP,   /*!< Stop process group the client belongs to */
+} ObClientStopMode;
+
 struct _ObClient
 {
     ObWindow obwin;
@@ -272,6 +280,8 @@ struct _ObClient
     gboolean skip_pager;
     /*! The window should not be displayed by taskbars */
     gboolean skip_taskbar;
+    /*! How to stop the process when it hides or iconifies */
+    ObClientStopMode stop_hidden;
     /*! The window is a 'fullscreen' window, and should be on top of all
       others */
     gboolean fullscreen;
@@ -548,6 +558,12 @@ void client_close(ObClient *self);
 /*! Kill the client off violently */
 void client_kill(ObClient *self);
 
+/*! Stop the client process when all its windows are hidded */
+void client_stop_hidden(ObClient *self);
+
+/*! Continue executing the client process when one of its windows appears */
+void client_continue_hidden(ObClient *self);
+
 /*! Sends the window to the specified desktop
   @param donthide If TRUE, the window will not be shown/hidden after its
                   desktop has been changed. Generally this should be FALSE.
diff --git a/openbox/config.c b/openbox/config.c
index dad5d1bf..4310906b 100644
--- a/openbox/config.c
+++ b/openbox/config.c
@@ -123,6 +123,7 @@ ObAppSettings* config_create_app_settings(void)
     settings->fullscreen = -1;
     settings->max_horz = -1;
     settings->max_vert = -1;
+    settings->stop_hidden = OB_CLIENT_STOP_MODE_NONE;
     return settings;
 }
 
@@ -148,6 +149,7 @@ void config_app_settings_copy_non_defaults(const ObAppSettings *src,
     copy_if(fullscreen, -1);
     copy_if(max_horz, -1);
     copy_if(max_vert, -1);
+    copy_if(stop_hidden, OB_CLIENT_STOP_MODE_NONE);
 
     if (src->pos_given) {
         dst->pos_given = TRUE;
@@ -328,6 +330,18 @@ static void parse_single_per_app_settings(xmlNodePtr app,
         if (!obt_xml_node_contains(n, "default"))
             settings->fullscreen = obt_xml_node_bool(n);
 
+    if ((n = obt_xml_find_node(app->children, "stop_hidden")))
+        if (!obt_xml_node_contains(n, "default")) {
+            gchar *s = obt_xml_node_string(n);
+            if (!g_ascii_strcasecmp(s, "no"))
+                settings->stop_hidden = OB_CLIENT_STOP_MODE_NONE;
+            else if (!g_ascii_strcasecmp(s, "process"))
+                settings->stop_hidden = OB_CLIENT_STOP_MODE_PROCESS;
+            else if (!g_ascii_strcasecmp(s, "group"))
+                settings->stop_hidden = OB_CLIENT_STOP_MODE_GROUP;
+            g_free(s);
+        }
+
     if ((n = obt_xml_find_node(app->children, "maximized"))) {
         if (!obt_xml_node_contains(n, "default")) {
             gchar *s = obt_xml_node_string(n);
diff --git a/openbox/config.h b/openbox/config.h
index 96a66cf1..567e88e6 100644
--- a/openbox/config.h
+++ b/openbox/config.h
@@ -42,6 +42,7 @@ struct _ObAppSettings
     GPatternSpec *group_name;
     GPatternSpec *title;
     ObClientType  type;
+    ObClientStopMode stop_hidden;
 
     GravityPoint position;
     gboolean pos_given;
diff --git a/openbox/session.c b/openbox/session.c
index d6c6f767..96fa066f 100644
--- a/openbox/session.c
+++ b/openbox/session.c
@@ -603,6 +603,8 @@ static gboolean session_save_to_file(const ObSMSaveData *savedata)
                 fprintf(f, "\t<max_vert />\n");
             if (c->undecorated)
                 fprintf(f, "\t<undecorated />\n");
+            if (c->stop_hidden)
+                fprintf(f, "\t<stop_hidden />\n");
             if (savedata->focus_client == c)
                 fprintf(f, "\t<focused />\n");
             fprintf(f, "</window>\n\n");
@@ -784,6 +786,8 @@ static void session_load_file(const gchar *path)
             obt_xml_find_node(node->children, "max_vert") != NULL;
         state->undecorated =
             obt_xml_find_node(node->children, "undecorated") != NULL;
+        state->stop_hidden =
+            obt_xml_find_node(node->children, "stop_hidden") != NULL;
         state->focused =
             obt_xml_find_node(node->children, "focused") != NULL;
 
diff --git a/openbox/session.h b/openbox/session.h
index f37e2111..a65f8397 100644
--- a/openbox/session.h
+++ b/openbox/session.h
@@ -34,7 +34,7 @@ struct _ObSessionState {
     gboolean shaded, iconic, skip_pager, skip_taskbar, fullscreen;
     gboolean above, below, max_horz, max_vert, undecorated;
     gboolean focused;
-
+    ObClientStopMode stop_hidden;
     gboolean matched;
 };
 
-- 
2.21.0

