Ubuntu – have a window showing small live preview of another workspace

unityworkspaces

Is it possible to mirror a live section of one workspace so that it is visible in the current workspace as a window that can be moved around?

The other day I had a Windows 10 VM running on my Ubuntu 16.04 host that took a really long time to complete updating. I kept checking on its progress via Expo (Super+S) on Ubuntu. That got me thinking that this problem has most likely been solved already since tools such as simplescreenrecorder can be configured to record just a portion of the screen. However, I don't know the proper terminology to use for my Google search.

I'd like to see the 300×150 screenshot below in the form of a floating window (with live updates) in the top right corner of whichever workspace happens to be current.

enter image description here

Best Answer

EDIT

(New answer)

DONE.
The answer below is now available in a polished form, as an indicator, as a ppa for Trusty, Xenial, Yakkety and Zesty :

sudo apt-add-repository ppa:vlijm/windowspy
sudo apt-get update
sudo apt-get install windowspy

Th indicator (including preview window) is now nicely low on juice. Options include a settings window, setting window border size/color, window size.

enter image description here

In the meantime, I found it useful to keep an eye on the AU window; see if there are any messages :)


OLD ANSWER

(first second rough concept)

Have a minimized representation of a window on another workspace

To my own (big) surprise, it can be effectively done, be it with trickery and deceit; have an updated representation of a window on another workspace. Not fit to watch a movie, definitely good enough to keep an eye on a window elsewhere (example: my tv-card window):

How it works in practice

  1. With the window in front, press a shortcut key:

    enter image description here

    (the window will minimize)

  2. Move to another workspace, press the shortcut key again, a small representation of the window will appear, updated every 4 seconds:

    enter image description here

    The window always shows on top of other windows. As it is, the window is 300px (width), but can be set to any size.

  3. To end it, press (again) the shortcut key. The small window will close, you will move to the viewport of the original window, which will appear again, unminimized.

The scripts

  1. The control script

    #!/usr/bin/env python3
    import subprocess
    import os
    import sys
    import time
    
    # paths
    imagepath = os.path.join(os.environ["HOME"], ".showcase")
    wfile = os.path.join(imagepath, "currentwindow")
    vpfile = os.path.join(imagepath, "last_vp")
    # setup path
    if not os.path.exists(imagepath):
        os.mkdir(imagepath)
    
    def get(command):
        try:
            return subprocess.check_output(command).decode("utf-8").strip()
        except subprocess.CalledProcessError:
            pass
    
    def get_vp():
        open(vpfile, "wt").write(get(["wmctrl", "-d"]).split()[5])
    
    def run(command):
        subprocess.Popen(command)
    
    def convert_tohex(widxd):
        return widxd[:2]+((10-len(widxd))*"0")+widxd[2:]
    
    def check_windowtype(wid):
        check = get(["xprop", "-id", wid])
        return not any([s in check for s in [
            "_NET_WM_WINDOW_TYPE_DOCK",
            "_NET_WM_WINDOW_TYPE_DESKTOP"]])
    
    def edit_winprops(wid, convert=True):
        run(["xdotool", "windowminimize", wid])
        if convert:
            widxd = convert_tohex(hex(int(wid)))
        else:
            widxd = wid
        run(["wmctrl", "-i", "-r", widxd, "-b", "add,sticky"])
        get_vp()
        open(os.path.join(imagepath, "currentwindow"), "wt").write(widxd)
    
    def initiate_min():
        # if not, minmize window, write the file
        wid = get(["xdotool", "getactivewindow"])
        if check_windowtype(wid):
            edit_winprops(wid)
        else:
            pidinfo = [l.split() for l in wlist.splitlines()]
            match = [l for l in pidinfo if all([
                get(["ps", "-p", l[2], "-o", "comm="]) == "VirtualBox",
                not "Manager" in l])]
            if match:
                edit_winprops(match[0][0], convert=False)
    
    # windowlist
    wlist = get(["wmctrl", "-lp"])
    
    if "Window preview" in wlist:
        # kill the miniwindow
        pid = get(["pgrep", "-f", "showmin"])
        run(["kill", pid])
        window = open(wfile).read().strip()
        viewport = open(vpfile).read().strip()
        run(["wmctrl", "-o", viewport])
        time.sleep(0.3)
        run(["wmctrl", "-i", "-r", window, "-b", "remove,sticky"])
        run(["wmctrl", "-ia", window])
        os.remove(wfile)
    
    else:
        # check if windowfile exists
        wfileexists = os.path.exists(wfile)
        if wfileexists:
            # if file exists, try to run miniwindow
            window = open(wfile).read().strip()
            if window in wlist:
                # if the window exists, run!
                run(["showmin", window])
            else:
                # if not, minmize window, write the file
                initiate_min()
        else:
            # if not, minmize window, write the file
            initiate_min()
    
  2. The window representatiom

    #!/usr/bin/env python3
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, GObject
    from PIL import Image
    import os
    import subprocess
    import time
    from threading import Thread
    import sys
    
    wid = sys.argv[1]
    xsize = 300
    
    imagepath = os.path.join(os.environ["HOME"], ".showcase")
    if not os.path.exists(imagepath):
        os.mkdir(imagepath)
    img_in = os.path.join(imagepath, "image.png")
    resized = os.path.join(imagepath, "resized.png")
    
    def get_img():
        subprocess.Popen([
            "import", "-window", wid, "-resize", str(xsize),  resized
            ])
    
    get_img()
    
    class Splash(Gtk.Window):
    
        def __init__(self):
            Gtk.Window.__init__(self, title="Window preview")
            maingrid = Gtk.Grid()
            self.add(maingrid)
            self.image = Gtk.Image()
            # set the path to the image below
            self.resized = resized
            self.image.set_from_file(self.resized)
            maingrid.attach(self.image, 0, 0, 1, 1)
            maingrid.set_border_width(3)
            self.update = Thread(target=self.update_preview)
            self.update.setDaemon(True)
            self.update.start()
    
        def update_preview(self):
            while True:
                get_img()
                time.sleep(3)
                GObject.idle_add(
                    self.image.set_from_file, self.resized,
                    priority=GObject.PRIORITY_DEFAULT
                    )
    
    def miniwindow():
        window = Splash()
        window.set_decorated(False)
        window.set_resizable(False)
        window.set_keep_above(True)
        window.set_wmclass("ShowCase", "showcase")
        window.connect("destroy", Gtk.main_quit)
        GObject.threads_init()
        window.show_all()
        window.move(70, 50)
        Gtk.main()
    
    miniwindow()
    

