Contactez-nous

Compétences mises en oeuvre : utilisation de `struct` pour modéliser une tâche, `Vec<T>` pour stocker les tâches, manipulation de chaînes, fonctions, interaction basique avec l'utilisateur

Maîtrisez les compétences Rust pour un gestionnaire de tâches CLI : structs pour les données, Vec pour le stockage, manipulation de chaînes, fonctions modulaires et interaction utilisateur.

Les outils Rust pour construire votre gestionnaire de tâches

La création de notre mini outil CLI de gestion de tâches va nous amener à mobiliser un ensemble spécifique et puissant de fonctionnalités du langage Rust. Ce projet est une excellente occasion de mettre en pratique des concepts allant de la définition de nos propres types de données à la gestion de collections dynamiques, en passant par l'organisation du code et l'interaction avec l'utilisateur. Ce sous-chapitre détaille les compétences essentielles que nous allons développer et utiliser.

Chacune de ces compétences représente un pilier dans la construction d'applications Rust robustes et bien structurées. En les appliquant dans le contexte d'un outil utilitaire concret, vous solidifierez votre compréhension de leur rôle et de leur mise en oeuvre. De la modélisation des données à l'orchestration des fonctionnalités, Rust nous offre des outils élégants et performants.

Préparez-vous à définir la structure de vos données, à gérer des listes évolutives, à découper et analyser le texte fourni par l'utilisateur, à architecturer votre code de manière logique, et à créer une interface en ligne de commande simple mais efficace.

Modéliser les données : l'utilisation de `struct` pour une tâche

Au coeur de notre gestionnaire se trouve la notion de "tâche". Pour représenter une tâche dans notre code Rust, nous utiliserons une structure (struct). Les structures nous permettent de regrouper plusieurs types de données apparentées sous un nom unique, créant ainsi un nouveau type personnalisé. Pour notre version de base, une tâche pourrait simplement avoir une description.

// Définition de notre structure Task
struct Task {
    description: String, // La description textuelle de la tâche
    // Plus tard, on pourrait ajouter d'autres champs :
    // id: usize,
    // completed: bool,
}

// Exemple de création d'une instance de Task
fn main() {
    let my_task = Task {
        description: String::from("Apprendre Rust"),
    };

    println!("Tâche : {}", my_task.description);
}

Dans cet exemple, Task est une structure avec un seul champ, description, de type String. Ce type est approprié car la description d'une tâche peut avoir une longueur variable et contenir n'importe quel caractère. L'utilisation de struct rend notre code plus expressif et plus facile à comprendre : au lieu de manipuler des chaînes de caractères isolées, nous manipulons des instances de Task, ce qui est sémantiquement plus clair.

Nous pourrions également implémenter des méthodes directement sur notre structure Task en utilisant un bloc impl Task { ... }, par exemple pour afficher une tâche de manière formatée ou pour la marquer comme complétée dans une version plus avancée.

Stocker une liste de tâches : le vecteur `Vec<T>`

Puisque notre application doit gérer une liste potentiellement croissante de tâches, nous avons besoin d'une structure de données capable de stocker une collection d'éléments de même type et de s'adapter dynamiquement. Le type vecteur de Rust, Vec<T>, est parfait pour cela. Un Vec pourra contenir toutes nos instances de Task.

struct Task {
    description: String,
}

fn main() {
    // Créer un vecteur vide pour stocker nos tâches
    let mut tasks: Vec = Vec::new(); // `mut` car nous allons y ajouter des éléments

    // Ajouter une première tâche
    let task1 = Task { description: String::from("Acheter du lait") };
    tasks.push(task1);

    // Ajouter une deuxième tâche
    let task2 = Task { description: String::from("Payer les factures") };
    tasks.push(task2);

    // Lister les tâches
    println!("Liste des tâches :");
    for (index, task) in tasks.iter().enumerate() {
        println!("{}. {}", index + 1, task.description);
    }
}

Ici, tasks est un vecteur mutable qui peut contenir des objets Task. La méthode push() permet d'ajouter un nouvel élément à la fin du vecteur. Pour afficher les tâches, nous utilisons iter().enumerate() qui nous donne à la fois l'index et une référence à chaque tâche, ce qui est pratique pour un affichage numéroté. Les vecteurs sont une des collections les plus fondamentales et les plus utilisées en Rust.

Traiter les commandes : manipulation de chaînes de caractères

L'utilisateur interagira avec notre programme en tapant des commandes comme "ajouter Ma nouvelle tâche" ou "lister". Notre code devra donc être capable de manipuler des chaînes de caractères (String et &str) pour parser ces entrées.

