/*********************************************************************************************************************
 *
 *  ██████╗ ███╗   ███╗██████╗ ██╗     ██╗███╗   ██╗███████╗
 * ██╔════╝ ████╗ ████║██╔══██╗██║     ██║████╗  ██║██╔════╝
 * ██║      ██╔████╔██║██║  ██║██║     ██║██╔██╗ ██║█████╗
 * ██║      ██║╚██╔╝██║██║  ██║██║     ██║██║╚██╗██║██╔══╝
 * ╚██████╗ ██║ ╚═╝ ██║██████╔╝███████╗██║██║ ╚████║███████╗
 *  ╚═════╝ ╚═╝     ╚═╝╚═════╝ ╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝
 *
 *  CmdLine : Ligne de Commande interactif — Serial + Telnet
 *  Version  : v20
 *  Cible    : Arduino UNO R4 Minima
 *  Shield   : Ethernet V2 (A000024 — W5500)
 *  Librairie: Ethernet@2.0.2
 *  Licence  : Libre d'utilisation — basé sur Terminal.cpp (rweather, MIT)
 *
 *********************************************************************************************************************
 *
 *  DESCRIPTION GÉNÉRALE
 *  ────────────────────
 *  Cette librairie fournit un interpréteur de commandes (CLI) interactif
 *  accessible simultanément par deux canaux indépendants :
 *    • Serial  : port USB/UART de l'Arduino (PuTTY, moniteur série…)
 *    • Telnet  : connexion TCP sur le port 23 (PuTTY Telnet, MobaXterm…)
 *
 *  Chaque canal maintient son propre état (buffer, curseur, historique,
 *  complétion TAB) → les deux peuvent être utilisés en même temps.
 *
 *********************************************************************************************************************
 *
 *  FONCTIONNALITÉS
 *  ───────────────
 *  ┌─────────────────────────────────────────────────────────────────────┐
 *  │  Saisie de ligne                                                    │
 *  │    • Écho des caractères imprimables                                │
 *  │    • Backspace / DEL : supprime le caractère à gauche               │
 *  │    • ← → : déplace le curseur dans la ligne (insertion au milieu)   │
 *  ├─────────────────────────────────────────────────────────────────────┤
 *  │  Historique des commandes                                           │
 *  │    • ↑ ↓ : navigation dans l'historique                             │
 *  │    • Profondeur configurable : CMDLINE_HISTORY_SIZE (défaut 10)     │
 *  │    • Sauvegarde de la ligne en cours avant navigation               │
 *  │    • Pas d'enregistrement des doublons consécutifs                  │
 *  ├─────────────────────────────────────────────────────────────────────┤
 *  │  Complétion TAB                                                     │
 *  │    • 1er TAB : 0 match → BEL  /  1 match → complétion + espace      │
 *  │               N matchs → liste toutes les commandes + charge la 1ère│
 *  │    • TABs suivants : défile les correspondances (cycle Re-TAB)      │
 *  │    • Préfixe mémorisé → cycle toujours sur le même préfixe d'origine│
 *  ├─────────────────────────────────────────────────────────────────────┤
 *  │  Insensibilité à la casse (optionnel — CMDLINE_CASE_INSENSITIVE)         │
 *  │    • PING=Ping=ping  /  SaveConf, LEDon, serveurWEB... préservés        │
 *  │    • cmdStrcmpLower() : compare sans modifier la table _commands[]      │
 *  │    • Exemple : "SAVECONF 1" → trouve "SaveConf" ✓  arg="1" intact      │
 *  ├─────────────────────────────────────────────────────────────────────┤
 *  │  Sortie simultanée Serial + Telnet                                  │
 *  │    • cmdLine.print(...)         remplace Serial.print(...)          │
 *  │    • cmdLine.println(...)       remplace Serial.println(...)        │
 *  │    • cmdLine.print(val, DEC)    remplace Serial.print(val, DEC/HEX)│
 *  │    • cmdLine.write(buf, len)    remplace Serial.write(buf, len)     │
 *  │    • Supporte tous les types : int, float, String, F("..."), …      │
 *  └─────────────────────────────────────────────────────────────────────┘
 *
 *********************************************************************************************************************
 *
 *  COMPATIBILITÉ TERMINAUX (testée)
 *  ─────────────────────────────────
 *  Terminal       ENTER          Backspace   Flèches
 *  ─────────────  ─────────────  ──────────  ──────────────────
 *  PuTTY Serial   CR (0x0D)      0x08        ESC [ A/B/C/D
 *  PuTTY Telnet   CR+LF ou CR+NUL 0x08/0x7F  ESC [ A/B/C/D
 *  MobaXterm      CR+LF          0x7F        ESC [ A/B/C/D
 *
 *  Notes PuTTY Telnet :
 *    • "Return key sends Telnet New Line" coché   → ENTER = CR + LF  (0x0D 0x0A)
 *    • "Return key sends Telnet New Line" décoché → ENTER = CR + NUL (0x0D 0x00)
 *    • Les deux cas sont gérés automatiquement par l'état TS_CR (RFC 854).
 *
 *********************************************************************************************************************
 *
 *  MACHINE D'ÉTAT TELNET — readTelnetKey()
 *  ────────────────────────────────────────
 *  Chaque octet reçu du client Telnet passe par une machine d'état à 9 états.
 *  L'état est stocké dans CanalState.tstate → persiste entre les appels loop().
 *
 *  ┌──────────┐  0xFF (IAC)    ┌──────────┐  WILL    ┌──────────┐
 *  │ TS_INIT  │──────────────► │ TS_IAC   │────────► │ TS_WILL  │──► répond WILL ECHO (1 fois)
 *  │          │                │          │  WONT    ┌──────────┐       + WILL SGA
 *  │          │                │          │────────► │ TS_WONT  │──► ignoré
 *  │          │                │          │  DO      ┌──────────┐
 *  │          │                │          │────────► │  TS_DO   │──► répond WILL SGA si SGA
 *  │          │                │          │  DONT    ┌──────────┐       sinon WONT
 *  │          │                │          │────────► │ TS_DONT  │──► ignoré
 *  │          │                └──────────┘          └──────────┘
 *  │          │
 *  │          │  0x1B (ESC)    ┌──────────┐  '[' + timeout 40ms
 *  │          │──────────────► │  TS_ESC  │──────────────────────►  ┌───────────┐
 *  │          │                │          │  timeout 40ms           │ TS_MATCH  │ A→KEY_UP
 *  │          │                │          │──────────────► TS_INIT  │           │ B→KEY_DOWN
 *  │          │                └──────────┘                         │           │ C→KEY_RIGHT
 *  │          │                                                     │           │ D→KEY_LEFT
 *  │          │  0x0D (CR)     ┌──────────┐                          └───────────┘
 *  │          │──────────────► │  TS_CR   │──► KEY_RETURN
 *  │          │                │          │    puis mange 0x0A (LF) ou 0x00 (NUL)
 *  │          │                └──────────┘
 *  │          │
 *  │          │  0x09 TAB     ──► KEY_TAB
 *  │          │  0x08/0x7F BS ──► KEY_BACKSPACE
 *  │          │  0x20..0x7E   ──► caractère ASCII (retourné tel quel)
 *  │          │  autres       ──► KEY_NONE (ignoré)
 *  └──────────┘
 *
 *  Tous les états retournent à TS_INIT après traitement d'un seul octet option.
 *
 *********************************************************************************************************************
 *
 *  MACHINE D'ÉTAT ESC SERIAL — update()
 *  ──────────────────────────────────────
 *  Version simplifiée sans timeout (Serial est synchrone, les 3 octets
 *  ESC [ A arrivent dans la même lecture).
 *
 *  _serialEscState :
 *    0  ──► 0x1B reçu ──► état 1
 *    1  ──► '[' reçu  ──► état 2   /   autre → état 0
 *    2  ──► A → KEY_UP  B → KEY_DOWN  C → KEY_RIGHT  D → KEY_LEFT  → état 0
 *
 *********************************************************************************************************************
 *
 *  NÉGOCIATION TELNET — désactivation de l'écho local PuTTY
 *  ──────────────────────────────────────────────────────────
 *  PuTTY Telnet affiche localement chaque octet qu'il envoie (écho local).
 *  Sans désactivation : TAB → "^I" affiché, flèche ↑ → "^[[A" affiché.
 *
 *  Solution (RFC 857) — négociation réactive :
 *    1. Le client envoie son bloc IAC de négociation à la connexion.
 *    2. À la réception du 1er WILL du client, le serveur répond :
 *         IAC WILL ECHO  (FF FB 01) → "c'est moi qui gère l'écho"
 *         IAC WILL SGA   (FF FB 03) → mode full-duplex
 *    3. PuTTY répond IAC DO ECHO et désactive son écho local.
 *
 *  La réponse n'est envoyée QU'UNE FOIS (flag willEchoSent dans CanalState).
 *  redrawLine() sert de filet de sécurité si la négociation arrive en retard.
 *
 *********************************************************************************************************************
 *
 *  HISTORIQUE DES VERSIONS
 *  ────────────────────────
 *  v17 : En-tête et commentaires détaillés. Option CMDLINE_CASE_INSENSITIVE.
 *  v16 : Réécriture basée sur Terminal.cpp (rweather). Machine d'état Telnet
 *        complète. Timeout ESC 40ms. Backspace = \b \b. WILL ECHO réactif.
 *  v15 : Tentatives de correction écho PuTTY (non fonctionnel).
 *  v14 : Premier diagnostic par dump hexadécimal réel PuTTY + MobaXterm.
 *
 *********************************************************************************************************************
 *
 *  USAGE MINIMAL — sketch Arduino
 *  ────────────────────────────────
 *
 *  #include <Ethernet.h>
 *  #include "CmdLineTelnet_UNOR4.h"
 *
 *  CmdLine cmdLine(Serial);
 *  EthernetServer serverTelnet(23);
 *
 *  void cmdHello(const char *arg) { cmdLine.println(F("Hello !")); }
 *  void cmdPing (const char *arg) { cmdLine.print(F("pong : ")); cmdLine.println(arg); }
 *
 *  const cmd_t commands[] = {
 *      { "hello", cmdHello },
 *      { "ping",  cmdPing  },   // accessible aussi via PING, Ping... (si CASE_INSENSITIVE=1)
 *  };
 *  #define countof(a) (sizeof(a)/sizeof(a[0]))
 *
 *  void setup() {
 *      Serial.begin(115200);
 *      Ethernet.begin(mac, ip);
 *      serverTelnet.begin();
 *      cmdLine.begin(commands, countof(commands));
 *  }
 *
 *  void loop() {
 *      cmdLine.update();                                // traite Serial
 *      EthernetClient c = serverTelnet.available();
 *      cmdLine.updateTelnet(c);                        // traite Telnet
 *  }
 *
 *********************************************************************************************************************/

