/* vdesk: virtual desktops for any window manager      vim:et:ts=4:
 * Copyright 2001, 2003, 2025 Adam Sampson <ats@offog.org>
 * Originally based on code from aewm:
 * Copyright (c) 1998-2001 Decklin Foster.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS", WITHOUT ANY EXPRESS
 * OR IMPLIED WARRANTIES OF ANY KIND. IN NO EVENT SHALL THE AUTHOR BE
 * HELD LIABLE FOR ANY DAMAGES CONNECTED WITH THE USE OF THIS PROGRAM.
 * 
 * You are granted permission to copy, publish, distribute, and/or sell
 * copies of this program and any modified versions or derived works,
 * provided that this copyright and notice are not removed or altered.
 */

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

Display *dpy;
Window root;
Atom wm_state, vdesk_desktop, wm_change_state;
/* Start off the current desk at 1 so that we can set the root
   window correctly the first time we start up. */
long todesk, curdesk = 1;

void set_window_desk_vis(Window w, long n, long vis) {
    long buf[2];
    buf[0] = n;
    buf[1] = vis;
    XChangeProperty(dpy, w, vdesk_desktop, vdesk_desktop, 32, PropModeReplace, 
        (unsigned char *)buf, 2);
}

long get_window_desk_vis(Window w, long *ovis) {
    Atom real_type;
    int real_format;
    unsigned long items_read, items_left;
    unsigned char *data;
    long n = -1, vis = -1;

    if (XGetWindowProperty(dpy, w, vdesk_desktop, 0L, 2L, False,
            vdesk_desktop, &real_type, &real_format, &items_read, &items_left,
            &data) == Success && items_read) {
        n = ((long *)data)[0];
        vis = ((long *)data)[1];
        XFree(data);
    }

    if (n == -1) {
        /* If it's not set, make it the current desk and visible. */
        set_window_desk_vis(w, curdesk, 1);
        *ovis = 1;
        return curdesk;
    } else {
        *ovis = vis;
        return n;
    }
}

long get_wm_state(Window w)
{
    Atom real_type;
    int real_format;
    unsigned long items_read, items_left;
    unsigned char *data;
    long state = WithdrawnState;

    if (XGetWindowProperty(dpy, w, wm_state, 0L, 2L, False,
            wm_state, &real_type, &real_format, &items_read, &items_left,
            &data) == Success && items_read) {
        state = ((long *)data)[0];
        XFree(data);
    }
    return state;
}

void iconify_window(Window w) {
    XEvent ev;

    memset(&ev, 0, sizeof ev);
    ev.xclient.type = ClientMessage;
    ev.xclient.window = w;
    ev.xclient.message_type = wm_change_state;
    ev.xclient.format = 32;
    ev.xclient.data.l[0] = IconicState;
    ev.xclient.data.l[1] = CurrentTime;
    XSendEvent(dpy, root, False, SubstructureRedirectMask, &ev);
}

void uniconify_window(Window w) {
    XMapWindow(dpy, w);
}

void on_clients(Window w) {
    Window dummy_w, *wins;
    unsigned int i, nwins;
    long state = get_wm_state(w);

    if (state != WithdrawnState) {
        long vis, n;
        n = get_window_desk_vis(w, &vis);

        if (state == NormalState) {
            /* If the window isn't sticky, changing desktops will
               always hide it. */
            if (n) {
                /* Since it was visible, it's been moved behind our back
                   and should really be on this desktop. */
                set_window_desk_vis(w, curdesk, 1);

                /* And hide it. */
                iconify_window(w);
            }
        } else {
            if (n == curdesk) {
                /* Invisible, but on the current desktop -- means
                   it's been iconified. */
                set_window_desk_vis(w, curdesk, 0); 
            } else if (n == todesk && vis == 1) {
                uniconify_window(w);
            }
        }
    } else if (XQueryTree(dpy, w, &dummy_w, &dummy_w, &wins, &nwins)) {
        for (i = 0; i < nwins; i++)
            on_clients(wins[i]);
        XFree(wins);
    }
}

void usage() {
    printf(PACKAGE_STRING " by Adam Sampson\n"
           "Usage: vdesk [OPTION]... [DESKTOP [WINDOW]...]\n\n"
           "To print the current desktop number:\n"
           "  vdesk\n"
           "To switch to desktop DESKTOP:\n"
           "  vdesk DESKTOP\n"
           "To move windows to a desktop (desk 0 is sticky):\n"
           "  vdesk DESKTOP WINDOWID [WINDOWID]...\n"
           "DESKTOP can be any positive integer. WINDOWIDs are in hex.\n\n"
           "Report bugs to <" PACKAGE_BUGREPORT ">.\n");
    exit(20);
}

int main(int argc, char **argv) {
    long t;
    int change = 0;

    opterr = 0;
    while (1) {
        int c = getopt(argc, argv, "+?");
        if (c == -1)
            break;

        /* No options supported yet; just show the usage message. */
        usage();
    }

    dpy = XOpenDisplay(NULL);
    if (!dpy) {
        fprintf(stderr, "%s: cannot open display\n", argv[0]);
        return 20;
    }

    wm_state = XInternAtom(dpy, "WM_STATE", False);
    vdesk_desktop = XInternAtom(dpy, "VDESK_DESKTOP", False);
    wm_change_state = XInternAtom(dpy, "WM_CHANGE_STATE", False);
    root = DefaultRootWindow(dpy);
    curdesk = get_window_desk_vis(root, &t);

    if (optind == argc) {
        /* Print the current desktop number. */
        printf("%ld\n", curdesk);
        goto out;
    }

    if ((argc - optind) == 1)
        change = 1;

    todesk = strtol(argv[optind], NULL, 0);
    if (todesk < 0 || (change && todesk == 0)) {
        fprintf(stderr, "%s: bad desktop number '%s'\n", argv[0], argv[optind]);
        usage();
    }
    ++optind;

    if (change) {
        /* Switch to the given desktop. */
        if (todesk != curdesk) {
            on_clients(root);
            set_window_desk_vis(root, todesk, 1);
        }
    } else {
        /* Move the given windows to the given desktop. */
        Window w;
        int i;

        for (i = optind; i < argc; i++) {
            w = strtol(argv[i], NULL, 0);
            if (w) {
                int is_visible = get_wm_state(w) == NormalState;

                set_window_desk_vis(w, todesk, is_visible);
                if (!is_visible && (todesk == curdesk || !todesk)) {
                    uniconify_window(w);
                }
                if (is_visible && todesk && todesk != curdesk) {
                    iconify_window(w);
                }
            } else {
                fprintf(stderr, "%s: bad window id '%s'\n", argv[0], argv[i]);
                usage();
            }
        }
    }

out:
    XCloseDisplay(dpy);
    return 0;
}