Cela implique typiquement plusieurs opérations :

  • Lire la ligne entière : Comme dans le projet précédent, avec io::stdin().read_line().
  • Nettoyer l'entrée : Souvent, en utilisant trim() pour enlever les espaces superflus au début et à la fin, ainsi que le caractère de nouvelle ligne.
  • Séparer les mots : Pour distinguer la commande (ex: "ajouter") de ses arguments (ex: "Ma nouvelle tâche"), la méthode split_whitespace() est très utile. Elle retourne un itérateur sur les sous-chaînes séparées par des espaces.
  • Reconstruire des arguments : Si un argument est composé de plusieurs mots (comme la description d'une tâche), il faudra les regrouper après avoir identifié la commande.
// Exemple de parsing simple d'une commande
fn main() {
    let user_input = "ajouter  Préparer la présentation pour demain   ";
    let trimmed_input = user_input.trim(); // "ajouter  Préparer la présentation pour demain"

    let mut words = trimmed_input.split_whitespace();

    // Le premier mot est la commande
    let command = words.next(); // `next()` avance l'itérateur et retourne un Option<&str>

    match command {
        Some("ajouter") => {
            // Le reste des mots constitue l'argument (la description de la tâche)
            let description: String = words.collect::>().join(" ");
            if description.is_empty() {
                println!("Erreur: La description de la tâche ne peut pas être vide.");
            } else {
                println!("Commande: ajouter, Description: '{}'", description);
                // Ici, on ajouterait la tâche à notre Vec
            }
        }
        Some("lister") => {
            println!("Commande: lister");
            // Ici, on listerait les tâches
        }
        Some(cmd) => {
            println!("Commande inconnue: '{}'", cmd);
        }
        None => {
            println!("Veuillez entrer une commande."); // L'utilisateur n'a rien tapé
        }
    }
}

Cette gestion des chaînes est cruciale pour toute application CLI qui doit interpréter des instructions textuelles.

Organiser le code : l'importance des fonctions

A mesure que notre application grandit, il devient essentiel d'organiser le code en fonctions pour maintenir la lisibilité, la modularité et faciliter la maintenance. Au lieu d'écrire tout le code dans la fonction main(), nous allons décomposer les différentes logiques en fonctions dédiées.

Par exemple, nous pourrions avoir :

  • Une fonction fn add_task(tasks: &mut Vec, description: String) qui prend une référence mutable au vecteur de tâches et la description de la nouvelle tâche, et s'occupe de créer l'objet Task et de l'ajouter au vecteur.
  • Une fonction fn list_tasks(tasks: &Vec) qui prend une référence immuable au vecteur de tâches et s'occupe de les afficher.
  • Une fonction fn process_command(line: String, tasks: &mut Vec) -> bool qui prend la ligne entrée par l'utilisateur et la référence mutable aux tâches, parse la commande, appelle la fonction appropriée, et retourne peut-être un booléen indiquant si le programme doit continuer ou s'arrêter (par exemple, si la commande est "quitter").
struct Task { description: String }

fn add_task(tasks: &mut Vec, description: String) {
    if description.trim().is_empty() {
        println!("La description ne peut pas être vide.");
        return;
    }
    let new_task = Task { description };
    tasks.push(new_task);
    println!("Tâche ajoutée.");
}

fn list_tasks(tasks: &Vec) {
    if tasks.is_empty() {
        println!("Aucune tâche à afficher.");
        return;
    }
    println!("Vos tâches :");
    for (i, task) in tasks.iter().enumerate() {
        println!("{}. {}", i + 1, task.description);
    }
}

// main() orchestrerait l'appel à process_command dans une boucle
// process_command appellerait add_task ou list_tasks
fn main() {
    let mut all_tasks: Vec = Vec::new();
    // ... boucle de lecture d'entrée utilisateur ...
    // let command_line = "ajouter Nouvelle tâche"; // exemple d'entrée
    // if command_line.starts_with("ajouter") { 
    //     let desc = command_line.strip_prefix("ajouter ").unwrap_or("").to_string();
    //     add_task(&mut all_tasks, desc);
    // } else if command_line == "lister" {
    //     list_tasks(&all_tasks);
    // }
    // ... etc. ...
}

L'utilisation de fonctions améliore grandement la structure du code et permet de réutiliser des blocs logiques. C'est une pratique fondamentale en programmation.

Interface utilisateur : interaction basique en ligne de commande

Enfin, l'interaction basique avec l'utilisateur en ligne de commande est une compétence que nous continuerons de développer. Cela inclut :

  • Afficher une invite claire (prompt) pour indiquer que le programme attend une entrée.
  • Lire l'entrée de l'utilisateur (avec std::io::stdin().read_line()).
  • Fournir un feedback approprié après chaque commande (confirmation d'ajout, affichage de la liste, messages d'erreur pour commandes inconnues ou arguments invalides).
  • Gérer une boucle principale qui continue de s'exécuter jusqu'à ce que l'utilisateur décide de quitter.

La clarté des messages et la prévisibilité du comportement du programme sont essentielles pour une bonne expérience utilisateur, même pour un outil CLI simple. Anticiper les erreurs de saisie courantes et guider l'utilisateur fait partie intégrante de cette compétence.

En combinant ces différentes compétences – modélisation avec struct, gestion de collections avec Vec, manipulation de chaînes, organisation en fonctions, et interaction CLI – vous serez bien équipé pour créer des applications Rust textuelles utiles et bien conçues.