The project is named winlin; it's a portmanteau of the words window and Linux.
I'm working on a CPython extension to interact with the X server and manipulate windows. I don't have a ton of functionality yet but I think I'm to a good point where most of the basic facets are established and I can start to expand it. But before I start really dumping code into it I'd like to see what people think about the existing layout and implementation. To make this I've incorporated code from xdotool and wmctrl.
I have a lot of experience with python and I'm okay at Bash. However I do not have much experience with C, Python C extensions, packaging Python or Docker.
So here is the project structure.
(λ) godel@recurser:~/personal/projects/winlin$ tree
.
├── dist
│ ├── winlin-1.0-cp310-cp310-linux_x86_64.whl
│ ├── winlin-1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
│ ├── winlin-1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
│ ├── winlin-1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
│ └── winlin-1.0.tar.gz
├── docker
│ ├── build_wheels.sh
│ ├── containers.yml
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── README.md
│ └── setup_docker.sh
├── LICENSE
├── manylinx.sh
├── pyproject.toml
├── README.md
├── requirements.txt
├── setup.py
├── src
│ └── winlin
│ ├── experiment.c
│ └── winlin.c
└── tests
└── test.py
5 directories, 20 files
I'll start with the actual code and then work through to the docker stuff for building it.
So starting with the poorly named experiment.c
(I'll update that later).
I didn't write most of this code but I feel it's important to include it for completeness. I mostly pulled this code from wmctrl
: a Linux package for manipulating windows.
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#include <X11/Xmu/WinUtil.h>
#include <glib.h>
#define _NET_WM_STATE_REMOVE 0 /* remove/unset property */
#define _NET_WM_STATE_ADD 1 /* add/set property */
#define _NET_WM_STATE_TOGGLE 2 /* toggle property */
#define MAX_PROPERTY_VALUE_LEN 4096
#define SELECT_WINDOW_MAGIC ":SELECT:"
#define ACTIVE_WINDOW_MAGIC ":ACTIVE:"
#define p_verbose(...) if (1 != 1 ) { \
fprintf(stderr, __VA_ARGS__); \
}
/*
* gcc -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include experiment.c -o ex -lglib-2.0 -lX11
*/
/*
* blah blah
*/
struct WindowData{
Window wid; //octal unsigned long?
signed int x;
signed int y;
signed int h;
signed int w;
char class[128];
char title[128];
unsigned long desktop;
unsigned long pid;
char machine[64];
};
static Window *get_client_list (Display *disp, unsigned long *size);
static int list_windows (Display *disp, unsigned long client_list_size, Window * client_list, struct WindowData *);
static gchar *get_output_str (gchar *str, gboolean is_utf8);
static gchar *get_window_title (Display *disp, Window win);
static gchar *get_window_class (Display *disp, Window win);
static gchar *get_property (Display *disp, Window win,
Atom xa_prop_type, gchar *prop_name, unsigned long *size);
static void init_charset(void);
static gboolean envir_utf8;
static void init_charset (void) {
const gchar *charset; /* unused */
gchar *lang = getenv("LANG") ? g_ascii_strup(getenv("LANG"), -1) : NULL;
gchar *lc_ctype = getenv("LC_CTYPE") ? g_ascii_strup(getenv("LC_CTYPE"), -1) : NULL;
/* this glib function doesn't work on my system ... */
envir_utf8 = g_get_charset(&charset);
/* ... therefore we will examine the environment variables */
if (lc_ctype && (strstr(lc_ctype, "UTF8") || strstr(lc_ctype, "UTF-8"))) {
envir_utf8 = TRUE;
}
else if (lang && (strstr(lang, "UTF8") || strstr(lang, "UTF-8"))) {
envir_utf8 = TRUE;
}
g_free(lang);
g_free(lc_ctype);
//envir_utf8 = TRUE;
p_verbose("envir_utf8: %d\n", envir_utf8);
}
static gchar *get_window_title (Display *disp, Window win) {
gchar *title_utf8;
gchar *wm_name;
gchar *net_wm_name;
wm_name = get_property(disp, win, XA_STRING, "WM_NAME", NULL);
net_wm_name = get_property(disp, win,
XInternAtom(disp, "UTF8_STRING", False), "_NET_WM_NAME", NULL);
if (net_wm_name) {
title_utf8 = g_strdup(net_wm_name);
}
else {
if (wm_name) {
title_utf8 = g_locale_to_utf8(wm_name, -1, NULL, NULL, NULL);
}
else {
title_utf8 = NULL;
}
}
g_free(wm_name);
g_free(net_wm_name);
return title_utf8;
}
static gchar *get_property (Display *disp, Window win, /*{{{*/
Atom xa_prop_type, gchar *prop_name, unsigned long *size) {
Atom xa_prop_name;
Atom xa_ret_type;
int ret_format;
unsigned long ret_nitems;
unsigned long ret_bytes_after;
unsigned long tmp_size;
unsigned char *ret_prop;
gchar *ret;
xa_prop_name = XInternAtom(disp, prop_name, False);
/* MAX_PROPERTY_VALUE_LEN / 4 explanation (XGetWindowProperty manpage):
*
* long_length = Specifies the length in 32-bit multiples of the
* data to be retrieved.
*
* NOTE: see
* http://mail.gnome.org/archives/wm-spec-list/2003-March/msg00067.html
* In particular:
*
* When the X window system was ported to 64-bit architectures, a
* rather peculiar design decision was made. 32-bit quantities such
* as Window IDs, atoms, etc, were kept as longs in the client side
* APIs, even when long was changed to 64 bits.
*
*/
if (XGetWindowProperty(disp, win, xa_prop_name, 0, MAX_PROPERTY_VALUE_LEN / 4, False,
xa_prop_type, &xa_ret_type, &ret_format,
&ret_nitems, &ret_bytes_after, &ret_prop) != Success) {
p_verbose("Cannot get %s property.\n", prop_name);
return NULL;
}
if (xa_ret_type != xa_prop_type) {
p_verbose("Invalid type of %s property.\n", prop_name);
XFree(ret_prop);
return NULL;
}
/* null terminate the result to make string handling easier */
tmp_size = (ret_format / 8) * ret_nitems;
/* Correct 64 Architecture implementation of 32 bit data */
if(ret_format==32) tmp_size *= sizeof(long)/4;
ret = g_malloc(tmp_size + 1);
memcpy(ret, ret_prop, tmp_size);
ret[tmp_size] = '0円';
if (size) {
*size = tmp_size;
}
XFree(ret_prop);
return ret;
};
static Window *get_client_list (Display *disp, unsigned long *size) {
Window *client_list;
if ((client_list = (Window *)get_property(disp, DefaultRootWindow(disp),
XA_WINDOW, "_NET_CLIENT_LIST", size)) == NULL) {
if ((client_list = (Window *)get_property(disp, DefaultRootWindow(disp),
XA_CARDINAL, "_WIN_CLIENT_LIST", size)) == NULL) {
fputs("Cannot get client list properties. \n"
"(_NET_CLIENT_LIST or _WIN_CLIENT_LIST)"
"\n", stderr);
return NULL;
}
}
return client_list;
}
static gchar *get_window_class (Display *disp, Window win) {
gchar *class_utf8;
gchar *wm_class;
unsigned long size;
wm_class = get_property(disp, win, XA_STRING, "WM_CLASS", &size);
if (wm_class) {
gchar *p_0 = strchr(wm_class, '0円');
if (wm_class + size - 1 > p_0) {
*(p_0) = '.';
}
class_utf8 = g_locale_to_utf8(wm_class, -1, NULL, NULL, NULL);
}
else {
class_utf8 = NULL;
}
g_free(wm_class);
return class_utf8;
}
static gchar *get_output_str (gchar *str, gboolean is_utf8) {/*{{{*/
gchar *out;
if (str == NULL) {
return NULL;
}
if (envir_utf8) {
if (is_utf8) {
out = g_strdup(str);
}
else {
if (! (out = g_locale_to_utf8(str, -1, NULL, NULL, NULL))) {
p_verbose("Cannot convert string from locale charset to UTF-8.\n");
out = g_strdup(str);
}
}
}
else {
if (is_utf8) {
if (! (out = g_locale_from_utf8(str, -1, NULL, NULL, NULL))) {
p_verbose("Cannot convert string from UTF-8 to locale charset.\n");
out = g_strdup(str);
}
}
else {
out = g_strdup(str);
}
}
return out;
}
static int list_windows (Display *disp, unsigned long client_list_size, Window * client_list, struct WindowData *windows) {
size_t i;
int max_client_machine_len = 0;
//int o = (int) client_list_size / sizeof(Window);
for (i = 0; i < client_list_size / sizeof(Window); i++) {
gchar *client_machine;
if ((client_machine = get_property(disp, client_list[i],
XA_STRING, "WM_CLIENT_MACHINE", NULL))) {
max_client_machine_len = strlen(client_machine);
}
g_free(client_machine);
}
for (i = 0; i < client_list_size / sizeof(Window); i++) {
gchar *title_utf8 = get_window_title(disp, client_list[i]); /* UTF8 */
gchar *title_out = get_output_str(title_utf8, TRUE);
gchar *client_machine;
gchar *class_out = get_window_class(disp, client_list[i]); /* UTF8 */
unsigned long *pid;
unsigned long *desktop;
int x, y, junkx, junky;
unsigned int wwidth, wheight, bw, depth;
Window junkroot;
/* desktop ID */
if ((desktop = (unsigned long *)get_property(disp, client_list[i],
XA_CARDINAL, "_NET_WM_DESKTOP", NULL)) == NULL) {
desktop = (unsigned long *)get_property(disp, client_list[i],
XA_CARDINAL, "_WIN_WORKSPACE", NULL);
}
/* client machine */
client_machine = get_property(disp, client_list[i],
XA_STRING, "WM_CLIENT_MACHINE", NULL);
/* pid */
pid = (unsigned long *)get_property(disp, client_list[i],
XA_CARDINAL, "_NET_WM_PID", NULL);
/* geometry */
XGetGeometry (disp, client_list[i], &junkroot, &junkx, &junky,
&wwidth, &wheight, &bw, &depth);
XTranslateCoordinates (disp, client_list[i], junkroot, junkx, junky,
&x, &y, &junkroot);
windows[i].wid = client_list[i];
windows[i].x = x;
windows[i].y = y;
windows[i].h = wheight;
windows[i].w = wwidth;
strcpy(windows[i].class, class_out);
strcpy(windows[i].title, title_out);
windows[i].desktop = *desktop;
windows[i].pid = *pid;
strcpy(windows[i].machine, client_machine);
g_free(title_utf8);
g_free(title_out);
g_free(desktop);
g_free(client_machine);
g_free(class_out);
g_free(pid);
}
//g_free(client_list);
return EXIT_SUCCESS;
}
How I got it to where it is by getting the source, figuring out how to compile it, slowly removing things that I didn't need and recompiling until it had only what I needed. Then I started adding/changing things to suit my needs. The most notable changes that I added are to the list_windows()
function and I added the WindowData
struct.
This file is then used by winlin.c
which sets up the functions to be used in a Python package.
#include <Python.h>
#include <xdo.h>
#include "experiment.c"
/*------------------------------------------------------------------------------*/
static PyObject* windows(PyObject* self, PyObject *args){
Display *disp;
Window *client_list;
unsigned long client_list_size;
setlocale(LC_ALL, "");
init_charset();
if (! (disp = XOpenDisplay(NULL))) {
fputs("Cannot open display.\n", stderr);
}
client_list = get_client_list(disp, &client_list_size);
struct WindowData windows[client_list_size];
list_windows(disp, client_list_size, client_list, windows);
// do python stuff
PyObject *dict = NULL;
PyObject * list;
list = (PyListObject *) Py_BuildValue("[]");
//Py_ssize_t size = 0;
//list = PyList_New(size);
size_t i;
for (i = 0; i < client_list_size / sizeof(Window); i++) {
dict = Py_BuildValue("{s:i, s:i, s:i, s:i, s:i, s:s, s:s, s:s, s:i}",
"id", windows[i].wid,
"x", windows[i].x,
"y", windows[i].y,
"h", windows[i].h,
"w", windows[i].w,
"class", windows[i].class,
"title", windows[i].title,
"machine", windows[i].machine,
"desktop", windows[i].desktop,
"pid", windows[i].pid);
PyList_Append(list, dict);
}
XCloseDisplay(disp);
return (PyObject *) list;
}
static char windows_docs[] = "return a list of dicts containing information about each window";
/*------------------------------------------------------------------------------*/
static PyObject* resize(PyObject* self, PyObject *args){
int wid;
int w;
int h;
if (!PyArg_ParseTuple(args, "iii", &wid, &w, &h))
return NULL;
xdo_t *xdo_inst = xdo_new(NULL);
xdo_set_window_size(xdo_inst, wid, w, h, 0);
xdo_free(xdo_inst);
Py_RETURN_NONE;
//return Py_BuildValue("s", "it worked, maybe?");
}
static char resize_docs[] = "change the size of a given window given its id and a new height and width";
/*----------------------------------------------------------------------------*/
static PyObject* move(PyObject* self, PyObject *args){
int wid;
int x;
int y;
if (!PyArg_ParseTuple(args, "iii", &wid, &x, &y))
return NULL;
xdo_t *xdo_inst = xdo_new(NULL);
xdo_move_window(xdo_inst, wid, x, y);
xdo_free(xdo_inst);
Py_RETURN_NONE;
}
static char move_docs[] = "change the position of a window given its ID and a new x and y";
/*----------------------------------------------------------------------------------*/
static PyMethodDef winlin_funcs[] = {
{"resize", (PyCFunction)resize, METH_VARARGS, resize_docs},
{"move", (PyCFunction)move, METH_VARARGS, move_docs},
{"windows", (PyCFunction)windows, METH_VARARGS, windows_docs},
{NULL}
};
static char winlin_module_docs[] = "Module used to manipulate windows in linux";
static struct PyModuleDef winlin_module = {
PyModuleDef_HEAD_INIT,
"winlin",
winlin_module_docs,
-1,
winlin_funcs
};
PyMODINIT_FUNC
PyInit_winlin(void){
PyObject* m = PyModule_Create(&winlin_module);
return m;
}
There are only 3 functions here:
windows()
: gathers a list of windowsmove()
: moves a window given its id and a new x,yresize()
: changes the size of a window given its id and a new w, h
In this file I include the xdo
and wmctrl
for the xdo functionality. I was able to use it without manipulating the source so it's just a dependency. xdo and wmctl both have their own ways of managing their connection with the X server. I think eventually I'll have to pull in / cannibalize more projects to get all of the features I want. So I'd like to standardize how the connection to the X server is opened and closed with all of my functions. Effectively I want to coerce both of these projects to be more internally consistent but I think that will be a slow process while I climb the learning curve.
While writing this I just thought of an idea: perhaps I could expose functions for connecting to and disconnecting from the X server and then users could use a context manager or something to manage the connections for their calls.
That idea aside, or perhaps in opposition to, is that I'd like to keep the C interface as simple as possible. I'm planning on providing a pure Python module that has more syntactic sugar for searching and updating window properties.
So I think that covers the implementation and motivation for the source of this project. I'll move into building and installing now.
In order to create portable Linux wheels it's recommended to use a manylinux container to create your wheel files, something that I didn't know existed a week ago. So I set up a basic Docker directory with some scripts/configs to help me with this task.
manylinux provides images with various platforms to build your code in if you want to make it as portable as possible. I tried to use Docker Compose for this task and haven't been able to figure it out yet, so for now I'm only targeting x86.
Here's the Dockerfile
; not much to look at.
# syntax=docker/dockerfile:1
FROM quay.io/pypa/manylinux2014_x86_64
There are two important scripts. Firstly, setup_docker.sh
which is to be run locally:
#!/usr/bin/env bash
project_dir=1ドル
pyvers="${@:2}"
docker build -t manylinux_x86 .
docker run -ti -d -p 3000:3000 --name=x86 manylinux_x86
sleep 2
docker container cp -q $project_dir x86:/project
sleep 2
container_cmd="/project/docker/build_wheels.sh ${pyvers}"
docker exec -it x86 /bin/bash $container_cmd
sleep 5
docker container cp -q x86:/project/wheelhouse/. "${project_dir}/dist/"
ls "${project_dir}/dist/"
docker container rm -f x86
This takes some arguments: specifically where your project is located and the Python versions that you want to build wheels for. I call it like this
./setup_docker.sh /home/godel/personal/projects/winlin cp38 cp39 cp310 cp311
This then starts the container, copies everything there and then propagates the arguments build_wheels.sh
script inside the container:
#!/usr/bin/env bash
yum install -y libxdo-devel
yum install -y libXmu-devel
yum install -y glib2-devel
for py in $@
do
pyex="/opt/python/${py}-${py}/bin/python"
output_dir="${py}_wheel"
mkdir "/project/${output_dir}"
cmd1="${pyex} -m pip install --upgrade pip"
cmd2="${pyex} -m pip wheel -w /project/${output_dir}/ /project/"
cmd3="auditwheel repair -w /project/wheelhouse/ /project/${output_dir}/*.whl"
eval "$cmd1"
eval "$cmd2"
eval "$cmd3"
done
This builds the actual wheels; I'm assuming there is something better to be done here than using eval
, but I couldn't think of the solution. After the wheels are built, setup_docker.sh
then copies the wheel files back to the projects dist dir.
A part of the reason that I stopped trying to get docker compose to work for this is that I'm not sure it's necessary to build wheels for those platforms.
I think to improve this I could add more stuff to the Dockerfile. The reason I did it this way is because I'm trying to write something sort of general that can be used by other projects in the future possibly. So I was trying to avoid hard coding my paths and data into the scripts.
Now then all of this uses the setup.py
file:
from setuptools import setup, Extension
winlin = Extension(
'winlin',
define_macros = [('MAJOR_VERSION', '1'),('MINOR_VERSION', '0')],
include_dirs = ['/usr/local/include/', '/usr/include/glib-2.0', '/usr/lib/x86_64-linux-gnu/glib-2.0/include', '/usr/lib64/glib-2.0/include'],
libraries = ['xdo', 'glib-2.0', 'X11'],
library_dirs = ['/usr/local/lib/'],
sources = ['src/winlin/winlin.c', 'src/winlin/experiment.c']
)
setup(
name = 'winlin',
version = '1.0',
description = 'A tool kit for manipulating windows in linux',
ext_modules = [winlin]
)
This one is sort of intentionally skimpy. I have been reading about it and I'm still pretty confused about setuptools and all the other building tools, as well as the artefacts they use - i.e. setup.py
, setup.cfg
and pyproject.toml
. I'm not sure what approach is going to be best for my intended purposes. So I figured I'd leave it minimal until I can figure out what approach best suits my use case.
So that should be most of the important aspects of this. I'm hoping to get some feedback on more of the broad strokes here. But all thoughts and suggestions are welcome. :) Particularly on anything to do with the docker stuff, setup.py, and the C code.
References:
-
\$\begingroup\$ no Wayland? \$\endgroup\$J_H– J_H2023年02月20日 23:56:43 +00:00Commented Feb 20, 2023 at 23:56
-
\$\begingroup\$ I'd like to add support for wayland as well! but that'll take time \$\endgroup\$gnarlyninja– gnarlyninja2023年02月21日 01:21:06 +00:00Commented Feb 21, 2023 at 1:21
-
\$\begingroup\$ @J_H i actually installed wayland for plasma and I've no idea how but it seems to be working. \$\endgroup\$gnarlyninja– gnarlyninja2023年02月21日 04:31:20 +00:00Commented Feb 21, 2023 at 4:31
-
1\$\begingroup\$ Woo hoo! So, that's supposed to be design feature, right? Some measure of compatibility, if we stick to the core functions. Chalk up a win. \$\endgroup\$J_H– J_H2023年02月21日 04:42:46 +00:00Commented Feb 21, 2023 at 4:42