How to use

  1. Install python3-pil, xdotool and wmctrl

    sudo apt-get install xdotool wmctrl python3-pil
    
  2. Create, if it doesn't exist yet, the directory ~/bin.

  3. Copy script 1, he control script, as (exactly) showcase_control (no extension) in ~/bin, and make it executable.
  4. Copy script 2, the mini- window script, as (exactly) showmin (no extension) in ~/bin, and make it executable.
  5. Log out and back in, and add the following command to a shortcut of your choice:

    showcase_control
    

    Choose: System Settings > "Keyboard" > "Shortcuts" > "Custom Shortcuts". Click the "+" and add the command:

    showcase_control
    

    and it should work!

    • Press the key once to grab the current window
    • move to the other workspace where you'd like the mini-window
    • Press again to show the miniwindow
    • Press again to move back to the original workspace, (automatically) un-minimize the original window and close the mini -one.

Downsides?

  • The setup, as it is currently, adds some work for your processor. On my (very) old system however, it adds (on average) appr. 4-5% I reckon, which I did not notice in any way.

    Update: It turns out import can resize the image in one step, together with fetching the window image. This means a substantial reduction in processor load. At the same time the refresh time is shorter (3 seconds now), still at lower "costs".

Explanation

  • My starting point was the way OP mentioned he wanted to use the option to keep an eye on a window on another workspace, waiting for either something to finish.
  • While literally having an exact (mini) copy of a window on another workspace seems impossible, we can make an image of an existing window with the import -command, once we have the window id. While this both works on minimized windows or windows without focus, there is however one issue: the window needs to be on the current workspace.
  • The trick is then to temporarily (while the mini- window is used) make the window "sticky" (be virtually available on all workspaces) with wmctrl, but minimized at the same time.
  • Since all is done automatically, the difference effectively is none, since also returning to the initial viewport, "un-" sticky the original window and un- minimizing it, is done automatically.

In short:

  1. Pressing the shortcut once: targeted window is made sticky, but minimized
  2. Pressing it again (presumably on another workspace): a small mini- version of the window appears in the upper left corner, updated once per four seconds.
  3. Pressing it again: mini- window is closed, the desktop moves to the initial workspace of the window, the window is restored un- sticky and un- minimized.

Specifically for VirtualBox

When the VBox window is in front, it turns out Ubuntu shortcut keys are disabled(!), so the control script needs to be launched in another way. Below a few brief ones.

Option 1

I edited the control script. Now only in the case of VirtualBox:

  • Click anywhere on the desktop, Then press your shortcut key. After that, simply use the shortcut key to show the window and exit.

    Explanation: The control script was made to exit if the window was of type "desktop", since you wouldn't want to minimize the desktop. Now the script first looks for possibly existing VirtualBox windows, to target, if the currently active window is the desktop.

Option 2

  • Copy the icon below (right- click -> save as), save it as minwinicon.png

    enter image description here

  • Copy the lines below into an empty file, save it as minwin.desktop in ~/.local/share/applications:

    [Desktop Entry]
    Type=Application
    Name=Window Spy
    Exec=showcase_control 
    Icon=/path/to/minwinicon.png
    StartupNotify=false
    

    You'd need to log out and back in for the launcher to "find" the local ~/bin path!
    Drag the icon on to the launcher to use it.

The second solution has an important downside: after using it from the launcher, it will keep blinking for a few seconds, waiting for a window to appear. During that, clicking again won't have any effect. That can be solved, as described here, but including that in this answer would really make it too long. If you want to use option two, please look into the link.