C# et la classe Console

C# et la classe Console

2018-06-17 0 By Sauleil

Tout a commencé quand j’ai voulu faire une preuve de concept: faire un Shell qui roule du C# comme script au lieu de faire du Power Shell ou des Batch File. Bien sûr, ce n’était pas aussi simple que je pensais. Donc, dans cet article, je vais tenter d’expliquer mes différentes découvertes par rapport à la console et son intégration en C#.

ReadLine

Tout d’abord, tout le monde connait probablement les fonctions de base. On s’en sert fréquamment pour débugger ou faire des programmes simples:

Console.WriteLine()
Console.ReadLine()

Donc, une des premières choses que je voulais faire, était de lire un commande. J’ai commencé par faire un simple “Console.ReadLine“. Même si ça fonctionne plutôt bien pour les cas simples, si on veut faire des raccourcis comme les flèches ou le tab, on est plutôt malpris. Donc on doit se rabbatre sur une méthode un peu plus manuelle. Voici la méthode “ReadLine()” ré-écrite à l’aide du “Read()” (simplifié):

public static string ReadLine()
{
  var sb = new StringBuilder();
  ConsoleKeyInfo character;
  do
  {
    character = Console.ReadKey(true);
    switch (character.Key)
    {
      case ConsoleKey.Enter:
        Console.WriteLine();
        break;
      default:
        sb.Append(character.KeyChar);
        Console.Write(character.KeyChar);
        break;
    }

  } while (character.Key != ConsoleKey.Enter);
  
  return sb.ToString();
}

Remarquez le paramètre de la fonction “Console.ReadKey(true)“. Le paramète  qu’on met à true est pour intercepter l’input de la console. Donc le comportement normal où le caractère est imprimé automatiquement n’arrivera plus. C’est pourquoi on doit faire des “Console.Write()” pour chacun des caractères, comme ils n’apparaîtront pas sur la console.

La raison pour faire cela est pour pouvoir supporter les cas spéciaux. Par exemple, si on veut utiliser la fèche par en haut pour afficher les dernières commandes exécutées (historique), si on n’intercepte pas la clef, nous allons voir des caractères supplémentaires non voulu apparaître dans la console et certaines clef seront impossible à intercepté à l’aide du “Console.ReadLine()“.

Donc dans le “switch” de l’exemple précédent, nous pourrons ainsi ajouter des cas supplémentaires:

case ConsoleKey.Tab:
  AutoComplete();
  break;

case ConsoleKey.UpArrow:
  ShowHistory(-1);
  break;

case ConsoleKey.DownArrow:
  ShowHistory(1);
  break;

case ConsoleKey.LeftArrow:
  MoveCursor(-1, character.Modifiers);
  break;

case ConsoleKey.RightArrow:
  MoveCursor(1, character.Modifiers);
  break;

Cursor

Une fois cette problématique réglée, il fallait ensuite comprendre comment contrôler le curseur. Comme je voulais pouvoir déplacer mon curseur de gauche à droite, il fallait que je découvre et comprenne les fonctions reliées au curseur:

Console.CursorLeft
Console.CursorTop

À partir de ces 2 propriétés, tout ce qui reste à faire sont des manipulations de string et l’utilisation de “Console.Write()“, que ce soit pour insérer ou effacer.

Et comme je suis dans les fonctions statiques des curseurs, on peut aussi changer la couleur de background et de foreground. Par contre si vous avez plusieurs fonctions différentes, assurez-vous de toujours remettre vos valeurs par défauts.

Console.BackgroundColor
Console.ForegroundColor

Editeur

Après avoir implémenté quelques fonctions de base (ls, mkdir, rm, cd, etc.), je me suis dit que ce serait très intéressant d’avoir un genre d’éditeur (comme nano ou vim) pour la console. En ce moment, dans la “cmd” de Windows, il faut appeler “notepad %file%” pour pouvoir lire un fichier. Je me suis dit que ça pourrait-être intéressant à implémenter.

Après avoir fait le design et commencé l’implémentation, je me suis buté sur un problème qui me semblait plus compliqué que je pensais. Quand je tapais “edit” dans mon command line, il fallait que j’initialise la windows de la console. Je voulais que quand on quitte la fenêtre du Edit, réinitialiser avec ce qui avait dans la fenêtre originale.

Première Ébauche

Je me suis dit que si je prenais la hauteur de la fenêtre et que je faisais des “WriteLine()” et que je jouais dans cette zone en utilisant les fonctions du curseur, tout fonctionnerait parfaitement. Et au moment de quitter, j’ai juste à écrire des espaces partout et remettre le curseur au début où on avait fait “edit\n“.

J’épargne les détails, mais une série de bugs se succédaient à chaque fois que j’en réglais un. Je me suis alors dit que si c’est si compliqué, il y a sûrement une alternative.

Deuxième Ébauche

Mon idée cette fois-ci, après avoir bien lu toutes les méthodes de la classe Console était:

  1. Faire un backup du buffer
  2. Changer la taille du buffer pour qu’elle soit la même que la fenêtre.
  3. Faire le code de l’éditeur dans ce nouveau buffer
  4. Quand on quitte, remettre l’ancien buffer
  5. Remettre le curseur à la bonne place.

Bon, cette fois-ci, j’étais déjà bloqué à la première étape de mon pseudo code. Comment se faisait-il qu’il n’y avait pas de méthode pour lire/écrire le buffer en C#. StackOverflow allait devenir encore une fois mon meilleur ami. Je me disais qu’il y avait sûrement des méthodes “Win32” qui permettaient plus de contrôle. Après quelques recherches, je trouva les appels suivant:

// See Editor/ConsoleSnapshot.cs

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadConsoleOutput(IntPtr hConsoleOutput, IntPtr lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpReadRegion);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteConsoleOutput(IntPtr hConsoleOutput, IntPtr lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpWriteRegion);

J’avais pensé d’ajouter ces méthodes comme extensions à la classe Console, mais comme c’est des méthodes globales et que je ne voulais pas copier la mémoire unmanaged dans du managed et vice versa, j’ai encapsulé dans une classe ConsoleSnapshot.

Donc à l’aide de ces méthode j’ai pu compléter l’étape 1 sans trop de difficultés. Bien sûr, pour faire l’éditeur complet, l’implémentation que j’ai faite est assez complexe. J’ai tenté reproduire le clavier de Jetbrain. Voir le code dans GIT pour plus de détails.

Git

https://bitbucket.org/angedelamort/cmdsharp/src/master/