#ifndef __CmdLineTelnet_UNOR4_H__
#define __CmdLineTelnet_UNOR4_H__

#include <Arduino.h>
#include <Ethernet.h>


// ════════════════════════════════════════════════════════════════════════════
//  CONSTANTES PARAMÉTRABLES
//  Modifier ces valeurs selon les besoins du projet.
// ════════════════════════════════════════════════════════════════════════════

#define CMDLINE_BUFFER_SIZE      128  // Taille max d'une ligne de commande (octets)
                                      // Inclut la commande + l'espace + l'argument

#define CMDLINE_HISTORY_SIZE      10  // Nombre de commandes mémorisées dans l'historique
                                      // Navigation ↑ ↓ dans cette fenêtre circulaire
                                      // Consommation RAM :
                                      //   CMDLINE_HISTORY_SIZE × CMDLINE_BUFFER_SIZE × 2 canaux
                                      //   ex: 10 × 128 × 2 = 2560 octets

#define ESC_TIMEOUT_MS            40  // Délai max (ms) après ESC avant de conclure
                                      // que c'est la touche Escape seule (pas une flèche)
                                      // Valeur issue de Terminal.cpp (rweather)

#define SEQ_TIMEOUT_MS           200  // Délai max (ms) pour recevoir la lettre finale
                                      // d'une séquence ESC [ x (A/B/C/D)
                                      // Au-delà → séquence abandonnée

