2026-02-25 : Initiation à Termux sur Android (Linux CLI sur Android)

J’ai eu un peu de temps dernièrement et j’ai voulu me faire une sorte de cyberdeck maison.

Après réflexion, acheter une uconsole ou un truc du genre c’est bien cool mais déjà c’est pas donné donné, et puis ça fait un peu le gadget qui va dormir au fond d’un tiroir au bout de 15 jours. Bref, pas vraiment l’idée d’avoir un matériel qui pourra me suivre au quotidien et être utile.

C’est là que rentre en ligne de compte termux :

https://termux.dev/en/

En gros, une app android, dispo sur F-Droid qui permet d’avoir un CLI Linux sur son smartphone. Top !

Je l’ai donc installé et ça fonctionne nickel. J’ai installé nano, micro pour éditer mon texte et on peut aussi lié l’app au système de fichier du téléphone (la SDcard). Voir :

https://wiki.termux.com/wiki/Termux-setup-storage

https://wiki.termux.com/wiki/Internal_and_external_storage

OK c’est cool ! Et dans la foulée, je voulais réduire les app que j’avais sur mon téléphone. Donc j’ai commencé à faire des scripts python pour remplacer deux ou trois trucs, du genre :

Et en plus, j’ai monté un petit site neocities qui me remonte certaines infos (journal, météo, calculatrice) grâce au module neocitizen :

https://pypi.org/project/neocitizen/

Donc ainsi j’ai un backup automatique de mes data (notamment le journal et la calculatrice, ce qui permet de moins craindre une foirade de mon téléphone).

Je pourrais aussi remonter les infos directement sur la capsule gemini, c’est en cours de réflexion.

Scripts

Attention : rajouter un # devant !/usr/bin/env python3 (là c’est détecter comme un titre niveau 1 donc je l’ai viré).

Récupérer les 10 derniers articles de Korben

!/usr/bin/env python3 import feedparser import requests import html import textwrap from bs4 import BeautifulSoup from unidecode import unidecode

RSS_URL = “https://korben.info/feed” NB_ARTICLES = 10 NB_LIGNES = 8 LINE_WIDTH = 52 # largeur du terminal

HEADERS = { “User-Agent”: “Mozilla/5.0 (korben-cli)” }

def normalize_text(text): if not text: return “” text = html.unescape(text) text = unidecode(text) return text.strip()

def format_text(text, width=LINE_WIDTH): return textwrap.fill( text, width=width, replace_whitespace=True, drop_whitespace=True )

def get_first_lines(url, max_lines=5): try: r = requests.get(url, headers=HEADERS, timeout=10) r.raise_for_status()

    soup = BeautifulSoup(r.content, "html.parser")

    article = soup.find("article")
    if not article:
        return ["[contenu introuvable]"]

    lines = []
    for p in article.find_all("p"):
        raw = p.get_text(" ", strip=True)
        text = normalize_text(raw)

        if text:
            lines.append(text)

        if len(lines) >= max_lines:
            break

    return lines if lines else ["[article vide ou protege]"]

except Exception as e:
    return [normalize_text(f"[erreur: {e}]")]

def main(): feed = feedparser.parse(RSS_URL)

print("\nDerniers articles de korben.info (lecture confortable)\n")

for i, entry in enumerate(feed.entries[:NB_ARTICLES], start=1):
    title = normalize_text(entry.title)
    print(f"{i}. {title}")
    print("─" * 60)

    for line in get_first_lines(entry.link, NB_LIGNES):
        formatted = format_text(line)
        for wrapped_line in formatted.splitlines():
            print(f"   {wrapped_line}")
        print()

    print("-" * 60 + "\n")

if name == “main”: main()

Journal en markdown

!/usr/bin/env python3

from datetime import datetime from pathlib import Path import sys import subprocess

JOURNAL_FILE = Path(“logs/journal.md”)

def read_multiline(): print(“Saisie du journal (Ctrl+D pour terminer, Ctrl+C pour annuler) :”) lines = [] try: while True: lines.append(input()) except EOFError: pass return “”.join(lines).strip()

def main(): content = read_multiline()

if not content:
    print("Entrée vide, rien ajouté.")
    sys.exit(0)

timestamp = datetime.now().strftime("%Y-%m-%d : %H-%M-%S")
entry = f"# {timestamp}\n\n{content}\n\n"

if JOURNAL_FILE.exists():
    previous = JOURNAL_FILE.read_text(encoding="utf-8")
else:
    previous = ""

JOURNAL_FILE.write_text(entry + previous, encoding="utf-8")
print("Entrée ajoutée au journal.")

subprocess.run(["bash","publish.sh"])

if name == “main”: main()

Météo avec coordonnées GPS manuelles + villes aux alentours

!/usr/bin/env python3

