@Grim : tes retours m'intéressent beaucoup, quand tu auras le temps 🙂
Dafyd a écritJe vois que tu as vachement bossé sur l'architecture de Touhy. Très impressionnant je trouve. Tout comme l'utilisation d'une lib graphique ou d'une autre selon leur présence, le fallback sur différents systèmes audios, etc.. On voit la très grande force de Python pour cet usage... En C, ce serait galère, à grand coup de dlopen...
Oui, je doute fortement que j'aurais réussi à faire quelque chose d'aussi souple avec un langage autre que Python ; ou en tout cas ça aurait été pas mal plus compliqué à gérer.
Bon, après, il n'y a pas que Python dans l'affaire : comme mentionné, GTK me fait quelques soucis pour l'instant, je pense qu'il n'apprécie pas trop ma façon de faire, mais je vais bien finir par réussir à le domestiquer 🙂
À vue de nez, j'ai une source de segfault dans le module image (ça n'a pas l'air systématique, mais comme sencol fait pas mal de traitement d'image (pour les icônes liées au presse-papier ou aux capteurs de température, par exemple), le plantage finit par être quasi-systématique), et je n'arrive pas encore à identifier où précisément pour l'instant. Si quelqu'un veut aider à ça, n'hésitez surtout pas…
Tiens, du coup, avant de répondre aux questions de Dafyd (en fait, après, mais je reviens écrire là pour que la lecture du message soit plus cohérente que son écriture), un peu plus de détails sur ce fameux module images, donc le fonctionnement est (je trouve) théoriquement très élégant mais assez atroce à débuguer en pratique.
Le principe, c'est donc de transformer une chaîne de caractères comme celles présentées plus haut en une image. Par exemple, prenons cette chaîne-là :
#overlay(#small(t,r):#icon(add)):#flip(h):#icon(folder)
Dans cette chaîne,
- « overlay » veut dire qu'on va prendre l'image qui est entre les grandes parenthèses, et la dessiner par dessus celle qui est après le « : » suivant.
- « small(t,r) » veut dire que l'image qui suit sera affichée en plus petit, en haut (« t ») à droite (« r ») du cadre.
- « icon(add) » veut dire qu'on va charger l'icône « add » (donc, conventionnellement, un + vert) du thème d'icônes actif.
- « flip(h) » veut dire qu'on va inverser l'image horizontalement.
Et je vous passe le deuxième chargement d'icône, vous avez compris. L'image résultante sera donc l'icône de répertoire du thème actif, inversée horizontalement, sur laquelle aura été superposée l'icône « add » en haut à droite. Ça va, ça reste lisible 🙂
On a donc trois sortes d'opérations ici : celles qui génèrent une image à partir uniquement de leurs paramètres (« icon » ici, mais il y a aussi « file » pour lire un fichier (en pratique, c'est plein de fonctions différentes en fonction du type mime, mais simplifions :-°), « text » pour écrire du texte, « empty » pour faire une image vide, etc.), celles qui transforment une image donnée en autre chose (« flip » et « small » ici, j'ai aussi « negative », « rotate », « tint », etc.), et celles qui combinent plusieurs images (« overlay » ici, et pour le coup je n'en ai qu'une seule autre : « shape », qui trace la forme d'une image avec les couleurs de l'autre).
Chaque « chemin » d'image doit pouvoir être tracé pour une taille donnée (et quelques autres éléments de contexte, comme le thème d'icône actif, par exemple), et dans un format donné (par exemple, en GTK3, j'utilise le format GdkPixbuf pour afficher des icônes, mais par contre le format CairoContext pour tracer le fond des widgets graphiques). Une des difficultés étant bien sûr que les opérations faciles à faire ne sont pas les mêmes d'un format à l'autre (le « flip », une méthode de GdkPixbuf fait ça directement, par contre, je ne sais pas faire avec cairo, sauf à aller bidouiller les pixels à la main, ce que je n'ai pas eu envie de faire.)
Donc, pour faire ça, de base, j'ai deux modules : « plans.py » et « gears.py ». Comme leurs nom l'indique, le second contient les bouts de mécanismes à assembler, et le premier gère la façon dont les assembler 🙂 En pratique, toutes les instructions qu'on peut trouver dans les chemins (overlay, flip, icon, etc. À quelques exceptions près comme small, qui sont en fait des combinaisons définies dans le fichier de config. En l'occurrence, small combine une instruction pour réduire la taille et une autre pour replacer la miniature à la bonne place) correspondent à une fonction (ou, vu qu'on est en Python, un
callable, donc un objet qu'on peut appeler comme une fonction) présente dans le module « gears.py ».
Le module « plans.py » se charge donc de parser le chemin, étape par étape, de chercher à chaque fois le
callable correspondant, puis l'appelle en lui passant son paramètre (ce qu'il y a entre les parenthèses, ou None s'il n'y avait pas de parenthèses), la suite du chemin, et le contexte. Cet appel ne fait aucun traitement d'images, pour l'instant, mais modifie le contexte si besoin, et renvoie un tuple de deux éléments :
- Le premier est un tuple contenant les paramètres à appeler lors de l'appel réel à la fonction, ou None. Si c'est None, cette étape de la procédure ne nécessite aucun traitement d'image à proprement parler, ce n'était qu'une modif' de contexte (par exemple, « #theme(machin) » permet de sélectionner le thème d'icônes « machin » pour les chargements d'icônes dans la suite, mais ne fait rien en lui-même). Sinon, il faudra appeler la fonction de traitement d'image proprement dite, mais une fois qu'on aura la ou les images sur lesquelles le traitement doit s'effectuer. D'où le second élément du résultat,
- un tuple contenant les chemins à appeler pour la suite. Si ce tuple est vide, on est sur une étape de création : on peut donc appeler la fonction de traitement d'image tout de suite, avec les paramètres sus-récupérés. S'il contient un seul élément, on va donc relancer la machine pour obtenir une image à partir de ce chemin, puis appeler la fonction de traitement sur cette image. S'il y en a deux, on va d'abord obtenir chacune de ces deux images séparément, puis ensuite on appelle la fonction avec les deux images.
Reste que, pour ça, il faut la fonction de traitement proprement dite, qui dépend du format de l'image à manipuler. En fait, chaque
callable qui nécessite un traitement d'image contient un attribut
_functs, qui est (en gros) un dictionnaire avec comme clef les formats manipulés en entrée et en sortie, et comme valeur la fonction spécifique à ces formats. L'appel global se fait avec le format de sortie désiré, puis, pour chaque étape, le programme regarde ce que je veux comme formats de sortie, ce que je sais gérer comme formats d'entrée, et demande à l'étape d'après de lui sortir une image qui sera du format le moins coûteux pour procéder à l'opération, en convertissant si besoin d'un format à l'autre au passage.
Tous les autres modules présents dans elzlibs/images/ et dans les différents _elzlibs_X/images (il n'y en a qu'en GTK 3, pour l'instant, mais je suppose que d'autres biblis graphiques ont des formats d'images spécifiques) servent ensuite à aller remplir ces différents
_functs en fournissant des fonctions de traitement pour certains formats particuliers.
Tant que j'y suis, un peu plus de détails que plus haut. Actuellement, je gère :
- Le format cairo.(Image)Surface, chez moi baptisé surface,
- Les cairo.Context correspondants, format chez moi baptisé context, parce que pas mal d'opérations (de cairo ou de GTK) prennent ou renvoie un contexte, donc c'était plus simple de faire un format de plus que de convertir à chaque fois,
- Le format PIL.Image, chez moi baptisé pillow,
- Le format GdkPixbuf, chez moi baptisé pixbuf,
- Un format baptisé matrix, qui est juste une liste de listes de listes d'entiers pour des opérations simples,
- Un format baptisé stream, qui est juste le flux de bytes de l'image encodée en PNG dans un io.BytesIO.
Chaque format possède son module spécifique (sauf context et surface qui sont évidemment ensemble), qui contient toutes les fonctions de traitement que je sais faire sur le format en question. Les fonctions de conversion d'un format à l'autre ou de chargement d'un fichier depuis le disque sont placées dans le module du format de sortie.
Sachant qu'en plus de ça, j'ai un module spécial pour tracer des images d'un composant GTK en particulier (utilisé par exemple pour donner un look d'onglets aux boutons permettant de passer d'un bureau à l'autre sur les captures sus-mentionnées ; il faudra sans doute faire un équivalent pour changer de bibli graphique), et que le module
streams.py gère aussi (enfin, gérera, parce que c'est tout cassé pour l'instant, mais je répare ça dès que j'ai débugué les segfaults) le fait qu'une application donnée puisse générer des images spécifiques pour une autre, à base de sockets.
L'__init__.py du module elzlibs/images fournit la liste des décorateurs (toute personne jetant un œil à mon code aura bien sûr remarqué que j'ai une certaine tendance à abuser des décorateurs) pour référencer tout ça, ainsi que deux fonctions de haut niveau : l'une pour charger un chemin donné à une taille (hauteur et largeur pouvant différer) donnée (qui peut déclencher des exceptions par exemple si on demande un traitement inconnu), l'autre pour charger une icône à une taille fixe (largeur et hauteur identiques) à partir d'une série de chemins (qui a priori ne déclenche aucune exception : si le premier chemin plante, ça tente le suivant, etc., jusqu'à balancer l'icône « image-broken » si ça n'est arrivé à rien).
Donc voilà, je trouve ça assez beau sur le papier, en pratique ça ne marche pas trop mal en dehors de ce souci de segfault, mais par contre, dans tout ça, pour identifier quel composant précisément est responsable d'une erreur qui n'a pas l'air systématique et qui fait planter le programme sans générer de stacktrace, ben… c'est coton, pour rester poli.
Dafyd a écrit-> Pourquoi ne pas utiliser les git submodules pour les elzlibs ? Chaque projet aurait son code, et un sous module pointant vers le dépôt d'elzlib. Ca pourrait te simplifier la vie en terme de déploiement par exemple...
En fait, j'utilise un truc assez proche de ça, mais (à mon humble avis, mais je connais sans doute trop mal submodules) plus souple.
Chaque appli a son dépôt, dans lequel j'intègre son code spécifique et les bouts de la bibliothèque commune dont j'ai besoin (et seulement eux : sencol n'a par exemple pas besoin de se trimballer l'entité média, il ne jouera pas de son. Pour show, dont on parlait plus haut, je n'ai pas besoin du daemon.py, vu que de toute façon ce sont des appels oneshots. Etc.), et j'ai un dépôt principal qui contient la liste des autres dépôts et quelques scripts de gestions du bazar.
Comme ça, le git de chaque appli est complètement autonome et ne contient que ce dont il a besoin (parce que, mine de rien, la elzlibs complète contient des tas de trucs pas forcément utiles), mais je peux facilement tout déployer au même endroit pour avoir les applis qui marchent dans un seul endroit. (Et les fichiers communs d'un dépôt à l'autre sont des liens physiques vers le même fichier, comme ça, les corrections apportées le sont partout en même temps. En temps normal (c'est-à-dire pas maintenant vu que je passe mon temps à casser des trucs pour finir la version repensée de la bibliothèque), les commit/push sur la elzlibs sont communs à tous les dépôts concernés).
Dafyd a écrit-> Pourquoi ne pas utiliser de venv python ? Ce serait pratique pour mettre en place un environnement de dev ou de prod contenant les libs dont tu as besoin, etc... Avec pip et un fichier requirements.txt.
Deux raisons à ça : d'abord, par confort personnel, je préfère n'avoir qu'un seul gestionnaire de paquets ou équivalent avec lequel me battre. Donc, chaque fois que c'est possible, je prends les modules pythons pour lesquels il existe un paquet deb dans les dépôts. Je ne suis même pas sûr d'avoir déjà utilisé pip, en fait…
Ensuite, le gros souci d'un projet comme Touhy, c'est que la liste des dépendances ne peut (sauf erreur de ma part, hein) pas se réduire à un requirements.txt : sencol a besoin par exemple de la commande externe acpi pour lire le niveau de la batterie, et je doute que pip puisse aller installer autre chose que des modules python. Pour l'interface graphique, pour l'instant, j'utilise encore quasi-essentiellement GTK, or depuis GTK 3, il n'y a plus des modules pythons directement comme c'était le cas avec PyGTK, mais une unique interface PyGI, et les dépendances sont ensuite à aller chercher côté GI, hors-python.
Donc bon, partant du principe que 1/ tout ce que j'utilise est dans les dépôts de Debian et d'Ubuntu, que 2/ l'installation automatique par pip serait a priori incomplète, et vu qu'en plus j'essaye justement de coder quelque chose qui s'adapte à ce qui va être présent ou pas sur la machine, je préfère lister les paquets deb dont j'ai eu besoin et laisser les gens installer ce qu'ils veulent à la main.
Sans doute que je me gourre, hein ; si quelqu'un veut contribuer en rajoutant ledit requirements.txt, ça me va 🙂
Dafyd a écritSurtout pour la longévité du projet
Bah, je fais ça par phases, en fait. De temps en temps, je suis d'humeur à coder, alors je me fais des sessions où j'avance (ou où je casse) plein de trucs, et puis au bout d'un petit moment je laisse tomber et je fais autre chose, puis je m'y remet un peu plus tard. Sachant qu'en plus, c'est mon environnement principal, donc si je me rend compte à l'usage qu'un truc ne marche pas, ça m'embête et je dois le corriger, bah ça marche plutôt pas mal ^^