// ── Insensibilité à la casse des commandes ────────────────────────────────────
// 1 : PING = Ping = ping
//     L'entrée utilisateur est convertie en minuscules (partie commande uniquement).
//     La table _commands[] est comparée via cmdStrcmpLower() / cmdStrncmpLower()
//     → les noms peuvent avoir des majuscules : SaveConf, LEDon, serveurWEB...
//       sans être modifiés. La casse originale est préservée pour l'affichage
//       dans la complétion TAB.
//     L'argument (après l'espace) n'est JAMAIS converti.
//     Exemple : "SAVECONF 1"  → commande comparée à "SaveConf" ✓  arg="1" intact
// 0 : casse respectée (comportement original — strcmp strict)
#define CMDLINE_CASE_INSENSITIVE   1

// ── Écho des caractères saisis ────────────────────────────────────────────────
// Valeur par défaut AU DÉMARRAGE (avant tout appel à setEcho()).
// 1 : écho activé   — Arduino renvoie chaque caractère tapé (mode standard)
// 0 : écho désactivé — silencieux (utile si le terminal gère son propre écho,
//                       comme MobaXterm qui affiche les caractères 2 fois)
// Modifiable à l'exécution : cmdLine.setEcho(true) / cmdLine.setEcho(false)
#define CMDLINE_ECHO_DEFAULT       1