import requests from datetime import datetime, timedelta import subprocess

URL = “https://api.open-meteo.com/v1/forecast” TIMEZONE = “Europe/Paris” MD_FILE = “logs/meteo.md”

CITIES = { “Vendôme”: (47.793, 1.065), “Le Mans”: (48.0061, 0.1996), “Tours”: (47.3941, 0.6848), “Orléans”: (47.9029, 1.9093), “Châteauroux”: (46.8090, 1.6910), “Poitiers”: (46.5802, 0.3404), “Chartres”: (48.4469, 1.4890), }

def fetch_weather(lat, lon): params = { “latitude”: lat, “longitude”: lon, “hourly”: [ “temperature_2m”, “precipitation”, “windspeed_10m” ], “daily”: [ “temperature_2m_max”, “temperature_2m_min”, “precipitation_sum”, “windspeed_10m_max” ], “timezone”: TIMEZONE } r = requests.get(URL, params=params, timeout=10) r.raise_for_status() return r.json()

def print_cli_vendome(data): now = datetime.now() limit = now + timedelta(hours=24)

print("\n⏱️  Météo prochaines 24h – Vendôme (pas de 2h)\n")

hourly = data["hourly"]
times = [datetime.fromisoformat(t) for t in hourly["time"]]

for i, t in enumerate(times):
    if now <= t <= limit and t.hour % 2 == 0:
        print(
            f"{t.strftime('%a %H:%M')} | "
            f"{hourly['temperature_2m'][i]:>4.1f}°C | "
            f"🌧️ {hourly['precipitation'][i]:>4.1f} mm | "
            f"💨 {hourly['windspeed_10m'][i]:>4.1f} km/h"
        )

print("\n📅 Prévisions J+2 / J+3\n")

daily = data["daily"]
for i in range(2, 4):
    date = datetime.fromisoformat(daily["time"][i]).strftime("%A %d %B")
    print(f"{date}")
    print(f"  🌡️ {daily['temperature_2m_min'][i]:.1f}°C → {daily['temperature_2m_max'][i]:.1f}°C")
    print(f"  🌧️ Pluie : {daily['precipitation_sum'][i]:.1f} mm")
    print(f"  💨 Vent max : {daily['windspeed_10m_max'][i]:.1f} km/h\n")

def write_markdown(all_data): now = datetime.now() limit = now + timedelta(hours=24)

with open(MD_FILE, "w", encoding="utf-8") as f:
    f.write("# 🌦️ Bulletin météo régional\n\n")
    f.write(f"_Généré le {now.strftime('%d/%m/%Y à %H:%M')}_\n\n")

    for city, data in all_data.items():
        hourly = data["hourly"]
        daily = data["daily"]

        f.write(f"## {city}\n\n")
        f.write("### ⏱️ Prochaines 24h (pas de 2h)\n\n")

        times = [datetime.fromisoformat(t) for t in hourly["time"]]

        for i, t in enumerate(times):
            if now <= t <= limit and t.hour % 2 == 0:
                f.write(
                    f"- **{t.strftime('%a %H:%M')}** : "
                    f"{hourly['temperature_2m'][i]:.1f}°C, "
                    f"🌧️ {hourly['precipitation'][i]:.1f} mm, "
                    f"💨 {hourly['windspeed_10m'][i]:.1f} km/h\n"
                )

        f.write("\n### 📅 J+2 / J+3\n\n")

        for i in range(2, 4):
            date = datetime.fromisoformat(daily["time"][i]).strftime("%A %d %B")
            f.write(
                f"- **{date}** : "
                f"{daily['temperature_2m_min'][i]:.1f}°C → {daily['temperature_2m_max'][i]:.1f}°C, "
                f"🌧️ {daily['precipitation_sum'][i]:.1f} mm, "
                f"💨 {daily['windspeed_10m_max'][i]:.1f} km/h\n"
            )

        f.write("\n---\n\n")

def main(): all_data = {}

for city, (lat, lon) in CITIES.items():
    all_data[city] = fetch_weather(lat, lon)

# CLI = Vendôme uniquement
print_cli_vendome(all_data["Vendôme"])

# Markdown = toutes les villes
write_markdown(all_data)

print(f"📄 Fichier {MD_FILE} mis à jour (Vendôme inclus)")

subprocess.run(["bash","publish.sh"])

if name == “main”: main()

Scanner IP locales

!/usr/bin/env python3 import ipaddress import socket import subprocess import platform from concurrent.futures import ThreadPoolExecutor

NETWORK_CIDR = “192.168.1.0/24” # <– À MODIFIER SELON LE RÉSEAU TIMEOUT_MS = “500” MAX_WORKERS = 50

