Suite à cette discussion, un écrit m'a fait tilt
En fait, si ces programmes sont des usines à gaz, c'est du fait de la conception du développeur et non d'un besoin réel.
Ce type de programme ne fait QUE générer une chaine qui va décrire notre prompt
echo $PS1
nous donne cette chaine qui peut-être par exemple \[\033[01;32m\][\u@\h\[\033[01;37m\] \w\[\033[01;32m\]]\$\[\033[00m\]
Par simplicité, ce script prend une chaine du même type en paramètre et va la transformer pour en faire un prompt personnalisé.
Ici, le programme ne fait que récupérer des sous-chaines (balises), les remplacer par des valeurs et retourner cette même chaine modifiée. Donc, même pas une usine à gaz à coder.
Pour la démo, existe 3 types de balises "perso" (toute la syntaxe PS1 reste valide !)
- couleurs au format rvb :
{#ff0000}
mettre une couleur au texte{#0}
annuler la couleur- variables (environnement et certaines internes au programme)
{$USER}
utilisateur{$ARCH}
architecture{$HEURE}
heure:minutes...
- commandes shell diverses, par exemple :
{ls -1 | wc -l}
nombre de fichiers visibles dans le répertoire courant{ls -1a | wc -l}
nombre de fichiers total dans le répertoire courantNous avons donc un programme simple pour l'utilisateur (pas plus compliqué que l'existant) et simple au niveau programmation :
1) récupérer la chaine passée en premier paramètre
2) récupérer les balises de cette chaine
3) interpréter ces balises
4) remplacer les balises par la valeur "interprétée"
5) retourner la nouvelle chaine
-------------------
Réalisation
ps: ici, le programme est écrit en python C'est clairement une mauvaise idée puisque python est lent et le charger à chaque affichage du prompt ne va pas arranger les choses. Attention, cette lenteur est très relative et ne devrait pas être visible pour un utilisateur.
Version très simplifiée avec uniquement les couleurs :
# conversion de couleur #rrvvbb en code couleur pour console
def rgb_fg(r: int, g: int, b: int) -> str:
return '\x1b[38;2;' + str(r) + ';' + str(g) + ';' + str(b) + 'm'
def html_color(html : str) -> str:
return rgb_fg(int('0x'+html[0:2], 0), int('0x'+html[2:4], 0), int('0x'+html[4:6], 0))
# on récupère la chaine passée en paramètre
ARG = sys.argv[1]
# on cherche les balises couleurs et les converties
COLORS = {"{#0}" : '\x1b[0m'} # code remise à zéro
for match in re.finditer(r'{#[a-fA-F0-9]{6}}', ARG, flags=0):
COLORS[match.group()] = html_color(match.group()[2:-1])
# on remplace ces balises couleur trouvées à la ligne précédente, par le code compréhensible par notre console
for section in COLORS:
ARG = ARG.replace( section, COLORS[section])
# et au final on ressort notre toute nouvelle chaine
print(f"{ARG}{COLORS['{#0}']}")
{#ff0000}\u{#0}@\h {#aacc00} \w \n$
Reste à faire le même chose pour les variables d'environnement et pour les commandes. Mais en fait, c'est exactement la même chose.
À noter que ici, les commandes shell vont être lancées en parallèle mais, c'est juste histoire de donner un peu de crédibilité à ce type de programme en python.
----------------
Tester / Configurer
Puisque ici, le but est la simplicité, pour tester notre futur beau prompt, il suffit de lancer notre script avec divers paramètres.
./prompter '{$USER} {#ff0000} {$PWD} \n $' -v
./prompter '{$USER}@{$MACHINE} {#ff0000} {$PWD} \n{$HEURE} $' -v
...
-v
(verbose) va afficher un log sur le fonctionnement de cette application.----------------
Installer ce programme ?
ps: le script ici, propose une option
-i
qui donne la marche à suivre# dans ~/.bashrc
function _update_ps1() {
PS1="$(prompter -pbash -e$? 2>/dev/null)"
}
PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
# Avec fish :
function fish_prompt
set -l last_pipestatus $pipestatus
eval prompter -pfish -e$last_pipestatus 2>/dev/null
end
Code du programme "prompter"
Attention, ici le prompt par défaut, si nous ne passons pas de paramètre, est chargé et sur 5 lignes
#!/usr/bin/python3
import os
import sys
from datetime import datetime
import re
import platform
import socket
from pathlib import Path
import subprocess
import concurrent.futures
try:
import psutil
except ModuleNotFoundError:
print("il manque un paquet: pacman -S python-psutil --asdeps")
exit(8)
# prompt par défaut
ARG = r'''\n➖{#779977}{$USER}{#777777}@{$MACHINE}:{$IP} Manjaro {$release}
💿{$RAM used}/{$RAM} 🏍 {$CPU freq} {$BATTERY}
🌡 {$TEMP} - {$TEMP nouveau}
{#779977}{$PWD}{#777777}{$GIT} ({$PWD count}) - {$DISK free} {$DISK writed}
{$HEURE}{#aa0000}{$ERROR}{#779977}{#f} 🟢 '''
version = "0.3"
def log(*args):
if VERBOSE:
print(*args, file=sys.stderr)
def rgb_fg(r: int, g: int, b: int) -> str:
return '\x1b[38;2;' + str(r) + ';' + str(g) + ';' + str(b) + 'm'
def html_color(html : str) -> str:
return rgb_fg(int('0x'+html[0:2], 0), int('0x'+html[2:4], 0), int('0x'+html[4:6], 0))
def run_command(index: str):
output = ""
try:
proc = subprocess.run(index[1:-1],
shell=True,
text=True,
capture_output=True,
check=True,
)
output = proc.stdout.rstrip()
except subprocess.CalledProcessError:
pass
return index, output
VERBOSE = True if "-v" in sys.argv else False
if "-i" in sys.argv:
print(""" # dans ~/.bashrc
function _update_ps1() {
PS1="$(prompter -e$? -pbash 2>/dev/null)"
}
PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND"
# avec fish:
function fish_prompt
set -l last_pipestatus $pipestatus
eval prompter -pfish -e$last_pipestatus 2>/dev/null
end
""")
exit(0)
if "-h" in sys.argv:
print("""
{#ff0000} mettre une couleur au texte
{#0} annuler la couleur
# variables environnement et particulières
{$ARCH} architecture
{$BATTERY} % de la batterie
{$CPU freq} fréquence actuelle des cpu
{$DISK free} Espace disponible sur /
{$DISK writed} Ecritures depuis le boot sur tous les disques
{$GIT} branche active
{$HEURE} heure:minutes
{$IP} ip locale de la machine
[$PARAM] libre parametre -xXXX passé au script (ex: -pBASH...-pZSH)
{$RAM} Mémoire totale
{$RAM used} mémoire utilisée
{$TEMP nouveau} température de la carte "nouveau"
{$TEMP} température des cpu
{$USER} utilisateur
# commandes shell
{ls -1 | wc -l} exemple: nombre de fichiers
""")
exit(0)
try:
log("read prompt in ~/.config/prompt.conf")
ARG = Path.home().joinpath('.config/prompt.conf').read_text()
except FileNotFoundError:
pass
try:
ARG = [x for x in sys.argv[1:] if x[0] != "-"][0]
except IndexError:
pass
VARS_ENV = {}
COLORS = {"{#0}": '\x1b[0m', "{#f}": ""}
COMMANDS = {}
try:
log(ARG)
# variables
log("Vars :")
for tag in (match.group() for match in re.finditer(r'{\$[|a-zA-Z0-9_ ]*}', ARG)):
#log('match: ', tag)
output = ""
match_str = tag[2:-1].split()
match_str[0] = match_str[0].upper()
match match_str:
case ["ARCH"]:
output = platform.uname().machine
case ["BATTERY"]:
bat = psutil.sensors_battery()
if bat:
output = f"{bat.percent:.0f}%"
case ["CPU", "freq"]:
output = f"{psutil.cpu_freq().current:.2f}Mhz"
case ["CPU"] | ["TEMP"]:
# pas possible % des cpu car il faut une tempo de 1 seconde mini
# output = f"{psutil.cpu_percent(interval=1, percpu=False):0.1f}%"
if VERBOSE:
log("psutil.sensors_temperatures()", psutil.sensors_temperatures())
temps = [f"{x.current:.0f}" for x in psutil.sensors_temperatures()['coretemp']]
output = f"{'/'.join(temps)}°"
case ["DISK", ask]:
disk_partitions = psutil.disk_partitions()
for partition in disk_partitions:
if partition.mountpoint != "/":
continue
match ask.lower():
case "free":
disk_usage = psutil.disk_usage("/")
output = f"{disk_usage.free / 1024 ** 3:0.1f}Go"
case "writed":
disk_rw = psutil.disk_io_counters()
output = f"{disk_rw.write_bytes / 1024 ** 3:0.1f}Go"
break
case ["ERROR"]:
try:
output = [x[2:] for x in sys.argv[1:] if x[0:2] == "-e"][0]
if output == "0":
output = ""
else:
COLORS["{#f}"] = '\033[0;31m'
except IndexError:
output = ""
case ["FREE", "used"] | ["RAM", "used"]:
output = f"{psutil.virtual_memory().used / 1024 ** 3:0.1f}Go"
if VERBOSE:
log("psutil.virtual_memory()", psutil.virtual_memory())
case ["GIT"]:
deep = 0
for parent in Path.cwd().joinpath('None').parents:
if parent.joinpath('.git').exists():
proc = subprocess.run("git rev-parse --abbrev-ref HEAD",
shell=True, text=True, capture_output=True, check=False)
output = f"[{'../' * deep if deep > 0 else ''}{proc.stdout.strip()}]"
break
deep += 1
case ["GPU"]:
output = f"{psutil.sensors_temperatures()['nouveau'][0].current:0.0f}°"
case ["HEURE"]:
now_date = datetime.now()
output = f"{now_date.hour:02d}:{now_date.minute:02d}"
case ["HOSTNAME"]:
output = socket.gethostname()
case ["IP"]:
output = socket.gethostbyname(socket.gethostname())
case ["PARAM"]:
try:
output = [x[2:] for x in sys.argv[1:] if x[0:2] == "-p"][0]
except IndexError:
pass
case ["PWD"]:
pwd = str(Path.cwd())
home = str(Path.home())
if pwd.startswith(home):
pwd = f"~{pwd[len(home):]}"
output = pwd
case ['PWD', "count"]:
output = len([True for x in Path.cwd().iterdir()])
case ["RAM"] | ["FREE"]:
output = str(round(psutil.virtual_memory().total / (1024.0 ** 3)))+"Go"
case ["RELEASE"]:
output = platform.uname().release
case ["TEMP", name]:
temps = [f"{x.current:.0f}" for x in psutil.sensors_temperatures()[name]]
output = f"{'/'.join(temps)}°"
case ["UNAME"] | ["MACHINE"]:
output = platform.uname().node
case _:
output = os.getenv(match_str[0], "")
VARS_ENV[tag] = str(output)
log(VARS_ENV)
log()
# commandes
log("Commands :")
futures = []
with concurrent.futures.ThreadPoolExecutor(max_workers=12) as executor:
for tag in (match.group() for match in re.finditer(r'{[^\$^\#][^}]+}', ARG)):
COMMANDS[tag] = ""
futures.append(executor.submit(run_command, tag))
log('match: ', tag)
log("A exécuter en //:", COMMANDS)
for future in concurrent.futures.as_completed(futures):
try:
tag, value = future.result()
COMMANDS[tag] = value
except Exception as e:
pass
log("Résultats des commandes shell", COMMANDS)
log()
# couleurs
log("colors :")
for tag in (match.group() for match in re.finditer(r'{#[a-fA-F0-9]{6}}', ARG)):
#log('match: ', tag)
COLORS[tag] = html_color(tag[2:-1])
log(COLORS)
log()
for section in VARS_ENV:
ARG = ARG.replace( section, VARS_ENV[section])
for section in COMMANDS:
ARG = ARG.replace( section, COMMANDS[section])
for section in COLORS:
ARG = ARG.replace( section, COLORS[section])
finally:
print(f"{ARG}{COLORS['{#0}']}", end="")