// ════════════════════════════════════════════════════════════════════════════
//  TYPE CALLBACK COMMANDE
// ════════════════════════════════════════════════════════════════════════════

// Pointeur vers la fonction exécutée quand une commande est reconnue.
// IN  arg : pointeur sur la partie argument de la ligne (peut être "" si absent)
typedef void (*cmd_action_t)(const char *arg);

// Entrée de la table des commandes
typedef struct {
    const char   *command;  // Nom de la commande — peut contenir des majuscules (SaveConf, LEDon...)
    cmd_action_t  action;   // Fonction à appeler
} cmd_t;


// ════════════════════════════════════════════════════════════════════════════
//  CODES TOUCHES — retournés par readTelnetKey() et produits par update()
// ════════════════════════════════════════════════════════════════════════════

#define KEY_NONE       -1     // Pas de touche disponible (attente)
#define KEY_RETURN      0x100 // ENTER  (CR, LF, ou CRLF)
#define KEY_BACKSPACE   0x101 // Backspace (0x08) ou DEL (0x7F)
#define KEY_TAB         0x102 // TAB (0x09)
#define KEY_UP          0x103 // Flèche ↑  (ESC [ A)
#define KEY_DOWN        0x104 // Flèche ↓  (ESC [ B)
#define KEY_RIGHT       0x105 // Flèche →  (ESC [ C)
#define KEY_LEFT        0x106 // Flèche ←  (ESC [ D)
// 0x20..0x7E : caractère ASCII imprimable (retourné tel quel comme int)


// ════════════════════════════════════════════════════════════════════════════
//  MACHINE D'ÉTAT TELNET — TelnetState
//
//  Stockée dans CanalState.tstate — persiste entre les appels de loop().
//  Voir l'en-tête du fichier pour le diagramme complet.
// ════════════════════════════════════════════════════════════════════════════

enum TelnetState : uint8_t {
    TS_INIT = 0,  // État normal — prêt à recevoir n'importe quel octet
    TS_CR,        // CR reçu → on attend LF ou NUL pour les manger (RFC 854)
    TS_ESC,       // ESC (0x1B) reçu → on attend '[' avec timeout ESC_TIMEOUT_MS
    TS_MATCH,     // ESC[ reçu → on attend A/B/C/D avec timeout SEQ_TIMEOUT_MS
    TS_IAC,       // IAC (0xFF) reçu → on attend la commande Telnet
    TS_WILL,      // IAC WILL reçu → on attend l'octet option
    TS_WONT,      // IAC WONT reçu → on attend l'octet option (sera ignoré)
    TS_DO,        // IAC DO   reçu → on attend l'octet option
    TS_DONT       // IAC DONT reçu → on attend l'octet option (sera ignoré)
};


// ════════════════════════════════════════════════════════════════════════════
//  STRUCTURE D'ÉTAT D'UN CANAL — CanalState
//
//  Chaque canal (Serial et Telnet) possède sa propre instance.
//  Cela garantit l'indépendance totale des deux canaux :
//  on peut taper simultanément sur le Serial et sur Telnet sans interférence.
// ════════════════════════════════════════════════════════════════════════════

struct CanalState {

    // ── Buffer de saisie ─────────────────────────────────────────────────────
    char   buf[CMDLINE_BUFFER_SIZE]; // Ligne en cours de saisie (non terminée)
    size_t len;   // Nombre de caractères valides dans buf  (0 .. BUFFER_SIZE-1)
    size_t cur;   // Position du curseur dans buf           (0=début .. len=fin)