def ping(ip): system = platform.system().lower() if system == “windows”: cmd = [“ping”, “-n”, “1”, “-w”, TIMEOUT_MS, str(ip)] else: cmd = [“ping”, “-c”, “1”, “-W”, “1”, str(ip)]

result = subprocess.run(
    cmd,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL
)
return result.returncode == 0

def resolve_hostname(ip): try: return socket.gethostbyaddr(str(ip))[0] except Exception: return None

def scan_ip(ip): if ping(ip): hostname = resolve_hostname(ip) return (str(ip), hostname) return None

def main(): network = ipaddress.ip_network(NETWORK_CIDR, strict=False)

print(f"🔍 Scan du réseau : {network}")
print("-" * 50)

results = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
    for res in executor.map(scan_ip, network.hosts()):
        if res:
            results.append(res)

for ip, hostname in results:
    if hostname:
        print(f"{ip:15}  →  {hostname}")
    else:
        print(f"{ip:15}  →  (hostname inconnu)")

print("-" * 50)
print(f"✔ {len(results)} hôtes détectés")

if name == “main”: main()

La super calculatrice avec logs

!/usr/bin/env python3 ““” Calculatrice interactive en ligne de commande.

Fonctionnalités : - Calculs mathématiques avec parenthèses - Mémoires persistantes mem1 à mem9 - Enchaînement des calculs à partir du dernier résultat - Commentaires libres enregistrés dans l’historique - Historique sauvegardé dans calc.md (incrémental) - Commandes : mem1..mem9, list, erase, quit

Usage : - Taper une expression mathématique puis Entrée - Taper +3, *2, etc. pour continuer depuis le dernier résultat - Taper du texte libre pour ajouter un commentaire ““”

import ast import operator import datetime import subprocess

HISTORY_FILE = “logs/calc.md”

OPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, }

def safe_eval(expr, variables): def _eval(node): if isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.BinOp): return OPStype(node.op) elif isinstance(node, ast.UnaryOp): return OPStype(node.op) elif isinstance(node, ast.Name): if node.id in variables and variables[node.id] is not None: return variables[node.id] raise ValueError(f”Variable inconnue: {node.id}“) else: raise TypeError(”Expression non autorisée”)

tree = ast.parse(expr, mode="eval")
return _eval(tree.body)

def write_history(lines): new_content = “”.join(lines).strip() + “”

try:
    with open(HISTORY_FILE, "r", encoding="utf-8") as f:
        old_content = f.read()
except FileNotFoundError:
    old_content = ""

with open(HISTORY_FILE, "w", encoding="utf-8") as f:
    f.write(new_content + old_content)

def is_incremental(expr): return expr[0] in “+-*/”

def main(): memories = {f”mem{i}“: None for i in range(1, 10)} last_result = None session_log = []

session_log.append(f"\n## Session {datetime.datetime.now().isoformat(timespec='seconds')}")

print("Calculatrice interactive")
print("Commandes : mem1..mem9 | list | erase | quit")

while True:
    try:
        raw = input(">>> ").strip()

        if not raw:
            continue

        if raw == "quit":
            break

        if raw == "list":
            for k in memories:
                print(f"{k}: {memories[k]}")
            continue

        if raw == "erase":
            for k in memories:
                memories[k] = None
            print("Mémoires vidées.")
            session_log.append("- **Mémoires vidées**")
            continue

        if raw in memories:
            if last_result is None:
                print("Aucun résultat à mémoriser.")
            else:
                memories[raw] = last_result
                print(f"{raw} = {last_result}")
                session_log.append(f"- `{raw}` ← **{last_result}**")
            continue

        expr = raw

        if last_result is not None and is_incremental(expr):
            expr = f"{last_result}{expr}"

        try:
            result = safe_eval(expr, memories)
            print(result)
            session_log.append(f"- `{expr}` = **{result}**")
            last_result = result
        except Exception:
            # commentaire libre
            print("(commentaire)")
            session_log.append(f"- 💬 {raw}")

    except KeyboardInterrupt:
        print("\nInterruption.")
        break

write_history(session_log)
print("Historique sauvegardé dans calc.md")

subprocess.run(["bash","publish.sh"])

if name == “main”: main()

Script publish pour balancer sur neocities les pages

Il faut installer pandoc pour la conversion de .md vers .html et que le résultat soit à peu près propre. Ne pas oublier de rajouter le # devant !/bin/bash.

!/bin/bash

pandoc logs/journal.md
-s
-M charset=utf-8
-o neocities/journal.html

pandoc logs/meteo.md
-s
-M charset=utf-8
-o neocities/meteo.html

pandoc logs/calc.md
-s
-M charset=utf-8
-o neocities/calc.html

export NEOCITIES_API_KEY=MON_API_KEY_NEOCITIES

neocitizen upload –dir=$HOME/scripts/neocities

Contenu sous licence CC BY-NC-SA 4.0