Ubuntu – Can I have a countdown window show the time until next suspend

gnomeguipower-managementscriptssuspend

Occasionally I leave my computer unsuspended when I need to go somewhere but I am leaving it with people I absolutely trust and I will be back shortly so no real need to suspend (like if it's family and I'm just popping to the loo which is only a small distance away from where I am and you know, it's not convenient to always suspend my machine when it's like this), or there may be other reasons why I want this, like to know if I should run to my machine to wiggle the mouse before it goes to sleep, or if I can go to my computer at a slower pace.

Anyway, to get to the point, I would like to be able to launch a countdown (preferably in a see-through (the see-through bit is not a must) window on top of everything in the top right hand corner of my screen just below the top bar) display which will show me a countdown to the next suspend time.

This could either reset the timer every time I interact with the machine or somehow directly interface with the suspend system to tell when the next suspend is (because I assume that there is some sort of countdown). How could something like this be achieved (of course I would not want the window to always be open but instead for me to be able to run a command any time that would start the window, or it could even be a countdown in a Terminal window that I would just move or would automatically be located in the top right hand corner if the GUI bit is at all problematic)?

I am running Ubuntu GNOME 15.10 with GNOME 3.18 but the solution should also work with Ubuntu GNOME 16.04 with GNOME 3.20 as I plan to upgrade soon.

Best Answer

EDIT

In the original answer, further below, the countdown window appeared after an arbitrary idle time. Re- reading your question, you might want it permanently. The permanent version is below (which is simpler), the original answer further down.


1a. Version, permanently showing countdown time

The solution is a background script, showing a semi- transparent countdown window. The window behaves like a notification: it is always visible on top, but (of course) you can work in your other windows as usual:

enter image description here enter image description here enter image description here enter image description here

The initial time is the idle- time before suspend should be activated. The time is reset on mouse- keyboard events.

As the image shows, the script comes with different pre- set color options (see further below).

How to setup

  1. The script needs xprintidle:

    sudo apt-get install xprintidle
    
  2. Copy the script below into an empty file, save it as countdown.py
  3. Run it with the idle time as argument:

    python3 /path/to/countdown.py <idle_time>
    

    e.g.

    python3 /path/to/countdown.py 300
    

    to enter suspend after 5 minutes.

  4. If all works fine, add it to startup applications: Dash > Startup Applications > Add. Add the command:

     /path/to/runner.py <idle_time>
    

The script

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject, Pango
from threading import Thread
import subprocess
import time
import signal
import sys
import os

# --- set the color (index) below (1 is the first)
color = 1
# ---
textcolors = ["grey", "orange", "green", "blue", "white"]
# --- don't change anything below
txtcolor = textcolors[color-1]

countdown = int(sys.argv[1])
susp = os.path.dirname(os.path.realpath(__file__))+"/susp.sh"

class CountDown(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self)
        maingrid = Gtk.Grid()
        self.add(maingrid)
        maingrid.set_border_width(40)
        # set initial text for the spash window
        self.label = Gtk.Label(convert_seconds(countdown))
        self.label.modify_font(Pango.FontDescription('Ubuntu 22'))
        self.label.set_width_chars(10) 
        maingrid.attach(self.label, 0, 0, 1, 1)
        self.update = Thread(target=self.start_countdown, args=[countdown])
        # daemonize the thread
        self.update.setDaemon(True)
        self.update.start()

    def start_countdown(self, countdown):
        idle1 = idletime()
        t = countdown
        while True:
            time.sleep(1)
            idle2 = idletime()
            if idle2 < idle1:
                t = countdown
            else:
                t -= 1
            if t <= 0:
                subprocess.Popen(["systemctl", "suspend"])
            GObject.idle_add(self.label.set_text, convert_seconds(t),
                priority=GObject.PRIORITY_DEFAULT)
            idle1 = idle2

    def stop(self):
        Gtk.main_quit()

def get_screen():
    scr = [s.split("x") for s in subprocess.check_output([
        "xrandr"]).decode("utf-8").split() if "+0+0" in s][0]
    return int(scr[0]) - 300

def convert_seconds(sec):
    timedisplay = [
        str(int(sec/3600)),
        str(int((sec % 3600)/60)),
        str(int(sec % 60)),
        ]
    for i, n in enumerate(timedisplay):
        if len(n) == 1:
            timedisplay[i] = "0"+n
    return ":".join(timedisplay)

def idletime():
    return int(subprocess.check_output(
        "xprintidle"
        ).decode("utf-8").strip())/1000

def splashwindow():
    window = CountDown()
    window.set_decorated(False)
    window.set_resizable(False)
    window.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(0,0,0,1))
    window.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse(txtcolor))
    window.set_opacity(0.6)
    window.move(get_screen(), 80)
    window.set_keep_above(True)
    window.show_all()
    Gtk.main()