    // ── Historique circulaire ─────────────────────────────────────────────────
    // Tableau circulaire FIFO : histHead pointe sur la prochaine case à écrire.
    // Les entrées valides sont à (histHead - histCount) .. (histHead - 1) mod N.
    char   history[CMDLINE_HISTORY_SIZE][CMDLINE_BUFFER_SIZE]; // Tableau circulaire
    int    histCount; // Nombre d'entrées valides     (0 .. CMDLINE_HISTORY_SIZE)
    int    histHead;  // Index de la prochaine écriture (avance à chaque push)
    int    histPos;   // Position de navigation :
                      //   -1 = on est sur la ligne courante (pas en navigation)
                      //    0 = entrée la plus récente
                      //    N = entrée la plus ancienne

    // ── Complétion TAB ───────────────────────────────────────────────────────
    int    tabMatchIdx;    // -1 = pas de cycle actif
                           // ≥0 = index de la prochaine correspondance à afficher
    int    tabMatchCount;  // Nombre total de correspondances pour le préfixe
    size_t tabPrefixLen;   // Longueur du préfixe mémorisé au 1er TAB
                           // Conservé pendant tout le cycle Re-TAB pour que
                           // la comparaison porte toujours sur le préfixe d'origine

    // ── Sauvegarde ligne courante (navigation historique) ────────────────────
    char   savedLine[CMDLINE_BUFFER_SIZE]; // Copie de buf avant le 1er ↑
    bool   lineSaved;     // true = savedLine contient une copie valide
                          // Permet de restaurer la ligne en cours quand on
                          // revient en bas de l'historique avec ↓

    // ── Machine d'état Telnet ────────────────────────────────────────────────
    // Utilisée uniquement par readTelnetKey() / updateTelnet().
    // Ignorée pour le canal Serial (qui a sa propre machine d'état simple).
    TelnetState   tstate;       // État courant de la machine d'état Telnet
    unsigned long timer;        // Timestamp millis() pour les timeouts ESC/SEQ
    bool          willEchoSent; // true = IAC WILL ECHO a déjà été envoyé
                                // Empêche de renvoyer la négociation plusieurs fois
};


// ════════════════════════════════════════════════════════════════════════════
//  CLASSE PRINCIPALE — CmdLine
// ════════════════════════════════════════════════════════════════════════════

class CmdLine {
public:

    // ── Constructeur ─────────────────────────────────────────────────────────
    // IN  stream : référence au stream Serial (ex: Serial, Serial1...)
    explicit CmdLine(Stream &stream);

    // ── Initialisation ───────────────────────────────────────────────────────
    // Enregistre la table des commandes et affiche le premier prompt Serial.
    // À appeler dans setup() après Serial.begin() et Ethernet.begin().
    // IN  commands : tableau de cmd_t déclaré en global dans le sketch
    // IN  num      : nombre d'entrées — utiliser countof(commands)
    void begin(const cmd_t *commands, size_t num);

    // ── Contrôle de l'écho ───────────────────────────────────────────────────
    // Active ou désactive le renvoi en écho des caractères saisis.
    //
    // Écho ACTIVÉ (true, défaut CMDLINE_ECHO_DEFAULT=1) :
    //   L'Arduino renvoie chaque caractère tapé vers le terminal.
    //   → Mode standard pour PuTTY Telnet (qui n'affiche que ce que l'Arduino envoie).
    //
    // Écho DÉSACTIVÉ (false) :
    //   L'Arduino n'envoie aucun écho des caractères imprimables.
    //   → Nécessaire pour MobaXterm dont l'écho local est activé par défaut :
    //     sans ce flag, chaque caractère apparaît 2 fois
    //     (affiché localement par MobaXterm + renvoyé par l'Arduino).
    //   → Utile pour tout terminal configuré en "local echo ON".
    //
    // Note : redrawLine(), prompt(), et les séquences ANSI (curseur, ESC[K)
    //        sont TOUJOURS envoyés, quel que soit l'état de l'écho.
    //        Seul l'écho brut des caractères imprimables dans insertChar()
    //        est conditionné par ce flag.
    //
    // Usage typique dans loop() après détection du type de terminal :
    //   cmdLine.setEcho(false);   // MobaXterm, terminal avec écho local
    //   cmdLine.setEcho(true);    // PuTTY Telnet standard (pas d'écho local)
    //
    // IN  enable : true = écho activé  /  false = écho désactivé
    void setEcho(bool enable) { _echoEnabled = enable; }

