Can I minimize a window into a box on Unity?

To my own surprise, it works quite nicely, As long as you do not have too many other things on your desktop.

I worked with it for a while, and it seems a weird, but strangely enough nice alternative to frequent workspace switches. Refreshing for its simplicity.

In practice

The solution is actually pretty much what you describe:

  • Pressing a key combination will "box" the window on your desktop, from a window:

    enter image description here

    into an icon, with the application's appearance:

    enter image description here

  • Double click the icon, and the window will re- appear and the icon will vanish.

How it works

The short story (explanation):

  • When pressing the shortcut key, the script is called with the argument box:

    windowbox box
    
  • The script then:

    • reads the window id of the frontmost window
    • checks if it is a "normal" window (you wouldn't want to unmap your desktop for example)
    • Looks up the process name of the application, owning the window.
    • Looks up the corresponding icon in the corresponding application's .desktop file in /usr/share/applications
    • creates a uniquely named .desktop file, with an Exec= line which calls the script (when double-clicked) with the argument show:

      windowbox show
      

The .desktop file will add a number of additional arguments arguments, such as the window id, the (file-) name of the .desktop file.

Subsequently:

  • The .desktop file is then made executable, to make it a double- clickable object.

  • When the .desktop file is double clicked, the window is (re-) mapped, the .desktop file is removed from your desktop.

How to set up

  1. Like practically always, when you want to play around with windows, the script needs both wmctrl and xdotool:

    sudo apt-get install xdotool wmctrl
    
  2. Create the directory ~/bin (~ stands for your home directory)
  3. Copy the script below into an empty file, save it as windowbox (no extension) in ~/bin.

    #!/usr/bin/env python3
    import subprocess
    import sys
    import os
    
    # --- On Unity, there is a (y-wise) deviation in window placement
    # set to zero for other window managers
    deviation = 28
    # ---
    
    args = sys.argv[1:]
    
    get = lambda cmd: subprocess.check_output(cmd).decode("utf-8").strip()
    
    def find_dtop():
        # get the localized path to the Desktop folder
        home = os.environ["HOME"]
        dr_file = home+"/.config/user-dirs.dirs"
        return [home+"/"+ l.split("/")[-1].strip() \
                for l in open(dr_file).readlines() \
                if l.startswith("XDG_DESKTOP_DIR=")][0].replace('"', "")
    
    def check_windowtype(w_id):
        # check the type of window; only unmap "NORMAL" windows
        return "_NET_WM_WINDOW_TYPE_NORMAL" in get(["xprop", "-id", w_id])
    
    def get_process(w_id):
        # get the name of the process, owning the window and window x/y position
        w_list = get(["wmctrl", "-lpG"]).splitlines()
        pid = [l for l in w_list if w_id in l][0].split()
        proc = get(["ps", "-p", pid[2], "-o", "comm="])
        xy = (" ").join(pid[3:5])
        return (proc, xy)
    
    def read_f(f, string, proc):
        # search for a possible match in a targeted .desktop file
        try:
            with open(f) as read:
                for l in read:
                    if all([l.startswith(string), proc in l]):
                        in_f = True
                        break
                    else:
                        in_f = False
        except:
            in_f = False
        return in_f
    
    def get_icon(proc, w_name):
        # search appropriate icon in /usr/share/applications
        exceptions = [item for item in [
            ["soffice", "libreoffice-main"],
            ["gnome-terminal", "utilities-terminal"],
            ["nautilus", "folder"],
            ] if item[0] in proc]
        if exceptions:
            if exceptions == [["soffice", "libreoffice-main"]]:
                loffice = [
                    ["Calc", "libreoffice-calc"],
                    ["Writer", "libreoffice-writer"],
                    ["Base", "libreoffice-base"],
                    ["Draw", "libreoffice-draw"],
                    ["Impress", "libreoffice-impress"],
                    ]
                match = [m[1] for m in loffice if m[0] in w_name]
                if match:
                    return match[0]
                else:
                    return exceptions[0][1]
            else:      
                return exceptions[0][1]
        else:
            default = "/usr/share/applications"
            dtfiles = [default+"/"+f for f in os.listdir(default)]
            for f in dtfiles:
                if read_f(f, "Exec=", proc) == True:   
                    for l in open(f).readlines():
                        if l.startswith("Icon="):
                            icon = l.replace("Icon=", "").strip()
                            print(f)
                            break
                    break
            return icon
    
    def create_name():
        # create unique (file-) name for boxed window
        n = 1
        while True:
            name = dtop+"/"+"boxed_"+str(n)+".desktop"
            if os.path.exists(name):
                n += 1
            else:
                break
        return name
    
    def convert_wid(w_id):
        # convert window- id, xdotool format, into wmctrl format
        w_id = hex(int(w_id))
        return w_id[:2]+(10-len(w_id))*"0"+w_id[2:]
    
    def create_icon(w_id, w_name, icon, pos):
        # create the launcher, representing the boxed window
        boxedwindow = create_name()
        f_content =[
                "[Desktop Entry]",
                "Name=[WINDOW] "+w_name,
                "Exec=windowbox show "+w_id+" '"+boxedwindow+"' "+pos,
                "Icon="+icon,
                "Type=Application",
                ]
        if icon == "generic":
            f_content.pop(3)
        with open(boxedwindow, "wt") as boxed:
            for l in f_content:
                boxed.write(l+"\n")
        command = "chmod +x "+"'"+boxedwindow+"'"
        subprocess.call(["/bin/bash", "-c", command])
    
    if args[0] == "box":
        dtop = find_dtop()
        w_id = convert_wid(get(["xdotool", "getactivewindow"]))
        w_name = get(["xdotool", "getwindowname", w_id])
        if check_windowtype(w_id) == True:
            procdata = get_process(w_id)
            procname = procdata[0]
            icon = get_icon(procname, w_name); icon = icon if icon != None else "generic"
            create_icon(w_id, w_name, icon, procdata[1])
            subprocess.call(["xdotool", "windowunmap", w_id])
    
    elif args[0] == "show":
        w_id = args[1]
        subprocess.call(["xdotool", "windowmap", w_id])    
        subprocess.call(["xdotool", "windowmove", "--sync", w_id, args[3], str(int(args[4])-deviation)])
        os.remove(args[2])
    
  4. Make the script executable

  5. To make the newly created directory "pop up" in $PATH, either log out/in, or run source ~/.profile (from a terminal window)
  6. Test- run the script from a terminal window by the command:

    windowbox box
    

    The window should disappear, the "boxed" window should appear on your desktop.

  7. If all works fine, add the following command to a shortcut key: choose the gear icon on the top right of your screen:

    Gear icon

  8. Go to System SettingsKeyboardShortcutsCustom Shortcuts. Click the + and add the command:

    windowbox box
    

That should do it.

Important note

The script uses xdotool's windowunmap to make the window invisible. The created "box" (icon) on your desktop is the only "gate" to the hidden window. In other words: don't remove the desktop file(s) manually. The window will be lost for good if you do.

Work to do [edit 20-12: done]

The script still could use some refinement:

  • The window geometry is not restored by definition. Can be fixed very well, but I thought I'd show you the first result.
  • In most cases, the boxed window has its correct icon. The function get_process(w_id) could use some improvement however. If the process is not found as a command in /usr/share/applications, the file has a generic icon.

Giving the boxed-window icons a different size than the other icons

The script names the created .desktop files always boxed_1.desktop, boxed_2.desktop etc, depending on the "available" name at the moment of creation (filenames, not the displayed name).

You can resize the files (in general), by right- click > icon size. The good news is that if you remove the file and recreate it, the size is remembered. Even if you create the file again after a restart. That means that if you ever resized the boxed windows (e.g.) 1-5, they will always have the same size when you (the script) create them again!

enter image description here


You can use fvwm to accomplish this.

  1. Install fvwm:

    sudo apt-get update
    sudo apt-get install fvwm
    
  2. Find a them that uses the iconify function - there are several here: http://www.jmcunx.com/fvwm_theme.html Several look like the screen-shot you show.

  3. Copy the text of the theme, then navigate to ~/.fvwm/ (show hidden files first) then create a file .fvwm2rc

  4. Open that file in a text editor (like gedit) and paste the theme text into it.

  5. Reboot the computer, and select fvwm and login.

enter image description here