GObject.threads_init()
splashwindow()

Note

The text color can be changed, as explained at the very bottom of the second version of the answer.

1b. As requested in a comment: luxury version of the same script: text color changes to yellow if half the time passed, to red 30 seconds before suspend.

enter image description here > enter image description here > enter image description here

Use it exactly as 1a.

The script

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject, Pango
from threading import Thread
import subprocess
import time
import signal
import sys
import os

# --- set the color (index) below (1 is the first)
color = 1
# ---
textcolors = ["grey", "orange", "green", "blue", "white", "yellow", "red"]
# --- don't change anything below

txtcolor = textcolors[color-1]
al_cl1 = textcolors[5]; al_cl2 = textcolors[6]
countdown = int(sys.argv[1])
alarm1 = int(countdown/2)
alarm2 = 30

class CountDown(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self)
        maingrid = Gtk.Grid()
        self.add(maingrid)
        maingrid.set_border_width(40)
        # set initial text for the spash window
        self.label = Gtk.Label(convert_seconds(countdown))
        self.label.modify_font(Pango.FontDescription('Ubuntu 22'))
        self.label.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse(txtcolor))
        self.label.set_width_chars(10) 
        maingrid.attach(self.label, 0, 0, 1, 1)
        self.update = Thread(target=self.start_countdown, args=[countdown])
        # daemonize the thread
        self.update.setDaemon(True)
        self.update.start()

    def mod_color(self, color):
        self.label.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse(color))

    def start_countdown(self, countdown):
        idle1 = idletime()
        t1 = countdown
        t2 = countdown
        while True:
            time.sleep(1)
            idle2 = idletime()
            if idle2 < idle1:
                t2 = countdown
                if t1 <= alarm1:
                    # print("change textcolor default")
                    GObject.idle_add(self.mod_color, txtcolor,
                        priority=GObject.PRIORITY_DEFAULT)
            else:
                t2 -= 1
            if all([t2 <= alarm2, t1 > alarm2]):
                # print("change textcolor red")
                GObject.idle_add(self.mod_color, al_cl2,          
                    priority=GObject.PRIORITY_DEFAULT)
            elif all([t2 <= alarm1, t1 > alarm1]):
                # print("change textcolor yellow")
                GObject.idle_add(self.mod_color, al_cl1,          
                    priority=GObject.PRIORITY_DEFAULT) 
            if t2 <= 0:
                subprocess.Popen(["systemctl", "suspend"])
            GObject.idle_add(self.label.set_text, convert_seconds(t2),
                priority=GObject.PRIORITY_DEFAULT)
            idle1 = idle2
            t1 = t2

    def stop(self):
        Gtk.main_quit()

def get_screen():
    scr = [s.split("x") for s in subprocess.check_output([
        "xrandr"]).decode("utf-8").split() if "+0+0" in s][0]
    return int(scr[0]) - 300

def convert_seconds(sec):
    timedisplay = [
        str(int(sec/3600)),
        str(int((sec % 3600)/60)),
        str(int(sec % 60)),
        ]
    for i, n in enumerate(timedisplay):
        if len(n) == 1:
            timedisplay[i] = "0"+n
    return ":".join(timedisplay)

def idletime():
    return int(subprocess.check_output(
        "xprintidle"
        ).decode("utf-8").strip())/1000

def splashwindow():
    window = CountDown()
    window.set_decorated(False)
    window.set_resizable(False)
    window.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(0,0,0,1))
    window.set_opacity(0.6)
    window.move(get_screen(), 80)
    window.set_keep_above(True)
    window.show_all()
    Gtk.main()

GObject.threads_init()
splashwindow()




2. Original answer: version, showing countdown time after x idle time

The setup below will show a countdown (during an arbitrary time length) to the next suspend:

enter image description here

The window is always on top of all other windows, exactly like the notification bubbles.

The setup replaces the "normal" suspend settings, which means you need to disable suspend from system settings.

About the solution

The command to suspend in the script is:

 systemctl suspend

which doesn't require sudo. The consequence is that you will need at least 15.04 to use this solution.
The script was written and tested on Ubuntu (Unity) 15.10, but there is no specific code in it that should be Unity specific. I assume it works fine on all default Ubuntu versions > 15.04

How it works

To setup (detailed version further below), simply copy the three scripts involved into one and the same directory, exactly named as indicated. To run, simply run the main script (running the time- check).

  • If the idle time exceeds a certain limit, the countdown window is called.
  • If during countdown the computer becomes un- idle (mouse- or keyboard event) the window is closed (its pid is killed).
  • If the timer has ended its count down, it runs a simple script to suspend