    // Retourne l'état courant de l'écho.
    // OUT bool : true = écho activé  /  false = écho désactivé
    bool getEcho() const { return _echoEnabled; }

    // ── Traitement Serial ─────────────────────────────────────────────────────
    // À appeler dans loop() à chaque cycle.
    // Lit les octets disponibles sur Serial et les traite via handleKey().
    // Retourne immédiatement si rien n'est disponible.
    void update();

    // ── Traitement Telnet ─────────────────────────────────────────────────────
    // À appeler dans loop() à chaque cycle.
    // Lit les octets disponibles sur le client Telnet, les décode via
    // readTelnetKey() (machine d'état complète) et les traite via handleKey().
    // Mémorise le pointeur client pour que print/println écrivent sur Telnet.
    // IN  client : EthernetClient courant (résultat de serverTelnet.available())
    void updateTelnet(EthernetClient &client);

    // ── Sortie simultanée Serial + Telnet ────────────────────────────────────
    // Ces fonctions remplacent Serial.print(...) + clientTelnet.print(...)
    // Envoient simultanément sur Serial ET sur le client Telnet (si connecté).

    void println();                              // Saut de ligne seul \r\n
    void print  (const __FlashStringHelper *t);  // Chaîne F("...") en PROGMEM
    void println(const __FlashStringHelper *t);  // Chaîne F("...") + \r\n

    // Template générique : couvre int, float, long, String, char[], etc.
    template<typename T>
    void print(T a) {
        Serial.print(a);
        if (_c && _c->connected()) _c->print(a);
    }
    template<typename T>
    void println(T a) {
        Serial.println(a);
        if (_c && _c->connected()) _c->println(a);
    }

    // ── Sortie avec base numérique : print(valeur, DEC/HEX/OCT/BIN) ─────────
    // Permet : cmdLine.print(valeur, DEC);  cmdLine.print(val, HEX); etc.
    // Couvre long et unsigned long (les autres types entiers se convertissent).
    // IN  n    : valeur entière à afficher
    // IN  base : DEC (10), HEX (16), OCT (8), BIN (2)
    void print  (long n,          int base);
    void println(long n,          int base);
    void print  (unsigned long n, int base);
    void println(unsigned long n, int base);

    // ── Écriture binaire brute : write(buf, len) ─────────────────────────────
    // Permet : cmdLine.write((uint8_t*)buf, idx);
    // Envoie simultanément sur Serial ET sur le client Telnet (si connecté).
    // IN  buf : pointeur sur les octets à envoyer
    // IN  len : nombre d'octets
    // OUT size_t : nombre d'octets effectivement écrits (valeur Serial)
    size_t write(const uint8_t *buf, size_t len);

private:

    // ── Décodage Telnet ───────────────────────────────────────────────────────

    // Lit UN octet du client Telnet et le décode via la machine d'état.
    // Gère : IAC (négociation protocole), ESC (flèches ANSI), CR/LF/NUL (ENTER),
    //        TAB, Backspace/DEL, caractères imprimables.
    // Retourne un KEY_xxx ou un char ASCII, ou KEY_NONE si pas encore complet.
    // IN   client : client Telnet courant
    // OUT  int    : KEY_xxx ou 0x20..0x7E ou KEY_NONE
    int readTelnetKey(EthernetClient &client);

    // Envoie une séquence de négociation Telnet : IAC cmd opt (3 octets).
    // IN  client : client destination
    // IN  cmd    : WILL / WONT / DO / DONT
    // IN  opt    : OPT_ECHO (0x01) / OPT_SGA (0x03) / autre
    void telnetCmd(EthernetClient &client, uint8_t cmd, uint8_t opt);

    // ── Traitement commun Serial + Telnet ─────────────────────────────────────

    // Traite une touche décodée (commune aux deux canaux).
    // Dispatch vers la bonne action selon le code touche.
    // IN  st  : état du canal concerné
    // IN  out : stream de sortie (Serial ou EthernetClient)
    // IN  key : KEY_xxx ou char imprimable 0x20..0x7E
    void handleKey(CanalState &st, Print &out, int key);

    // Valide la ligne, l'enregistre dans l'historique, exécute la commande,
    // puis affiche le prompt suivant.
    // IN  st, out
    void onEnter(CanalState &st, Print &out);