How to setup

  1. The script needs xprintidle:

    sudo apt-get install xprintidle
    
  2. Copy the three scripts below into separate empty files, save the in one and the same directory, exactly named as indicated:

    A. save (exactly) as win.py:

    #!/usr/bin/env python3
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, Gdk, GObject, Pango
    from threading import Thread
    import subprocess
    import time
    import signal
    import sys
    import os
    
    # --- set the color (index) below (1 is the first)
    color = 1
    # ---
    textcolors = ["grey", "orange", "green", "blue", "white"]
    # --- don't change anything below
    txtcolor = textcolors[color-1]
    
    countdown = int(sys.argv[1])
    susp = os.path.dirname(os.path.realpath(__file__))+"/susp.sh"
    
    class CountDown(Gtk.Window):
    
        def __init__(self):
            Gtk.Window.__init__(self)
            maingrid = Gtk.Grid()
            self.add(maingrid)
            maingrid.set_border_width(40)
            # set initial text for the spash window
            self.label = Gtk.Label(convert_seconds(countdown))
            self.label.modify_font(Pango.FontDescription('Ubuntu 22'))
            self.label.set_width_chars(10) 
            maingrid.attach(self.label, 0, 0, 1, 1)
            self.update = Thread(target=self.start_countdown, args=[countdown])
            # daemonize the thread
            self.update.setDaemon(True)
            self.update.start()
    
        def start_countdown(self, countdown):
            t = countdown
            while t > 0:
                time.sleep(1)
                t -= 1
                GObject.idle_add(self.label.set_text, convert_seconds(t),
                    priority=GObject.PRIORITY_DEFAULT)
                print(t)
            subprocess.Popen(["/bin/bash", susp])
            self.stop()
    
        def stop(self):
            Gtk.main_quit()
    
    def get_screen():
        scr = [s.split("x") for s in subprocess.check_output([
            "xrandr"]).decode("utf-8").split() if "+0+0" in s][0]
        return int(scr[0]) - 300
    
    def convert_seconds(sec):
        timedisplay = [
            str(int(sec/3600)),
            str(int((sec % 3600)/60)),
            str(int(sec % 60)),
            ]
        for i, n in enumerate(timedisplay):
            if len(n) == 1:
                timedisplay[i] = "0"+n
        return ":".join(timedisplay)
    
    def splashwindow():
        window = CountDown()
        window.set_decorated(False)
        window.set_resizable(False)
        window.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(0,0,0,1))
        window.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse(txtcolor))
        window.set_opacity(0.6)
        window.move(get_screen(), 80)
        window.set_keep_above(True)
        window.show_all()
        Gtk.main()
    
    GObject.threads_init()
    splashwindow()
    

    B. Save exactly as runner.py:

    #!/usr/bin/env python3
    import subprocess
    import time
    import os
    import sys
    
    window_mod = os.path.dirname(os.path.realpath(__file__))+"/win.py" 
    
    suspend = int(sys.argv[1])
    countdown = int(sys.argv[2])
    
    w = False
    while True:  
        time.sleep(1)
        idletime = int(subprocess.check_output(
            "xprintidle"
            ).decode("utf-8").strip())/1000
        if all([idletime > suspend-countdown, w == False]):
            subprocess.Popen(["python3", window_mod, str(countdown)])
            w = True
        elif all([idletime < suspend-countdown, w == True]):
            try:
                procdata = subprocess.check_output([
                    "pgrep", "-f", window_mod
                    ]).decode("utf-8").strip()
                procs = procdata.splitlines()
            except subprocess.CalledProcessError:
                pass
            else:
                for p in procs:
                    subprocess.Popen(["kill", p])
            w = False
    

    C. Save (exactly) as susp.sh:

    #!/bin/bash
    
    sleep 3
    systemctl suspend
    
  3. Make all three scripts executable, and again, make sure they are in one and the same directory.

  4. You're practically done.

    • Disable your "usual" suspend settings
    • Test- run the script the suspend time (idle time before suspend should be applied), and the count down time (in seconds) as arguments, e.g.:

      /path/to/runner.py 600 300
      

      to set idle time to 10 minutes, counter starts 5 minutes before suspend.

  5. If all works fine, add it to startup applications: Dash > Startup Applications > Add. Add the command:

     /path/to/runner.py <idle_time> <countdown_time>
    

Notes

  1. In the head section of the win.py, you can set different colors for the displayed text:

    # --- set the color (index) below (1 is the first)
    color = 1
    # ---
    textcolors = ["grey", "orange", "green", "blue", "white"]
    
  2. Playing with the values in the lines:

    maingrid.set_border_width(10)
    

    and

    return int(scr[0]) - 200 
    

    (from the function get_screen, where 200 is the distance of the left side of the window to the right side of the screen), and

    window.move(get_screen(), 35)
    

    (where 35 is the distance between the window and the top of the screen), you can easily change the geometry of the window, e.g.:

    enter image description here

Have fun :)