    // Parse st.buf, isole commande et argument, recherche dans _commands,
    // appelle l'action correspondante ou affiche "Invalid command".
    // Si CMDLINE_CASE_INSENSITIVE=1, convertit la commande en minuscules
    // avant la recherche (l'argument reste intact).
    // IN  st, out
    void processCommand(CanalState &st, Print &out);

    // Remet le buffer à zéro et affiche "> " sur le canal.
    // IN  st, out
    void prompt(CanalState &st, Print &out);

    // Réaffiche toute la ligne depuis le début : \r + "> " + buf + ESC[K.
    // Replace ensuite le curseur à la position st.cur.
    // Rôle Telnet : le \r écrase visuellement l'écho parasite de PuTTY
    //               (^I pour TAB, ^[[A pour flèche ↑…) sur la même ligne.
    // IN  st, out
    void redrawLine(CanalState &st, Print &out);

    // ── Historique ────────────────────────────────────────────────────────────

    // Ajoute cmd dans l'historique circulaire de st.
    // N'enregistre pas les lignes vides ni les doublons consécutifs.
    // IN  st
    // IN  cmd : chaîne '\0'-terminée à enregistrer
    void historyPush(CanalState &st, const char *cmd);

    // Navigue dans l'historique (↑ ou ↓) et réaffiche la ligne chargée.
    // Sauvegarde la ligne courante dans st.savedLine avant le 1er ↑,
    // la restaure quand on revient en bas avec ↓.
    // IN  st, out
    // IN  dir : -1 = ↑ (vers le passé)  /  +1 = ↓ (vers le présent)
    void historyNav(CanalState &st, Print &out, int dir);

    // ── Complétion TAB ────────────────────────────────────────────────────────

    // Complète la commande en cours sur la base du préfixe st.buf[0..cur-1].
    // 1er TAB  : compte les matchs, complète ou liste selon le résultat.
    // Re-TAB   : défile les correspondances en cycle.
    // Appelle redrawLine() après chaque modification du buffer.
    // IN  st, out
    void tabComplete(CanalState &st, Print &out);

    // ── Édition de ligne ──────────────────────────────────────────────────────

    // Déplace le curseur logique d'une position et envoie ESC[C ou ESC[D.
    // Ne fait rien aux bornes (pas de bip).
    // IN  st, out
    // IN  dir : -1 = ← gauche  /  +1 = → droite
    void moveCursor(CanalState &st, Print &out, int dir);

    // Insère le caractère c à la position st.cur dans st.buf.
    // Décale les caractères suivants vers la droite (memmove).
    // Renvoie c en écho (out.write) — seul endroit où un écho brut est émis.
    // Cas milieu de ligne : réaffiche les caractères décalés + repositionne curseur.
    // IN  st, out
    // IN  c : caractère imprimable 0x20..0x7E
    void insertChar(CanalState &st, Print &out, unsigned char c);

    // Supprime le caractère à gauche du curseur (Backspace).
    // Utilise la méthode "\b \b" de Terminal.cpp : BS + espace + BS(s).
    // Universelle PuTTY + MobaXterm (contrairement aux séquences ESC[D).
    // IN  st, out
    void delLeft(CanalState &st, Print &out);


    // ── Membres privés ────────────────────────────────────────────────────────

    Stream         &_stream;           // Référence au stream Serial (constructeur)
    EthernetClient *_c = nullptr;      // Pointeur vers le client Telnet courant
                                       // Mis à jour à chaque appel updateTelnet()
                                       // Utilisé par print() / println()

    CanalState _stS;    // État complet du canal Serial
    CanalState _stT;    // État complet du canal Telnet

    const cmd_t *_commands = nullptr;  // Table des commandes (pointeur sketch)
    size_t       _num      = 0;        // Nombre d'entrées dans _commands

    // Écho des caractères imprimables
    // true  = l'Arduino renvoie chaque caractère tapé (PuTTY Telnet, Serial)
    // false = pas d'écho brut (MobaXterm, terminal avec écho local activé)
    // Initialisé à CMDLINE_ECHO_DEFAULT, modifiable via setEcho()
    bool _echoEnabled = (CMDLINE_ECHO_DEFAULT != 0);

    // Machine d'état ESC pour Serial (plus simple que Telnet, pas de timeout)
    // 0 = init  /  1 = ESC reçu  /  2 = ESC+[ reçus
    uint8_t _serialEscState = 0;
};

#endif // __CmdLineTelnet_UNOR4_H__
