diff --git a/.memo-notes/note_1750279605.note b/.memo-notes/note_1750279605.note
new file mode 100644
index 0000000..b65e346
--- /dev/null
+++ b/.memo-notes/note_1750279605.note
@@ -0,0 +1,10 @@
+---
+title: First Note
+created: 2025-06-18T16:46:45.100499-04:00
+modified: 2025-06-18T16:46:45.101096-04:00
+tags:
+ - foo
+ - bar
+---
+
+this is a first note. How do I end it? just with a `return`?
\ No newline at end of file
diff --git a/.memo-notes/note_1750280302.note b/.memo-notes/note_1750280302.note
new file mode 100644
index 0000000..19683d6
--- /dev/null
+++ b/.memo-notes/note_1750280302.note
@@ -0,0 +1,25 @@
+---
+title: Another Note
+created: 2025-06-18T16:58:22.701219-04:00
+modified: 2025-06-18T16:58:22.70125-04:00
+---
+
+Sonnet 120 - the Author.
+
+That you were once unkind befriends me now,
+And for that sorrow, which I then did feel,
+Needs must I under my transgression bow,
+Unless my nerves were brass or hammer’d steel.
+
+For if you were by my unkindness shaken,
+As I by yours, you’ve passed a hell of time;
+And I, a tyrant, have no leisure taken
+To weigh how once I suffered in your crime.
+
+O! that our night of woe might have remembered
+My deepest sense, how hard true sorrow hits,
+And soon to you, as you to me, then tendered
+The humble salve, which wounded bosoms fits!
+
+But that your trespass now becomes a fee;
+Mine ransoms yours, and yours must ransom me.
diff --git a/.memo-notes/note_1753813035.note b/.memo-notes/note_1753813035.note
new file mode 100644
index 0000000..81a52c0
--- /dev/null
+++ b/.memo-notes/note_1753813035.note
@@ -0,0 +1,7 @@
+---
+title: First Note
+created: 2025-07-29T14:17:15.05061-04:00
+modified: 2025-07-29T14:17:15.050651-04:00
+---
+
+this is a first note. and a royal pain.
\ No newline at end of file
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..d22a794
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,317 @@
+# Memo Application Architecture
+
+This document outlines the architectural structure of the Memo CLI application (a version of Future-Proof-Notes) after refactoring to implement clean separation of concerns and the Command Pattern.
+
+## Overall Architecture
+
+```mermaid
+graph TB
+ %% Entry Point
+ Main[main.go
Entry Point] --> App[cmd/App
Application Controller]
+
+ %% Command Layer
+ App --> CR[cmd/CommandRegistry
Command Management]
+ CR --> CC[CreateCommand]
+ CR --> LC[ListCommand]
+ CR --> RC[ReadCommand]
+ CR --> EC[EditCommand]
+ CR --> DC[DeleteCommand]
+ CR --> SC[SearchCommand]
+ CR --> STC[StatsCommand]
+ CR --> HC[HelpCommand]
+
+ %% Command Context
+ CC --> CTX[CommandContext
Shared State]
+ LC --> CTX
+ RC --> CTX
+ EC --> CTX
+ DC --> CTX
+ SC --> CTX
+ STC --> CTX
+ HC --> CTX
+
+ %% Internal Layers
+ CTX --> Storage[internal/storage
FileStorage]
+ CC --> UI[internal/ui
User Interface]
+ LC --> UI
+ RC --> UI
+ EC --> UI
+ DC --> UI
+ SC --> UI
+ STC --> UI
+ HC --> UI
+
+ %% Domain Layer
+ Storage --> NotePackage[internal/note
Domain Models]
+ UI --> NotePackage
+
+ %% External Dependencies
+ Storage --> FS[File System
.memo-notes/]
+ UI --> Console[Console I/O
stdin/stdout]
+ Note --> YAML[YAML Library
gopkg.in/yaml.v3]
+
+ %% Styling
+ classDef entryPoint fill:#e1f5fe
+ classDef cmdLayer fill:#f3e5f5
+ classDef internal fill:#e8f5e8
+ classDef domain fill:#fff3e0
+ classDef external fill:#ffebee
+
+ class Main entryPoint
+ class App,CR,CC,LC,RC,EC,DC,SC,STC,HC,CTX cmdLayer
+ class Storage,UI internal
+ class NotePackage domain
+ class FS,Console,YAML external
+```
+
+## Package Structure
+
+```mermaid
+graph LR
+ subgraph "Application Entry"
+ M[main.go]
+ end
+
+ subgraph "Command Layer (cmd/)"
+ A[App]
+ CI[Command Interface]
+ CC[Command Implementations]
+ CTX[CommandContext]
+ end
+
+ subgraph "Internal Packages (internal/)"
+ subgraph "Domain (note/)"
+ NoteModel[Note]
+ NM[Metadata]
+ end
+
+ subgraph "Storage (storage/)"
+ FS[FileStorage]
+ end
+
+ subgraph "UI (ui/)"
+ UI[User Interface]
+ end
+ end
+
+ subgraph "External Dependencies"
+ YAML[YAML Parser]
+ FileSystem[File System]
+ Console[Console I/O]
+ end
+
+ %% Dependencies
+ M --> A
+ A --> CI
+ CI --> CC
+ CC --> CTX
+ CTX --> FS
+ CC --> UI
+ FS --> NoteModel
+ UI --> NoteModel
+ FS --> FileSystem
+ UI --> Console
+ NoteModel --> YAML
+```
+
+## Dependency Flow
+
+```mermaid
+graph TD
+ %% Layer definitions
+ subgraph "Presentation Layer"
+ CLI[CLI Commands]
+ end
+
+ subgraph "Application Layer"
+ APP[App Controller]
+ CMD[Command Pattern]
+ end
+
+ subgraph "Domain Layer"
+ NOTE[Note Domain]
+ end
+
+ subgraph "Infrastructure Layer"
+ STORAGE[File Storage]
+ UI[User Interface]
+ end
+
+ subgraph "External"
+ FILES[File System]
+ CONSOLE[Console]
+ end
+
+ %% Dependencies (top to bottom)
+ CLI --> APP
+ APP --> CMD
+ CMD --> NOTE
+ CMD --> STORAGE
+ CMD --> UI
+ STORAGE --> FILES
+ UI --> CONSOLE
+
+ %% Clean Architecture Rules
+ NOTE -.->|"No dependencies on outer layers"| NOTE
+```
+
+## Command Pattern Implementation
+
+```mermaid
+classDiagram
+ class Command {
+ <>
+ +Execute(args []string) error
+ }
+
+ class CommandContext {
+ +Storage *FileStorage
+ +CurrentListing []*Note
+ +SetCurrentListing(notes []*Note)
+ +GetCurrentListing() []*Note
+ }
+
+ class App {
+ -ctx *CommandContext
+ -commands map[string]Command
+ +NewApp() *App
+ +registerCommands()
+ +Run()
+ }
+
+ class CreateCommand {
+ -ctx *CommandContext
+ +Execute(args []string) error
+ }
+
+ class ListCommand {
+ -ctx *CommandContext
+ +Execute(args []string) error
+ }
+
+ class ReadCommand {
+ -ctx *CommandContext
+ +Execute(args []string) error
+ +resolveNoteID(identifier string) (string, error)
+ }
+
+ %% Relationships
+ Command <|.. CreateCommand
+ Command <|.. ListCommand
+ Command <|.. ReadCommand
+ App --> CommandContext
+ App --> Command
+ CreateCommand --> CommandContext
+ ListCommand --> CommandContext
+ ReadCommand --> CommandContext
+```
+
+## Domain Model
+
+```mermaid
+classDiagram
+ class Note {
+ +Metadata Metadata
+ +Content string
+ +FilePath string
+ +New(title, content string, tags []string) *Note
+ +SetFilePath(path string)
+ +UpdateContent(content string)
+ +UpdateTags(tags []string)
+ +ToFileContent() (string, error)
+ +Save() error
+ }
+
+ class Metadata {
+ +Title string
+ +Created time.Time
+ +Modified time.Time
+ +Tags []string
+ +Author string
+ +Status string
+ +Priority int
+ }
+
+ class FileStorage {
+ -notesDir string
+ -noteExtension string
+ +NewFileStorage() *FileStorage
+ +SaveNote(note *Note) error
+ +GetAllNotes() []*Note, error
+ +FindNoteByID(noteID string) (*Note, error)
+ +DeleteNote(noteID string) error
+ +SearchNotes(query string) ([]*Note, error)
+ +FilterNotesByTag(tag string) ([]*Note, error)
+ }
+
+ Note --> Metadata
+ FileStorage --> Note
+```
+
+## Data Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant CLI
+ participant App
+ participant Command
+ participant Storage
+ participant NoteModel as Note
+ participant FileSystem
+
+ User->>CLI: memo create
+ CLI->>App: Run()
+ App->>App: Parse command line
+ App->>Command: CreateCommand.Execute()
+ Command->>Command: Prompt for input
+ Command->>NoteModel: New(title, content, tags)
+ NoteModel-->>Command: *Note
+ Command->>Storage: SaveNote(note)
+ Storage->>NoteModel: ToFileContent()
+ NoteModel-->>Storage: YAML + content
+ Storage->>FileSystem: WriteFile()
+ FileSystem-->>Storage: success
+ Storage-->>Command: nil
+ Command-->>App: nil
+ App-->>CLI: success
+ CLI-->>User: "Note created successfully"
+```
+
+## Key Architectural Benefits
+
+### 1. **Separation of Concerns**
+- **Domain Logic**: Pure business rules in `internal/note`
+- **Storage Logic**: File operations in `internal/storage`
+- **UI Logic**: User interaction in `internal/ui`
+- **Command Logic**: CLI handling in `cmd`
+
+### 2. **Command Pattern Benefits**
+- **Extensibility**: Easy to add new commands
+- **Testability**: Each command can be unit tested
+- **Maintainability**: Single responsibility per command
+- **Consistency**: All commands follow same interface
+
+### 3. **Clean Architecture Principles**
+- **Dependency Inversion**: Outer layers depend on inner layers
+- **Interface Segregation**: Small, focused interfaces
+- **Single Responsibility**: Each package has one reason to change
+- **Open/Closed**: Open for extension, closed for modification
+
+### 4. **Future-Proofing**
+- **Plugin Architecture**: Commands can be dynamically registered
+- **Storage Abstraction**: Easy to swap file storage for database
+- **UI Abstraction**: Can support web UI or GUI in future
+- **Domain Purity**: Business logic independent of infrastructure
+
+## Package Responsibilities
+
+| Package | Responsibility | Dependencies |
+|---------|---------------|--------------|
+| `main` | Application entry point | `cmd` |
+| `cmd` | CLI command handling & routing | `internal/*` |
+| `internal/note` | Domain models & business logic | Standard library, YAML |
+| `internal/storage` | Data persistence operations | `internal/note` |
+| `internal/ui` | User interface & interaction | `internal/note` |
+
+This architecture makes the codebase beginner-friendly while maintaining professional standards for scalability and maintainability.
\ No newline at end of file
diff --git a/DATA_STRUCTURES_GUIDE.md b/DATA_STRUCTURES_GUIDE.md
new file mode 100644
index 0000000..5fab1e4
--- /dev/null
+++ b/DATA_STRUCTURES_GUIDE.md
@@ -0,0 +1,2481 @@
+# Using Data Structures Guide: Personal Notes Manager
+
+A beginner-friendly guide to understanding Lists and Maps/Dictionaries through the Progressive Enhancement tasks. Learn fundamental data structures by building real functionality in your Personal Notes Manager.
+
+**Target Audience:** Beginning programmers learning data structures and collections
+**Languages:** Java, Python, Go, or similar
+**Focus:** Practical application of Lists, Arrays, Maps, and Dictionaries
+**Companion to:** Progressive Enhancement Guide
+
+## Overview
+
+Data structures are the building blocks of programming - they're how we organize and store information in memory. Think of them as different types of containers for your data, each with their own strengths and best uses.
+
+**Why Learn Data Structures Through This Project?**
+- See real-world applications, not just theory
+- Understand when to use Lists vs Maps vs Sets
+- Learn through building something useful
+- Develop intuition for choosing the right structure
+
+**The Two Main Players:**
+
+**Lists/Arrays** - Ordered collections of items
+- Like a numbered shopping list
+- Items have positions (index 0, 1, 2...)
+- Great for sequences, collections that need ordering
+- Examples: note titles, search results, tags
+
+**Maps/Dictionaries** - Key-value pairs
+- Like a phone book (name → phone number)
+- Look up values using unique keys
+- Great for associations, lookups, counting
+- Examples: tag counts, metadata, user settings
+
+---
+
+## Task 1: Basic Arrays and String Handling
+
+**Data Structures Used:** Arrays, Strings (which are arrays of characters)
+
+**Core Concept:** Every programming language has ways to work with ordered lists of items. Understanding arrays is fundamental to everything else.
+
+### String Arrays in Action
+
+**Why strings are important:** When we read a file, we get one big string. To work with it, we need to break it into smaller pieces.
+
+```pseudocode
+// Reading and processing file content
+method readNoteFromFile(filename):
+ content = readFromFile(filename) // One big string
+ lines = split(content, "\n") // Array of strings!
+
+ // Now we can work with individual lines
+ title = lines[0] // First element
+ separator = lines[1] // Second element
+ noteContent = join(lines[3:], "\n") // Slice and rejoin
+
+ return createNote(title, noteContent)
+```
+
+**What's happening here:**
+1. `split()` converts one string into an array of strings
+2. `lines[0]` accesses the first element (arrays start at 0!)
+3. `lines[3:]` takes a "slice" from index 3 to the end
+4. `join()` converts an array back into one string
+
+### Array Operations You'll Use
+
+**Creating arrays:**
+```pseudocode
+// Empty array
+emptyList = []
+names = ["Alice", "Bob", "Charlie"]
+
+// In different languages:
+// Java: ArrayList names = new ArrayList<>();
+// Python: names = []
+// Go: names := make([]string, 0)
+```
+
+**Adding items:**
+```pseudocode
+// Add to end
+add "David" to names // names = ["Alice", "Bob", "Charlie", "David"]
+
+// Insert at position
+insert "Eve" at position 1 // names = ["Alice", "Eve", "Bob", "Charlie", "David"]
+```
+
+**Accessing items:**
+```pseudocode
+firstPerson = names[0] // "Alice"
+lastPerson = names[length(names) - 1] // "David"
+```
+
+**Finding items:**
+```pseudocode
+bobIndex = find "Bob" in names // Returns 2 (or -1 if not found)
+hasBob = contains(names, "Bob") // Returns true/false
+```
+
+### Practical Example: Building File Content
+
+In Task 1, we create formatted file content by building it piece by piece:
+
+```pseudocode
+method saveNoteToFile(note, filename):
+ // Start with title
+ fileContent = note.title + "\n"
+
+ // Add separator line (array of "=" characters)
+ separatorChars = []
+ for i from 0 to length(note.title):
+ add "=" to separatorChars
+ separator = join(separatorChars, "") // "=========="
+
+ // Build final content
+ fileContent += separator + "\n\n" + note.content
+
+ writeToFile(filename, fileContent)
+```
+
+**Alternative using string repetition:**
+```pseudocode
+separator = repeat("=", length(note.title)) // Much simpler!
+```
+
+### Common Beginner Mistakes
+
+1. **Off-by-one errors:**
+```pseudocode
+// WRONG - this will crash if array has 3 items
+for i from 0 to length(array): // Goes 0,1,2,3 - but index 3 doesn't exist!
+
+// RIGHT
+for i from 0 to length(array) - 1: // Goes 0,1,2
+// OR BETTER
+for each item in array: // Let the language handle indexing
+```
+
+2. **Forgetting arrays start at 0:**
+```pseudocode
+lines = ["title", "====", "", "content"]
+title = lines[1] // WRONG - gets "===="
+title = lines[0] // RIGHT - gets "title"
+```
+
+3. **Modifying arrays while iterating:**
+```pseudocode
+// DANGEROUS - can skip items or crash
+for each item in myList:
+ if shouldRemove(item):
+ remove item from myList // Changes list size while iterating!
+
+// BETTER - iterate backwards or collect items to remove
+itemsToRemove = []
+for each item in myList:
+ if shouldRemove(item):
+ add item to itemsToRemove
+
+for each item in itemsToRemove:
+ remove item from myList
+```
+
+### Test Your Understanding
+
+Try these exercises with your chosen language:
+
+1. **Split and Join Practice:**
+ - Take the string "apple,banana,cherry"
+ - Split it into an array
+ - Remove "banana"
+ - Join it back into "apple,cherry"
+
+2. **File Line Processing:**
+ - Create a text file with 5 lines
+ - Read it and split into lines
+ - Print each line with its line number (starting from 1)
+
+**Success Criteria:**
+- Understand array indexing (starts at 0)
+- Can split strings into arrays and join arrays into strings
+- Comfortable adding, removing, and accessing array items
+- Know how to iterate through arrays safely
+
+---
+
+## Task 2: Arrays for Tags and Metadata Maps
+
+**Data Structures Used:** Arrays (for tags), Maps/Dictionaries (for metadata)
+
+**Core Concept:** Real applications need to store multiple types of related information. Maps let us associate names with values, while arrays hold lists of similar items.
+
+### Arrays for Tags
+
+Tags are a perfect use case for arrays - they're a list of labels, order doesn't matter much, but we need to add, remove, and search through them.
+
+```pseudocode
+class Note:
+ title: string
+ content: string
+ tags: array of strings // This is our tag array!
+ created: datetime
+ modified: datetime
+```
+
+**Working with tag arrays:**
+```pseudocode
+method createNote(title, content, tagString):
+ note = new Note()
+ note.title = title
+ note.content = content
+
+ // Convert comma-separated string to array
+ if empty(tagString):
+ note.tags = [] // Empty array
+ else:
+ rawTags = split(tagString, ",") // ["work ", " meeting", "important "]
+ note.tags = []
+ for each tag in rawTags:
+ cleanTag = trim(tag) // Remove spaces
+ if not empty(cleanTag):
+ add cleanTag to note.tags // Only add non-empty tags
+
+ note.created = getCurrentDateTime()
+ note.modified = note.created
+ return note
+```
+
+**Why clean the tags?** Users might type "work, meeting, important" (with spaces) but we want `["work", "meeting", "important"]` (clean).
+
+### Maps/Dictionaries for Metadata
+
+YAML headers contain key-value pairs - perfect for maps! Instead of having separate variables for each piece of metadata, we can store it all in one structure.
+
+```pseudocode
+// Instead of this:
+title = "My Note"
+created = "2024-01-01T10:00:00Z"
+modified = "2024-01-01T10:30:00Z"
+tags = ["work", "meeting"]
+
+// We can use this:
+metadata = {
+ "title": "My Note",
+ "created": "2024-01-01T10:00:00Z",
+ "modified": "2024-01-01T10:30:00Z",
+ "tags": ["work", "meeting"]
+}
+```
+
+**Map operations:**
+```pseudocode
+// Creating maps
+emptyMap = {}
+userInfo = {"name": "Alice", "age": 25, "city": "New York"}
+
+// Adding/updating values
+userInfo["email"] = "alice@example.com"
+userInfo["age"] = 26 // Updates existing value
+
+// Getting values
+name = userInfo["name"] // "Alice"
+phone = userInfo["phone"] // Might crash if key doesn't exist!
+
+// Safe getting (different in each language)
+phone = userInfo.get("phone", "unknown") // Returns "unknown" if not found
+// OR
+if "phone" in userInfo:
+ phone = userInfo["phone"]
+else:
+ phone = "unknown"
+```
+
+### YAML Parsing with Maps
+
+When we read YAML, we're essentially building a map from the text:
+
+```pseudocode
+method parseYAMLHeader(yamlText):
+ metadata = {} // Empty map
+ lines = split(yamlText, "\n")
+
+ for each line in lines:
+ if contains(line, ":"):
+ parts = split(line, ":", 1) // Split only on first ":"
+ key = trim(parts[0])
+ value = trim(parts[1])
+
+ // Handle special cases
+ if key == "tags":
+ // Convert "[tag1, tag2]" to array
+ cleanValue = remove(value, "[")
+ cleanValue = remove(cleanValue, "]")
+ if empty(cleanValue):
+ metadata[key] = []
+ else:
+ tagArray = split(cleanValue, ",")
+ metadata[key] = map(tagArray, tag => trim(tag))
+ else:
+ metadata[key] = value
+
+ return metadata
+```
+
+### Building YAML from Maps
+
+Going the other direction - from our data structures to YAML text:
+
+```pseudocode
+method buildYAMLHeader(note):
+ yamlLines = ["---"] // Array to collect lines
+
+ // Basic fields
+ add "title: " + note.title to yamlLines
+ add "created: " + formatDateTime(note.created) to yamlLines
+ add "modified: " + formatDateTime(note.modified) to yamlLines
+
+ // Tags array → YAML format
+ if empty(note.tags):
+ add "tags: []" to yamlLines
+ else:
+ tagString = "[" + join(note.tags, ", ") + "]"
+ add "tags: " + tagString to yamlLines
+
+ add "---" to yamlLines
+ return join(yamlLines, "\n")
+```
+
+### Language-Specific Notes
+
+**Python:**
+```python
+# Lists (arrays)
+tags = ["work", "meeting"]
+tags.append("important")
+has_work = "work" in tags
+
+# Dictionaries (maps)
+metadata = {"title": "My Note", "created": "2024-01-01"}
+title = metadata.get("title", "Untitled")
+```
+
+**Java:**
+```java
+// ArrayLists (dynamic arrays)
+List tags = new ArrayList<>();
+tags.add("work");
+boolean hasWork = tags.contains("work");
+
+// HashMaps (maps)
+Map metadata = new HashMap<>();
+metadata.put("title", "My Note");
+String title = metadata.getOrDefault("title", "Untitled");
+```
+
+**Go:**
+```go
+// Slices (dynamic arrays)
+tags := []string{"work", "meeting"}
+tags = append(tags, "important")
+
+// Maps
+metadata := map[string]string{
+ "title": "My Note",
+ "created": "2024-01-01",
+}
+title, exists := metadata["title"]
+```
+
+### Common Patterns
+
+**Checking if array contains item:**
+```pseudocode
+method containsTag(note, tagName):
+ for each tag in note.tags:
+ if tag == tagName:
+ return true
+ return false
+
+// Many languages have built-in contains() functions
+hasWorkTag = contains(note.tags, "work")
+```
+
+**Adding unique items to array:**
+```pseudocode
+method addUniqueTag(note, newTag):
+ if not containsTag(note, newTag):
+ add newTag to note.tags
+```
+
+**Converting between formats:**
+```pseudocode
+// Array to comma-separated string
+tagString = join(note.tags, ", ") // "work, meeting, important"
+
+// Comma-separated string to array
+tagArray = split(tagString, ", ") // ["work", "meeting", "important"]
+```
+
+### Test Your Understanding
+
+1. **Tag Management:**
+ - Create a note with tags "work, meeting, important"
+ - Add a new tag "urgent" (only if not already present)
+ - Remove the "meeting" tag
+ - Convert the tags back to a comma-separated string
+
+2. **YAML Processing:**
+ - Parse this YAML into a map: `title: My Note\ncreated: 2024-01-01\ntags: [work, meeting]`
+ - Extract the title and tags from your map
+ - Build YAML text from a map containing title, date, and tags
+
+**Success Criteria:**
+- Understand when to use arrays vs maps
+- Can parse simple YAML into key-value structures
+- Comfortable converting between arrays and strings
+- Know how to safely access map values that might not exist
+
+---
+
+## Task 3: Collections for Note Management and Filtering
+
+**Data Structures Used:** Arrays of objects, filtered collections, nested data structures
+
+**Core Concept:** Real applications work with collections of complex objects. You need to load, filter, search, and organize multiple notes efficiently.
+
+### Arrays of Note Objects
+
+Now we're moving beyond simple strings and numbers to arrays containing complex objects:
+
+```pseudocode
+// Instead of separate variables for each note:
+note1 = createNote("Meeting Notes", "Discussed project timeline")
+note2 = createNote("Shopping List", "Milk, eggs, bread")
+note3 = createNote("Ideas", "New feature concepts")
+
+// We use an array to manage them all:
+allNotes = [note1, note2, note3] // Array of Note objects
+```
+
+**Why this matters:** With arrays of objects, you can process your entire note collection with loops and filters instead of handling each note individually.
+
+### Loading Multiple Notes
+
+When your app starts, it needs to discover and load all existing notes:
+
+```pseudocode
+method listAllNotes(notesDirectory):
+ files = listFilesInDirectory(notesDirectory) // Array of filenames
+ noteFiles = [] // Will hold filtered results
+
+ // Filter to only note files
+ for each filename in files:
+ if endsWith(filename, ".note") or endsWith(filename, ".txt"):
+ add filename to noteFiles
+
+ loadedNotes = [] // Array of Note objects
+
+ for each filename in noteFiles:
+ try:
+ note = readNoteFromFile(filename)
+ note.filename = filename // Add filename to note object
+ add note to loadedNotes
+ catch error:
+ print "Warning: Could not read " + filename
+ // Continue with other files - don't let one bad file stop everything
+
+ return loadedNotes
+```
+
+**Key concepts:**
+- **Filtering:** Starting with all files, keep only the ones we want
+- **Transformation:** Converting filenames (strings) into Note objects
+- **Error handling:** Bad files don't crash the whole process
+
+### Filtering and Searching Collections
+
+Once you have an array of notes, you can filter it to find specific ones:
+
+```pseudocode
+method searchNotes(allNotes, query):
+ matchingNotes = [] // Empty array for results
+ queryLower = toLowerCase(query)
+
+ for each note in allNotes:
+ noteMatches = false
+
+ // Check title
+ if contains(toLowerCase(note.title), queryLower):
+ noteMatches = true
+
+ // Check content
+ if contains(toLowerCase(note.content), queryLower):
+ noteMatches = true
+
+ // Check tags
+ for each tag in note.tags:
+ if contains(toLowerCase(tag), queryLower):
+ noteMatches = true
+ break // Stop checking other tags
+
+ // If any part matched, add to results
+ if noteMatches:
+ add note to matchingNotes
+
+ return matchingNotes
+```
+
+**Pattern breakdown:**
+1. **Input:** Array of notes + search term
+2. **Process:** Check each note against criteria
+3. **Output:** New array with only matching notes
+
+### Advanced Filtering Patterns
+
+**Filter by multiple criteria:**
+```pseudocode
+method findWorkNotes(allNotes):
+ workNotes = []
+ for each note in allNotes:
+ if containsTag(note, "work"):
+ add note to workNotes
+ return workNotes
+
+method findRecentNotes(allNotes, daysCutoff):
+ cutoffDate = getCurrentDateTime() - daysCutoff
+ recentNotes = []
+
+ for each note in allNotes:
+ if note.modified > cutoffDate:
+ add note to recentNotes
+
+ return recentNotes
+```
+
+**Combining filters:**
+```pseudocode
+method findRecentWorkNotes(allNotes):
+ workNotes = findWorkNotes(allNotes) // First filter
+ recentWorkNotes = findRecentNotes(workNotes, 7) // Second filter
+ return recentWorkNotes
+
+// OR do it in one pass:
+method findRecentWorkNotesEfficient(allNotes):
+ results = []
+ cutoffDate = getCurrentDateTime() - 7
+
+ for each note in allNotes:
+ if containsTag(note, "work") and note.modified > cutoffDate:
+ add note to results
+
+ return results
+```
+
+### Sorting Collections
+
+Users often want notes in a specific order:
+
+```pseudocode
+method sortNotesByTitle(notes):
+ // Most languages have built-in sorting
+ return sort(notes, by title ascending)
+
+method sortNotesByDate(notes):
+ return sort(notes, by created descending) // Newest first
+
+method sortNotesByModified(notes):
+ return sort(notes, by modified descending) // Recently modified first
+```
+
+**Custom sorting:**
+```pseudocode
+method sortNotesByRelevance(notes, searchTerm):
+ // Score each note by how well it matches
+ scoredNotes = []
+
+ for each note in notes:
+ score = 0
+ if contains(note.title, searchTerm):
+ score += 10 // Title matches worth more
+ if contains(note.content, searchTerm):
+ score += 1 // Content matches worth less
+
+ scoredNote = {note: note, score: score}
+ add scoredNote to scoredNotes
+
+ sortedScores = sort(scoredNotes, by score descending)
+
+ // Extract just the notes from the scored objects
+ results = []
+ for each scoredNote in sortedScores:
+ add scoredNote.note to results
+
+ return results
+```
+
+### Working with Nested Data
+
+Notes contain arrays (tags) and you're working with arrays of notes - this is nested data:
+
+```pseudocode
+method getAllUniqueTagsFromNotes(allNotes):
+ uniqueTags = [] // Will collect all tags
+
+ for each note in allNotes: // Outer loop: notes
+ for each tag in note.tags: // Inner loop: tags within note
+ if not contains(uniqueTags, tag): // Check if we've seen this tag
+ add tag to uniqueTags
+
+ return sort(uniqueTags) // Return alphabetical list
+```
+
+**Using Sets for uniqueness (if your language supports them):**
+```pseudocode
+method getAllUniqueTagsWithSet(allNotes):
+ tagSet = empty set // Sets automatically handle uniqueness
+
+ for each note in allNotes:
+ for each tag in note.tags:
+ add tag to tagSet // Set ignores duplicates
+
+ return sort(convertToArray(tagSet))
+```
+
+### Collection Transformation Patterns
+
+**Map pattern:** Transform each item in a collection
+```pseudocode
+method getNoteTitles(allNotes):
+ titles = []
+ for each note in allNotes:
+ add note.title to titles
+ return titles
+
+// Many languages have built-in map functions:
+titles = map(allNotes, note => note.title)
+```
+
+**Reduce pattern:** Combine all items into a single value
+```pseudocode
+method getTotalWordCount(allNotes):
+ totalWords = 0
+ for each note in allNotes:
+ noteWords = countWords(note.content)
+ totalWords += noteWords
+ return totalWords
+```
+
+**Group by pattern:** Organize items into categories
+```pseudocode
+method groupNotesByFirstTag(allNotes):
+ groups = {} // Map from tag to notes
+
+ for each note in allNotes:
+ if empty(note.tags):
+ groupKey = "untagged"
+ else:
+ groupKey = note.tags[0] // Use first tag
+
+ if groupKey not in groups:
+ groups[groupKey] = [] // Create new group
+
+ add note to groups[groupKey]
+
+ return groups
+```
+
+### Language-Specific Collection Operations
+
+**Python:**
+```python
+# List comprehensions for filtering
+work_notes = [note for note in all_notes if "work" in note.tags]
+
+# Built-in functions
+titles = [note.title for note in all_notes]
+total_notes = len(all_notes)
+sorted_notes = sorted(all_notes, key=lambda n: n.created)
+```
+
+**Java:**
+```java
+// Stream API for filtering and transforming
+List workNotes = allNotes.stream()
+ .filter(note -> note.getTags().contains("work"))
+ .collect(Collectors.toList());
+
+// Method references
+List titles = allNotes.stream()
+ .map(Note::getTitle)
+ .collect(Collectors.toList());
+```
+
+### Performance Considerations
+
+**For small collections (< 1000 notes):** Simple loops are fine and easy to understand.
+
+**For larger collections:** Consider these optimizations:
+- **Index frequently searched fields** (keep a map of tag → notes)
+- **Lazy loading** (only read note content when needed)
+- **Pagination** (show results in chunks)
+
+### Test Your Understanding
+
+1. **Collection Filtering:**
+ - Create an array of 5 notes with different tags
+ - Find all notes containing the tag "important"
+ - Find all notes created in the last 7 days
+ - Combine both filters to find recent important notes
+
+2. **Data Transformation:**
+ - Extract all unique tags from your note collection
+ - Create a list of just the note titles
+ - Group notes by their first tag
+
+3. **Sorting Practice:**
+ - Sort notes by title alphabetically
+ - Sort notes by creation date (newest first)
+ - Sort notes by number of tags (most tagged first)
+
+**Success Criteria:**
+- Comfortable working with arrays of complex objects
+- Understand filtering patterns and when to use them
+- Can combine multiple filters and sorts
+- Know how to extract and transform data from collections
+
+---
+
+## Task 4: List Operations for CRUD Functionality
+
+**Data Structures Used:** Dynamic arrays, list modification, index management
+
+**Core Concept:** CRUD (Create, Read, Update, Delete) operations require you to modify collections safely. You'll learn to add, find, modify, and remove items from lists while maintaining data integrity.
+
+### The CRUD Challenge with Collections
+
+When working with collections of notes, you need to:
+- **Create:** Add new notes to your collection
+- **Read:** Find and display specific notes
+- **Update:** Modify existing notes in place
+- **Delete:** Remove notes and clean up references
+
+The tricky part: When you modify a collection, you need to keep everything synchronized and handle edge cases.
+
+### Finding Items in Collections
+
+Before you can update or delete, you need to find the right item:
+
+```pseudocode
+method findNoteById(allNotes, noteId):
+ for i from 0 to length(allNotes) - 1:
+ if allNotes[i].id == noteId:
+ return {note: allNotes[i], index: i} // Return both note and position
+ return null // Not found
+
+method findNoteByTitle(allNotes, title):
+ matches = []
+ for i from 0 to length(allNotes) - 1:
+ if contains(toLowerCase(allNotes[i].title), toLowerCase(title)):
+ add {note: allNotes[i], index: i} to matches
+ return matches
+
+method findNotesByQuery(allNotes, searchQuery):
+ results = []
+ for each note in allNotes:
+ if matchesSearchCriteria(note, searchQuery):
+ add note to results
+ return results
+```
+
+**Why return both note and index?** When you need to update or delete, you need to know WHERE in the array the item lives.
+
+### Interactive Note Selection
+
+Real applications need users to choose from multiple options:
+
+```pseudocode
+method selectNoteInteractively(allNotes):
+ if empty(allNotes):
+ print "No notes available"
+ return null
+
+ // Display options with numbers
+ print "Available notes:"
+ for i from 0 to length(allNotes) - 1:
+ note = allNotes[i]
+ print (i + 1) + ". " + note.title // Show 1-based numbers to user
+ print " Created: " + formatDateTime(note.created)
+
+ print "Enter note number (1-" + length(allNotes) + "): "
+ userInput = readUserInput()
+
+ // Convert user input to array index
+ try:
+ noteNumber = parseInt(userInput)
+ arrayIndex = noteNumber - 1 // Convert 1-based to 0-based
+
+ if arrayIndex < 0 or arrayIndex >= length(allNotes):
+ print "Invalid selection"
+ return null
+
+ return {note: allNotes[arrayIndex], index: arrayIndex}
+ catch error:
+ print "Please enter a valid number"
+ return null
+```
+
+**Key pattern:** Always show 1-based numbers to users (1, 2, 3...) but use 0-based indexing internally (0, 1, 2...).
+
+### Safe List Modification
+
+**Adding items (Create):**
+```pseudocode
+method addNote(allNotes, newNote):
+ // Generate unique ID if needed
+ newNote.id = generateUniqueId(allNotes)
+
+ // Add to end of list
+ add newNote to allNotes
+
+ // Keep track of addition
+ print "Added note: " + newNote.title + " (ID: " + newNote.id + ")"
+
+ return allNotes
+
+method generateUniqueId(existingNotes):
+ // Simple approach: find highest ID and add 1
+ maxId = 0
+ for each note in existingNotes:
+ if note.id > maxId:
+ maxId = note.id
+ return maxId + 1
+
+ // Alternative: use timestamp or UUID
+ return getCurrentTimestamp()
+```
+
+**Updating items (Update):**
+```pseudocode
+method updateNote(allNotes, noteIndex, updatedNote):
+ if noteIndex < 0 or noteIndex >= length(allNotes):
+ print "Error: Invalid note index"
+ return false
+
+ // Preserve important fields
+ originalNote = allNotes[noteIndex]
+ updatedNote.id = originalNote.id // Keep same ID
+ updatedNote.created = originalNote.created // Keep creation date
+ updatedNote.modified = getCurrentDateTime() // Update modification time
+
+ // Replace the note in the array
+ allNotes[noteIndex] = updatedNote
+
+ print "Updated note: " + updatedNote.title
+ return true
+
+method editNoteInPlace(allNotes, noteIndex):
+ if noteIndex < 0 or noteIndex >= length(allNotes):
+ return false
+
+ note = allNotes[noteIndex]
+
+ // Edit title
+ print "Current title: " + note.title
+ print "New title (or Enter to keep current): "
+ newTitle = readUserInput()
+ if not empty(trim(newTitle)):
+ note.title = newTitle
+
+ // Edit tags
+ print "Current tags: " + join(note.tags, ", ")
+ print "New tags (comma-separated, or Enter to keep current): "
+ tagInput = readUserInput()
+ if not empty(trim(tagInput)):
+ note.tags = split(tagInput, ",")
+ note.tags = map(note.tags, tag => trim(tag)) // Clean whitespace
+
+ // Update modification time
+ note.modified = getCurrentDateTime()
+
+ // Note is already in the array, so changes are automatically saved
+ return true
+```
+
+**Removing items (Delete):**
+```pseudocode
+method deleteNote(allNotes, noteIndex):
+ if noteIndex < 0 or noteIndex >= length(allNotes):
+ print "Error: Invalid note index"
+ return false
+
+ noteToDelete = allNotes[noteIndex]
+
+ // Confirm deletion
+ print "Delete '" + noteToDelete.title + "'? (y/N): "
+ confirmation = toLowerCase(readUserInput())
+
+ if confirmation != "y":
+ print "Deletion cancelled"
+ return false
+
+ // Create backup before deletion
+ backupNote(noteToDelete)
+
+ // Remove from array
+ remove allNotes[noteIndex] from allNotes
+
+ print "Deleted note: " + noteToDelete.title
+ return true
+
+// Alternative: mark as deleted instead of removing
+method softDeleteNote(allNotes, noteIndex):
+ if noteIndex < 0 or noteIndex >= length(allNotes):
+ return false
+
+ allNotes[noteIndex].deleted = true
+ allNotes[noteIndex].deletedAt = getCurrentDateTime()
+
+ // When listing notes, filter out deleted ones
+ return true
+```
+
+### Batch Operations
+
+Sometimes you need to work with multiple items at once:
+
+```pseudocode
+method deleteMultipleNotes(allNotes, noteIndices):
+ // Sort indices in descending order to avoid index shifting issues
+ sortedIndices = sort(noteIndices, descending)
+
+ deletedCount = 0
+ for each index in sortedIndices:
+ if deleteNote(allNotes, index):
+ deletedCount += 1
+
+ print "Deleted " + deletedCount + " notes"
+ return deletedCount
+
+method addTagToMultipleNotes(allNotes, noteIndices, newTag):
+ for each index in noteIndices:
+ if index >= 0 and index < length(allNotes):
+ note = allNotes[index]
+ if not contains(note.tags, newTag):
+ add newTag to note.tags
+ note.modified = getCurrentDateTime()
+```
+
+**Why sort indices in descending order?** When you remove item at index 2, all items at indices 3, 4, 5... shift down. By removing from the end first, you don't mess up the other indices.
+
+### List Synchronization Patterns
+
+Keep your in-memory list synchronized with files:
+
+```pseudocode
+method syncNoteToFile(note):
+ filename = generateFilename(note)
+ saveNoteToFile(note, filename)
+ note.filename = filename // Remember where it's saved
+
+method syncAllNotesToFiles(allNotes):
+ for each note in allNotes:
+ syncNoteToFile(note)
+
+method reloadNotesFromFiles(notesDirectory):
+ // Completely refresh from disk
+ return listAllNotes(notesDirectory)
+
+method addNoteWithFileSync(allNotes, newNote):
+ // Add to memory
+ add newNote to allNotes
+
+ // Save to disk
+ syncNoteToFile(newNote)
+
+ return newNote
+```
+
+### Handling Concurrent Modifications
+
+What if the file changes while your program is running?
+
+```pseudocode
+method safeUpdateNote(allNotes, noteIndex, updatedFields):
+ note = allNotes[noteIndex]
+
+ // Check if file was modified since we loaded it
+ if fileModificationTime(note.filename) > note.loadedAt:
+ print "Warning: File was modified externally"
+ print "Reload the note? (y/N): "
+
+ if toLowerCase(readUserInput()) == "y":
+ reloadedNote = readNoteFromFile(note.filename)
+ allNotes[noteIndex] = reloadedNote
+ note = reloadedNote
+
+ // Apply updates
+ for each field, value in updatedFields:
+ note[field] = value
+
+ note.modified = getCurrentDateTime()
+ syncNoteToFile(note)
+```
+
+### Error Recovery Patterns
+
+Always plan for things to go wrong:
+
+```pseudocode
+method safeDeleteNote(allNotes, noteIndex):
+ note = allNotes[noteIndex]
+
+ try:
+ // Create backup first
+ backupFilename = note.filename + ".backup"
+ copyFile(note.filename, backupFilename)
+
+ // Remove from memory
+ remove note from allNotes
+
+ // Delete file
+ deleteFile(note.filename)
+
+ print "Deleted successfully. Backup saved as: " + backupFilename
+ return true
+
+ catch error:
+ print "Error during deletion: " + error.message
+
+ // If we removed from memory but file deletion failed
+ if note not in allNotes:
+ add note back to allNotes // Restore to memory
+
+ return false
+```
+
+### Common List Modification Pitfalls
+
+1. **Index shifting during iteration:**
+```pseudocode
+// WRONG - indices change as you remove items
+for i from 0 to length(notes) - 1:
+ if shouldDelete(notes[i]):
+ remove notes[i] // Shifts all subsequent items!
+
+// RIGHT - iterate backwards
+for i from length(notes) - 1 down to 0:
+ if shouldDelete(notes[i]):
+ remove notes[i]
+```
+
+2. **Forgetting to update timestamps:**
+```pseudocode
+// WRONG - modified time doesn't reflect the change
+note.title = "New Title"
+saveNoteToFile(note, filename)
+
+// RIGHT - always update timestamps on changes
+note.title = "New Title"
+note.modified = getCurrentDateTime()
+saveNoteToFile(note, filename)
+```
+
+3. **Not validating indices:**
+```pseudocode
+// WRONG - will crash if index is invalid
+selectedNote = notes[userSelectedIndex]
+
+// RIGHT - always check bounds
+if userSelectedIndex >= 0 and userSelectedIndex < length(notes):
+ selectedNote = notes[userSelectedIndex]
+else:
+ print "Invalid selection"
+```
+
+### Test Your Understanding
+
+1. **Interactive Selection:**
+ - Create a list of 5 notes
+ - Display them with numbers (1-5)
+ - Let user select one by number
+ - Handle invalid selections gracefully
+
+2. **Safe Modification:**
+ - Add a new note to your collection
+ - Update an existing note's title and tags
+ - Delete a note with confirmation
+ - Verify all operations update timestamps correctly
+
+3. **Batch Operations:**
+ - Find all notes with a specific tag
+ - Add a new tag to all of them
+ - Remove notes older than a certain date
+
+**Success Criteria:**
+- Can safely add, update, and delete items in collections
+- Understand index management and bounds checking
+- Know how to handle user selection from lists
+- Can synchronize in-memory changes with persistent storage
+
+---
+
+## Task 5: Complex Data Structures for Advanced Search
+
+**Data Structures Used:** Nested maps, multi-level filtering, search indices, query objects
+
+**Core Concept:** Advanced search requires combining multiple data structures to efficiently filter and rank results. You'll build query parsers, search indices, and complex filtering systems.
+
+### Structured Query Objects
+
+Instead of simple string searches, advanced search uses structured queries:
+
+```pseudocode
+class SearchQuery:
+ textQuery: string // "meeting notes"
+ requiredTags: array of strings // ["work", "important"]
+ excludedTags: array of strings // ["draft", "archived"]
+ dateFrom: datetime // Only notes after this date
+ dateTo: datetime // Only notes before this date
+ titleOnly: boolean // Search only in titles
+ hasTag: boolean // Only notes that have any tags
+ wordCount: {min: int, max: int} // Word count range
+```
+
+**Why use objects instead of strings?** Objects let you represent complex search criteria that would be hard to express in a single string.
+
+### Query Parsing with Maps
+
+Convert user input into structured queries:
+
+```pseudocode
+method parseAdvancedQuery(queryString):
+ query = new SearchQuery()
+ query.requiredTags = []
+ query.excludedTags = []
+
+ // Split query into parts
+ parts = split(queryString, " ")
+ textParts = []
+
+ for each part in parts:
+ if startsWith(part, "tag:"):
+ tagName = substring(part, 4) // Remove "tag:" prefix
+ add tagName to query.requiredTags
+
+ else if startsWith(part, "-tag:"):
+ tagName = substring(part, 5) // Remove "-tag:" prefix
+ add tagName to query.excludedTags
+
+ else if startsWith(part, "after:"):
+ dateString = substring(part, 6) // Remove "after:" prefix
+ query.dateFrom = parseDate(dateString)
+
+ else if startsWith(part, "before:"):
+ dateString = substring(part, 7) // Remove "before:" prefix
+ query.dateTo = parseDate(dateString)
+
+ else if part == "title-only":
+ query.titleOnly = true
+
+ else if part == "has-tags":
+ query.hasTag = true
+
+ else if startsWith(part, "words:"):
+ // Parse "words:10-50" or "words:10+"
+ rangeString = substring(part, 6)
+ query.wordCount = parseWordCountRange(rangeString)
+
+ else:
+ // Regular search term
+ add part to textParts
+
+ query.textQuery = join(textParts, " ")
+ return query
+
+method parseWordCountRange(rangeString):
+ if contains(rangeString, "-"):
+ // Range like "10-50"
+ parts = split(rangeString, "-")
+ return {min: parseInt(parts[0]), max: parseInt(parts[1])}
+ else if endsWith(rangeString, "+"):
+ // Minimum like "10+"
+ minValue = parseInt(substring(rangeString, 0, length(rangeString) - 1))
+ return {min: minValue, max: 999999}
+ else:
+ // Exact count like "10"
+ exactValue = parseInt(rangeString)
+ return {min: exactValue, max: exactValue}
+```
+
+### Multi-Criteria Filtering
+
+Apply all search criteria to find matching notes:
+
+```pseudocode
+method executeAdvancedSearch(allNotes, searchQuery):
+ matchingNotes = []
+
+ for each note in allNotes:
+ if matchesAllCriteria(note, searchQuery):
+ add note to matchingNotes
+
+ return matchingNotes
+
+method matchesAllCriteria(note, query):
+ // Text search
+ if not empty(query.textQuery):
+ if not matchesTextQuery(note, query):
+ return false
+
+ // Required tags - ALL must be present
+ for each requiredTag in query.requiredTags:
+ if not contains(note.tags, requiredTag):
+ return false
+
+ // Excluded tags - NONE can be present
+ for each excludedTag in query.excludedTags:
+ if contains(note.tags, excludedTag):
+ return false
+
+ // Date range
+ if query.dateFrom and note.created < query.dateFrom:
+ return false
+ if query.dateTo and note.created > query.dateTo:
+ return false
+
+ // Has tags requirement
+ if query.hasTag and empty(note.tags):
+ return false
+
+ // Word count range
+ if query.wordCount:
+ wordCount = countWords(note.content)
+ if wordCount < query.wordCount.min or wordCount > query.wordCount.max:
+ return false
+
+ return true // Passed all criteria
+
+method matchesTextQuery(note, query):
+ queryLower = toLowerCase(query.textQuery)
+
+ if query.titleOnly:
+ return contains(toLowerCase(note.title), queryLower)
+ else:
+ return contains(toLowerCase(note.title), queryLower) or
+ contains(toLowerCase(note.content), queryLower)
+```
+
+### Search Indices for Performance
+
+For large collections, build indices to speed up searches:
+
+```pseudocode
+class SearchIndex:
+ tagToNotes: map from string to array of notes // "work" -> [note1, note3]
+ wordToNotes: map from string to array of notes // "meeting" -> [note2, note5]
+ dateToNotes: map from date to array of notes // "2024-01" -> [note1, note4]
+
+method buildSearchIndex(allNotes):
+ index = new SearchIndex()
+ index.tagToNotes = {}
+ index.wordToNotes = {}
+ index.dateToNotes = {}
+
+ for each note in allNotes:
+ // Index by tags
+ for each tag in note.tags:
+ if tag not in index.tagToNotes:
+ index.tagToNotes[tag] = []
+ add note to index.tagToNotes[tag]
+
+ // Index by words in title and content
+ allWords = extractWords(note.title) + extractWords(note.content)
+ for each word in allWords:
+ wordLower = toLowerCase(word)
+ if wordLower not in index.wordToNotes:
+ index.wordToNotes[wordLower] = []
+ if note not in index.wordToNotes[wordLower]: // Avoid duplicates
+ add note to index.wordToNotes[wordLower]
+
+ // Index by month
+ monthKey = formatDate(note.created, "YYYY-MM")
+ if monthKey not in index.dateToNotes:
+ index.dateToNotes[monthKey] = []
+ add note to index.dateToNotes[monthKey]
+
+ return index
+
+method extractWords(text):
+ // Split on whitespace and punctuation, remove empty strings
+ rawWords = split(text, /\s+|[.,!?;:]/)
+ words = []
+ for each word in rawWords:
+ cleanWord = trim(word)
+ if not empty(cleanWord) and length(cleanWord) > 2: // Skip very short words
+ add cleanWord to words
+ return words
+```
+
+### Using Indices for Fast Search
+
+```pseudocode
+method fastSearchWithIndex(searchIndex, query):
+ candidateNotes = []
+
+ // Start with tag filtering (usually most selective)
+ if not empty(query.requiredTags):
+ firstTag = query.requiredTags[0]
+ if firstTag in searchIndex.tagToNotes:
+ candidateNotes = searchIndex.tagToNotes[firstTag]
+
+ // Intersect with other required tags
+ for i from 1 to length(query.requiredTags) - 1:
+ tag = query.requiredTags[i]
+ if tag in searchIndex.tagToNotes:
+ candidateNotes = intersection(candidateNotes, searchIndex.tagToNotes[tag])
+ else:
+ return [] // Tag doesn't exist, no results
+ else:
+ return [] // First tag doesn't exist
+
+ // If no tag filtering, start with text search
+ else if not empty(query.textQuery):
+ words = split(query.textQuery, " ")
+ if length(words) > 0:
+ firstWord = toLowerCase(words[0])
+ if firstWord in searchIndex.wordToNotes:
+ candidateNotes = searchIndex.wordToNotes[firstWord]
+
+ // Intersect with other words
+ for i from 1 to length(words) - 1:
+ word = toLowerCase(words[i])
+ if word in searchIndex.wordToNotes:
+ candidateNotes = intersection(candidateNotes, searchIndex.wordToNotes[word])
+ else:
+ return []
+
+ // If no initial filtering, use all notes
+ else:
+ candidateNotes = getAllNotesFromIndex(searchIndex)
+
+ // Apply remaining filters
+ results = []
+ for each note in candidateNotes:
+ if matchesAllCriteria(note, query):
+ add note to results
+
+ return results
+
+method intersection(array1, array2):
+ // Return items that appear in both arrays
+ result = []
+ for each item in array1:
+ if contains(array2, item):
+ add item to result
+ return result
+```
+
+### Complex Result Ranking
+
+Rank search results by relevance:
+
+```pseudocode
+class SearchResult:
+ note: Note
+ score: int
+ matchReasons: array of strings // ["title match", "tag match"]
+
+method rankSearchResults(notes, query):
+ scoredResults = []
+
+ for each note in notes:
+ result = new SearchResult()
+ result.note = note
+ result.score = 0
+ result.matchReasons = []
+
+ // Score different types of matches
+ if not empty(query.textQuery):
+ titleMatch = contains(toLowerCase(note.title), toLowerCase(query.textQuery))
+ if titleMatch:
+ result.score += 10
+ add "title match" to result.matchReasons
+
+ contentMatch = contains(toLowerCase(note.content), toLowerCase(query.textQuery))
+ if contentMatch:
+ result.score += 5
+ add "content match" to result.matchReasons
+
+ // Boost for tag matches
+ for each requiredTag in query.requiredTags:
+ if contains(note.tags, requiredTag):
+ result.score += 3
+ add "tag: " + requiredTag to result.matchReasons
+
+ // Boost for recent notes
+ daysSinceCreated = (getCurrentDateTime() - note.created) / (24 * 60 * 60)
+ if daysSinceCreated < 7:
+ result.score += 2
+ add "recent" to result.matchReasons
+
+ // Boost for recently modified notes
+ daysSinceModified = (getCurrentDateTime() - note.modified) / (24 * 60 * 60)
+ if daysSinceModified < 3:
+ result.score += 1
+ add "recently modified" to result.matchReasons
+
+ add result to scoredResults
+
+ // Sort by score (highest first)
+ sortedResults = sort(scoredResults, by score descending)
+ return sortedResults
+```
+
+### Aggregated Search Results
+
+Provide insights about search results:
+
+```pseudocode
+class SearchStats:
+ totalResults: int
+ tagDistribution: map from string to int // "work" -> 5
+ dateDistribution: map from string to int // "2024-01" -> 3
+ averageWordCount: float
+ matchReasons: map from string to int // "title match" -> 8
+
+method analyzeSearchResults(searchResults):
+ stats = new SearchStats()
+ stats.totalResults = length(searchResults)
+ stats.tagDistribution = {}
+ stats.dateDistribution = {}
+ stats.matchReasons = {}
+
+ totalWords = 0
+
+ for each result in searchResults:
+ note = result.note
+
+ // Count tags
+ for each tag in note.tags:
+ if tag not in stats.tagDistribution:
+ stats.tagDistribution[tag] = 0
+ stats.tagDistribution[tag] += 1
+
+ // Count by month
+ monthKey = formatDate(note.created, "YYYY-MM")
+ if monthKey not in stats.dateDistribution:
+ stats.dateDistribution[monthKey] = 0
+ stats.dateDistribution[monthKey] += 1
+
+ // Count match reasons
+ for each reason in result.matchReasons:
+ if reason not in stats.matchReasons:
+ stats.matchReasons[reason] = 0
+ stats.matchReasons[reason] += 1
+
+ // Word count
+ totalWords += countWords(note.content)
+
+ if stats.totalResults > 0:
+ stats.averageWordCount = totalWords / stats.totalResults
+
+ return stats
+```
+
+### Query Optimization Patterns
+
+**Optimize query order:**
+```pseudocode
+method optimizeQuery(query):
+ // Reorder criteria from most to least selective
+
+ // Tags are usually most selective
+ if not empty(query.requiredTags):
+ query.priority = 1
+ // Date ranges can be very selective
+ else if query.dateFrom or query.dateTo:
+ query.priority = 2
+ // Text queries are moderately selective
+ else if not empty(query.textQuery):
+ query.priority = 3
+ // Word count ranges are least selective
+ else:
+ query.priority = 4
+
+ return query
+```
+
+**Cache frequent queries:**
+```pseudocode
+queryCache = {} // Global cache
+
+method cachedSearch(allNotes, query):
+ queryKey = serializeQuery(query) // Convert to string key
+
+ if queryKey in queryCache:
+ cachedResult = queryCache[queryKey]
+ // Check if cache is still valid
+ if cachedResult.timestamp > lastNoteModification:
+ return cachedResult.results
+
+ // Perform search
+ results = executeAdvancedSearch(allNotes, query)
+
+ // Cache results
+ queryCache[queryKey] = {
+ results: results,
+ timestamp: getCurrentDateTime()
+ }
+
+ return results
+```
+
+### Test Your Understanding
+
+1. **Query Parsing:**
+ - Parse "meeting tag:work -tag:draft after:2024-01-01"
+ - Parse "title-only has-tags words:10-50"
+ - Handle invalid date formats and malformed queries
+
+2. **Multi-Criteria Search:**
+ - Create notes with various tags, dates, and word counts
+ - Search for notes that match multiple criteria
+ - Verify exclusion filters work correctly
+
+3. **Search Indexing:**
+ - Build a search index for your note collection
+ - Compare search speed with and without indices
+ - Test index accuracy with various queries
+
+**Success Criteria:**
+- Can parse complex search queries into structured data
+- Understand how to combine multiple filtering criteria
+- Know when and how to use search indices for performance
+- Can rank and analyze search results meaningfully
+
+---
+
+## Task 6: Maps for Statistics and Aggregation
+
+**Data Structures Used:** Hash maps for counting, nested maps for grouping, sorted maps for rankings
+
+**Core Concept:** Statistics require aggregating data across your entire collection. Maps are perfect for counting, grouping, and analyzing patterns in your notes. You'll learn to use maps as counters, accumulators, and organizational tools.
+
+### Maps as Counters
+
+The most common statistical operation is counting occurrences:
+
+```pseudocode
+method countTagFrequency(allNotes):
+ tagCounts = {} // Map: tag name -> count
+
+ for each note in allNotes:
+ for each tag in note.tags:
+ if tag in tagCounts:
+ tagCounts[tag] += 1 // Increment existing count
+ else:
+ tagCounts[tag] = 1 // Initialize count
+
+ return tagCounts
+
+// Example result: {"work": 15, "personal": 8, "ideas": 12}
+```
+
+**Pattern explanation:** This is the classic "counting pattern" - maps make it easy to keep track of how many times you've seen each unique item.
+
+### Aggregating Numeric Data
+
+Beyond counting, you can sum, average, and analyze numeric values:
+
+```pseudocode
+method calculateWordCountStats(allNotes):
+ stats = {
+ "totalWords": 0,
+ "noteCount": 0,
+ "wordCountByTag": {}, // tag -> total words in that tag's notes
+ "averageWordCount": 0,
+ "minWordCount": 999999,
+ "maxWordCount": 0
+ }
+
+ for each note in allNotes:
+ wordCount = countWords(note.content)
+
+ // Overall statistics
+ stats["totalWords"] += wordCount
+ stats["noteCount"] += 1
+
+ if wordCount < stats["minWordCount"]:
+ stats["minWordCount"] = wordCount
+ if wordCount > stats["maxWordCount"]:
+ stats["maxWordCount"] = wordCount
+
+ // Per-tag statistics
+ for each tag in note.tags:
+ if tag not in stats["wordCountByTag"]:
+ stats["wordCountByTag"][tag] = 0
+ stats["wordCountByTag"][tag] += wordCount
+
+ // Calculate average
+ if stats["noteCount"] > 0:
+ stats["averageWordCount"] = stats["totalWords"] / stats["noteCount"]
+
+ return stats
+```
+
+### Nested Maps for Complex Grouping
+
+Group data by multiple dimensions using nested maps:
+
+```pseudocode
+method groupNotesByTagAndMonth(allNotes):
+ // Structure: tag -> month -> array of notes
+ groupedNotes = {}
+
+ for each note in allNotes:
+ monthKey = formatDate(note.created, "YYYY-MM")
+
+ // Handle notes with no tags
+ tagsToProcess = note.tags
+ if empty(tagsToProcess):
+ tagsToProcess = ["untagged"]
+
+ for each tag in tagsToProcess:
+ // Ensure tag exists in outer map
+ if tag not in groupedNotes:
+ groupedNotes[tag] = {}
+
+ // Ensure month exists in inner map
+ if monthKey not in groupedNotes[tag]:
+ groupedNotes[tag][monthKey] = []
+
+ // Add note to the appropriate group
+ add note to groupedNotes[tag][monthKey]
+
+ return groupedNotes
+
+// Example result:
+// {
+// "work": {
+// "2024-01": [note1, note3],
+// "2024-02": [note5, note7]
+// },
+// "personal": {
+// "2024-01": [note2],
+// "2024-02": [note4, note6]
+// }
+// }
+```
+
+### Time-Based Analysis with Maps
+
+Track activity patterns over time:
+
+```pseudocode
+method analyzeActivityPatterns(allNotes):
+ patterns = {
+ "monthlyActivity": {}, // "2024-01" -> count
+ "weeklyActivity": {}, // "Monday" -> count
+ "hourlyActivity": {}, // "14" -> count (2 PM)
+ "creationVsModification": {} // "created" -> count, "modified" -> count
+ }
+
+ for each note in allNotes:
+ // Monthly pattern
+ monthKey = formatDate(note.created, "YYYY-MM")
+ incrementMapCount(patterns["monthlyActivity"], monthKey)
+
+ // Weekly pattern
+ dayOfWeek = formatDate(note.created, "dddd") // "Monday", "Tuesday", etc.
+ incrementMapCount(patterns["weeklyActivity"], dayOfWeek)
+
+ // Hourly pattern
+ hour = formatDate(note.created, "HH") // "09", "14", "23", etc.
+ incrementMapCount(patterns["hourlyActivity"], hour)
+
+ // Creation vs modification activity
+ incrementMapCount(patterns["creationVsModification"], "created")
+
+ // Count modifications (if note was modified after creation)
+ if note.modified > note.created:
+ incrementMapCount(patterns["creationVsModification"], "modified")
+
+ return patterns
+
+method incrementMapCount(map, key):
+ if key in map:
+ map[key] += 1
+ else:
+ map[key] = 1
+```
+
+### Advanced Statistical Maps
+
+**Calculating distributions and percentiles:**
+```pseudocode
+method calculateTagDistribution(tagCounts):
+ totalNotes = sum(values(tagCounts))
+ distribution = {}
+
+ for each tag, count in tagCounts:
+ percentage = (count * 100.0) / totalNotes
+ distribution[tag] = {
+ "count": count,
+ "percentage": round(percentage, 1),
+ "rank": 0 // Will calculate below
+ }
+
+ // Assign ranks based on count
+ sortedTags = sortByCount(tagCounts, descending)
+ for i, tag in enumerate(sortedTags):
+ distribution[tag]["rank"] = i + 1
+
+ return distribution
+
+method findTopItems(countMap, limit):
+ // Convert map to array of {item, count} objects
+ items = []
+ for each key, count in countMap:
+ add {item: key, count: count} to items
+
+ // Sort by count (highest first)
+ sortedItems = sort(items, by count descending)
+
+ // Return top N items
+ return take(sortedItems, limit)
+```
+
+### Correlation Analysis with Maps
+
+Find relationships between different attributes:
+
+```pseudocode
+method analyzeTagCorrelations(allNotes):
+ // Find which tags commonly appear together
+ tagPairs = {} // "tag1,tag2" -> count
+
+ for each note in allNotes:
+ // Generate all pairs of tags in this note
+ for i from 0 to length(note.tags) - 1:
+ for j from i + 1 to length(note.tags) - 1:
+ tag1 = note.tags[i]
+ tag2 = note.tags[j]
+
+ // Create consistent key (alphabetical order)
+ pairKey = tag1 < tag2 ? tag1 + "," + tag2 : tag2 + "," + tag1
+
+ incrementMapCount(tagPairs, pairKey)
+
+ // Convert to correlation scores
+ correlations = {}
+ for each pairKey, count in tagPairs:
+ if count >= 2: // Only include pairs that occur multiple times
+ tags = split(pairKey, ",")
+ correlations[pairKey] = {
+ "tag1": tags[0],
+ "tag2": tags[1],
+ "coOccurrences": count,
+ "strength": calculateCorrelationStrength(tags[0], tags[1], allNotes)
+ }
+
+ return correlations
+
+method calculateCorrelationStrength(tag1, tag2, allNotes):
+ tag1Count = countNotesWithTag(allNotes, tag1)
+ tag2Count = countNotesWithTag(allNotes, tag2)
+ bothTagsCount = countNotesWithBothTags(allNotes, tag1, tag2)
+
+ // Jaccard similarity: intersection / union
+ union = tag1Count + tag2Count - bothTagsCount
+ if union == 0:
+ return 0
+
+ return (bothTagsCount * 1.0) / union
+```
+
+### Map-Based Reporting
+
+Generate readable reports from statistical maps:
+
+```pseudocode
+method generateStatisticsReport(allNotes):
+ // Collect all statistics
+ tagCounts = countTagFrequency(allNotes)
+ wordStats = calculateWordCountStats(allNotes)
+ patterns = analyzeActivityPatterns(allNotes)
+
+ report = []
+
+ // Overview section
+ add "=== NOTES STATISTICS ===" to report
+ add "" to report
+ add "Total Notes: " + length(allNotes) to report
+ add "Total Words: " + wordStats["totalWords"] to report
+ add "Average Words per Note: " + round(wordStats["averageWordCount"], 1) to report
+ add "" to report
+
+ // Top tags section
+ add "Most Used Tags:" to report
+ topTags = findTopItems(tagCounts, 10)
+ for i, tagData in enumerate(topTags):
+ rank = i + 1
+ tag = tagData.item
+ count = tagData.count
+ percentage = (count * 100.0) / length(allNotes)
+ add " " + rank + ". " + tag + " (" + count + " notes, " + round(percentage, 1) + "%)" to report
+ add "" to report
+
+ // Activity patterns
+ add "Monthly Activity:" to report
+ sortedMonths = sort(keys(patterns["monthlyActivity"]))
+ for each month in sortedMonths:
+ count = patterns["monthlyActivity"][month]
+ bar = repeat("*", min(count, 50)) // Simple text bar chart
+ add " " + month + ": " + count + " notes " + bar to report
+ add "" to report
+
+ return join(report, "\n")
+
+method generateTagReport(tagCounts):
+ report = []
+ add "=== TAG ANALYSIS ===" to report
+ add "" to report
+
+ distribution = calculateTagDistribution(tagCounts)
+ sortedTags = sort(keys(distribution), by distribution[tag]["rank"])
+
+ for each tag in sortedTags:
+ info = distribution[tag]
+ add info["rank"] + ". " + tag to report
+ add " Count: " + info["count"] to report
+ add " Percentage: " + info["percentage"] + "%" to report
+ add "" to report
+
+ return join(report, "\n")
+```
+
+### Memory-Efficient Statistics for Large Collections
+
+For very large note collections, use streaming statistics:
+
+```pseudocode
+class StreamingStats:
+ count: int = 0
+ sum: float = 0
+ sumOfSquares: float = 0
+ min: float = infinity
+ max: float = negative_infinity
+
+method addValue(stats, value):
+ stats.count += 1
+ stats.sum += value
+ stats.sumOfSquares += value * value
+
+ if value < stats.min:
+ stats.min = value
+ if value > stats.max:
+ stats.max = value
+
+method calculateFinalStats(stats):
+ if stats.count == 0:
+ return null
+
+ mean = stats.sum / stats.count
+ variance = (stats.sumOfSquares / stats.count) - (mean * mean)
+ standardDeviation = sqrt(variance)
+
+ return {
+ "count": stats.count,
+ "mean": mean,
+ "min": stats.min,
+ "max": stats.max,
+ "standardDeviation": standardDeviation
+ }
+
+method calculateWordCountStatsStreaming(allNotes):
+ wordCountStats = new StreamingStats()
+
+ for each note in allNotes:
+ wordCount = countWords(note.content)
+ addValue(wordCountStats, wordCount)
+
+ return calculateFinalStats(wordCountStats)
+```
+
+### Common Statistical Patterns
+
+**Top-K pattern (find most/least common items):**
+```pseudocode
+method findTopK(countMap, k):
+ return findTopItems(countMap, k)
+
+method findBottomK(countMap, k):
+ items = convertMapToArray(countMap)
+ sortedItems = sort(items, by count ascending) // Lowest first
+ return take(sortedItems, k)
+```
+
+**Histogram pattern (group by ranges):**
+```pseudocode
+method createWordCountHistogram(allNotes):
+ histogram = {
+ "0-10": 0,
+ "11-50": 0,
+ "51-100": 0,
+ "101-500": 0,
+ "500+": 0
+ }
+
+ for each note in allNotes:
+ wordCount = countWords(note.content)
+
+ if wordCount <= 10:
+ histogram["0-10"] += 1
+ else if wordCount <= 50:
+ histogram["11-50"] += 1
+ else if wordCount <= 100:
+ histogram["51-100"] += 1
+ else if wordCount <= 500:
+ histogram["101-500"] += 1
+ else:
+ histogram["500+"] += 1
+
+ return histogram
+```
+
+### Test Your Understanding
+
+1. **Basic Counting:**
+ - Count how many notes have each tag
+ - Find the most and least used tags
+ - Calculate what percentage of notes have no tags
+
+2. **Aggregation:**
+ - Calculate total word count across all notes
+ - Find average, minimum, and maximum note length
+ - Group notes by creation month and count each group
+
+3. **Advanced Analysis:**
+ - Find which tags commonly appear together
+ - Analyze activity patterns by day of week
+ - Create a histogram of note lengths
+
+**Success Criteria:**
+- Comfortable using maps for counting and aggregation
+- Understand nested maps for multi-dimensional grouping
+- Can generate meaningful statistics and reports
+- Know efficient patterns for analyzing large datasets
+
+---
+
+## Task 7: Data Structure Patterns for CLI Commands
+
+**Data Structures Used:** Command maps, argument arrays, nested configuration structures, state management
+
+**Core Concept:** A professional CLI requires organized data structures to parse commands, manage arguments, store configuration, and maintain application state. You'll learn to build flexible, extensible command systems using maps and arrays.
+
+### Command Registry with Maps
+
+Organize commands using a map-based registry:
+
+```pseudocode
+class Command:
+ name: string
+ description: string
+ handler: function
+ arguments: array of ArgumentSpec
+ examples: array of strings
+
+class ArgumentSpec:
+ name: string
+ required: boolean
+ description: string
+ type: string // "string", "int", "boolean", "array"
+ defaultValue: any
+
+// Global command registry
+commandRegistry = {} // Map: command name -> Command object
+
+method registerCommand(command):
+ commandRegistry[command.name] = command
+
+method setupAllCommands():
+ // Create command
+ createCommand = new Command()
+ createCommand.name = "create"
+ createCommand.description = "Create a new note interactively"
+ createCommand.handler = handleCreateCommand
+ createCommand.arguments = [] // No arguments needed
+ createCommand.examples = ["notes create"]
+ registerCommand(createCommand)
+
+ // List command
+ listCommand = new Command()
+ listCommand.name = "list"
+ listCommand.description = "List all notes or search with filters"
+ listCommand.handler = handleListCommand
+ listCommand.arguments = [
+ new ArgumentSpec("query", false, "Search query with filters", "string", "")
+ ]
+ listCommand.examples = [
+ "notes list",
+ "notes list meeting",
+ "notes list tag:work after:2024-01-01"
+ ]
+ registerCommand(listCommand)
+
+ // Edit command
+ editCommand = new Command()
+ editCommand.name = "edit"
+ editCommand.description = "Edit a note by search term"
+ editCommand.handler = handleEditCommand
+ editCommand.arguments = [
+ new ArgumentSpec("query", true, "Search term to find note", "string", null)
+ ]
+ editCommand.examples = [
+ "notes edit meeting",
+ "notes edit tag:work"
+ ]
+ registerCommand(editCommand)
+
+ // And so on for other commands...
+```
+
+### Argument Parsing with Arrays and Maps
+
+Parse command-line arguments into structured data:
+
+```pseudocode
+class ParsedCommand:
+ commandName: string
+ arguments: map from string to any // argument name -> value
+ flags: array of strings // boolean flags like "--verbose"
+ errors: array of strings // validation errors
+
+method parseCommandLine(args):
+ if empty(args):
+ return createHelpCommand()
+
+ parsed = new ParsedCommand()
+ parsed.commandName = args[0]
+ parsed.arguments = {}
+ parsed.flags = []
+ parsed.errors = []
+
+ // Check if command exists
+ if parsed.commandName not in commandRegistry:
+ add "Unknown command: " + parsed.commandName to parsed.errors
+ return parsed
+
+ command = commandRegistry[parsed.commandName]
+ remainingArgs = args[1:] // Skip command name
+
+ // Parse flags (start with --)
+ cleanArgs = []
+ for each arg in remainingArgs:
+ if startsWith(arg, "--"):
+ flagName = substring(arg, 2) // Remove "--"
+ add flagName to parsed.flags
+ else:
+ add arg to cleanArgs
+
+ // Parse positional arguments
+ for i from 0 to length(command.arguments) - 1:
+ argSpec = command.arguments[i]
+
+ if i < length(cleanArgs):
+ // Argument provided
+ value = cleanArgs[i]
+ parsed.arguments[argSpec.name] = convertArgument(value, argSpec.type)
+ else:
+ // Argument not provided
+ if argSpec.required:
+ add "Missing required argument: " + argSpec.name to parsed.errors
+ else:
+ parsed.arguments[argSpec.name] = argSpec.defaultValue
+
+ return parsed
+
+method convertArgument(value, type):
+ if type == "int":
+ return parseInt(value)
+ else if type == "boolean":
+ return toLowerCase(value) in ["true", "1", "yes", "on"]
+ else if type == "array":
+ return split(value, ",")
+ else:
+ return value // Default to string
+```
+
+### Configuration Management with Nested Maps
+
+Store and manage application configuration:
+
+```pseudocode
+class AppConfig:
+ notesDirectory: string
+ defaultEditor: string
+ dateFormat: string
+ maxSearchResults: int
+ backupEnabled: boolean
+ colors: map // color theme settings
+ aliases: map // command aliases
+
+// Global configuration
+appConfig = null
+
+method loadConfiguration():
+ config = new AppConfig()
+
+ // Default values
+ config.notesDirectory = "./notes"
+ config.defaultEditor = "nano"
+ config.dateFormat = "YYYY-MM-DD HH:mm"
+ config.maxSearchResults = 50
+ config.backupEnabled = true
+ config.colors = {
+ "title": "blue",
+ "date": "gray",
+ "tag": "green",
+ "warning": "yellow",
+ "error": "red"
+ }
+ config.aliases = {
+ "ls": "list",
+ "new": "create",
+ "rm": "delete"
+ }
+
+ // Try to load from config file
+ if fileExists("config.json"):
+ try:
+ configData = readJSONFile("config.json")
+ mergeConfiguration(config, configData)
+ catch error:
+ print "Warning: Could not load config.json: " + error.message
+
+ return config
+
+method mergeConfiguration(baseConfig, newConfig):
+ // Merge new configuration into base configuration
+ for each key, value in newConfig:
+ if key in baseConfig:
+ if isMap(baseConfig[key]) and isMap(value):
+ // Recursively merge nested maps
+ mergeConfiguration(baseConfig[key], value)
+ else:
+ baseConfig[key] = value
+```
+
+### State Management for Interactive Commands
+
+Maintain application state across command executions:
+
+```pseudocode
+class AppState:
+ currentNotes: array of notes // Currently loaded notes
+ lastSearchQuery: string // Remember last search
+ lastSearchResults: array of notes // Cache search results
+ selectedNoteIndex: int // For interactive selection
+ isDirty: boolean // Whether notes need saving
+ statistics: map // Cached statistics
+
+// Global application state
+appState = null
+
+method initializeAppState():
+ state = new AppState()
+ state.currentNotes = []
+ state.lastSearchQuery = ""
+ state.lastSearchResults = []
+ state.selectedNoteIndex = -1
+ state.isDirty = false
+ state.statistics = {}
+
+ return state
+
+method refreshNotesInState():
+ appState.currentNotes = listAllNotes(appConfig.notesDirectory)
+ appState.isDirty = false
+
+ // Invalidate cached data
+ appState.statistics = {}
+
+ print "Loaded " + length(appState.currentNotes) + " notes"
+
+method markStateDirty():
+ appState.isDirty = true
+ appState.statistics = {} // Invalidate statistics cache
+```
+
+### Command Handler Patterns
+
+Implement consistent command handlers:
+
+```pseudocode
+method handleListCommand(parsedCommand):
+ // Ensure notes are loaded
+ if empty(appState.currentNotes) or appState.isDirty:
+ refreshNotesInState()
+
+ query = parsedCommand.arguments["query"]
+
+ if empty(query):
+ // List all notes
+ displayNoteList(appState.currentNotes)
+ appState.lastSearchResults = appState.currentNotes
+ else:
+ // Search notes
+ searchQuery = parseAdvancedQuery(query)
+ results = executeAdvancedSearch(appState.currentNotes, searchQuery)
+
+ displaySearchResults(results, query)
+
+ // Cache results for potential follow-up commands
+ appState.lastSearchQuery = query
+ appState.lastSearchResults = results
+
+method handleEditCommand(parsedCommand):
+ query = parsedCommand.arguments["query"]
+
+ // Use cached results if query matches last search
+ if query == appState.lastSearchQuery and not empty(appState.lastSearchResults):
+ candidates = appState.lastSearchResults
+ else:
+ searchQuery = parseAdvancedQuery(query)
+ candidates = executeAdvancedSearch(appState.currentNotes, searchQuery)
+
+ if empty(candidates):
+ print "No notes found matching: " + query
+ return
+
+ if length(candidates) == 1:
+ // Exactly one match - edit it
+ editNote(candidates[0])
+ markStateDirty()
+ else:
+ // Multiple matches - let user choose
+ selectedNote = selectFromMultiple(candidates)
+ if selectedNote != null:
+ editNote(selectedNote)
+ markStateDirty()
+
+method selectFromMultiple(notes):
+ print "Multiple notes found:"
+ displayNoteList(notes)
+
+ print "Enter note number (1-" + length(notes) + ") or 'cancel': "
+ userInput = readUserInput()
+
+ if toLowerCase(userInput) == "cancel":
+ return null
+
+ try:
+ noteNumber = parseInt(userInput)
+ index = noteNumber - 1
+
+ if index >= 0 and index < length(notes):
+ return notes[index]
+ else:
+ print "Invalid selection"
+ return null
+ catch error:
+ print "Please enter a valid number"
+ return null
+```
+
+### Command Aliases and Shortcuts
+
+Support command aliases using maps:
+
+```pseudocode
+method resolveCommandAlias(commandName):
+ if commandName in appConfig.aliases:
+ return appConfig.aliases[commandName]
+ else:
+ return commandName
+
+method executeCommand(args):
+ if empty(args):
+ displayHelp()
+ return
+
+ // Resolve aliases
+ originalCommand = args[0]
+ resolvedCommand = resolveCommandAlias(originalCommand)
+
+ if resolvedCommand != originalCommand:
+ print "Using alias: " + originalCommand + " -> " + resolvedCommand
+ args[0] = resolvedCommand
+
+ // Parse and execute
+ parsed = parseCommandLine(args)
+
+ if not empty(parsed.errors):
+ for each error in parsed.errors:
+ print "Error: " + error
+ return
+
+ if parsed.commandName in commandRegistry:
+ command = commandRegistry[parsed.commandName]
+ command.handler(parsed)
+ else:
+ print "Unknown command: " + parsed.commandName
+ displayHelp()
+```
+
+### Help System with Structured Data
+
+Generate help using command metadata:
+
+```pseudocode
+method displayHelp(specificCommand):
+ if specificCommand != null:
+ displayCommandHelp(specificCommand)
+ return
+
+ print "Personal Notes Manager"
+ print ""
+ print "Usage: notes [arguments] [flags]"
+ print ""
+ print "Commands:"
+
+ // Sort commands alphabetically
+ sortedCommands = sort(keys(commandRegistry))
+
+ for each commandName in sortedCommands:
+ command = commandRegistry[commandName]
+ print " " + padRight(commandName, 12) + command.description
+
+ print ""
+ print "Global Flags:"
+ print " --help Show help information"
+ print " --verbose Show detailed output"
+ print " --config Use specific config file"
+ print ""
+ print "Examples:"
+ print " notes create # Create a new note"
+ print " notes list # List all notes"
+ print " notes list tag:work # Find work-related notes"
+ print " notes edit meeting # Edit note containing 'meeting'"
+ print ""
+ print "Use 'notes --help' for detailed command help"
+
+method displayCommandHelp(commandName):
+ if commandName not in commandRegistry:
+ print "Unknown command: " + commandName
+ return
+
+ command = commandRegistry[commandName]
+
+ print "Command: " + command.name
+ print "Description: " + command.description
+ print ""
+
+ if not empty(command.arguments):
+ print "Arguments:"
+ for each arg in command.arguments:
+ required = arg.required ? " (required)" : " (optional)"
+ print " " + arg.name + required
+ print " " + arg.description
+ if not arg.required and arg.defaultValue != null:
+ print " Default: " + arg.defaultValue
+ print ""
+
+ if not empty(command.examples):
+ print "Examples:"
+ for each example in command.examples:
+ print " " + example
+ print ""
+```
+
+### Error Handling and Validation
+
+Centralized error handling for all commands:
+
+```pseudocode
+method safeExecuteCommand(args):
+ try:
+ executeCommand(args)
+ catch ValidationError as error:
+ print "Validation Error: " + error.message
+ suggestCorrection(error)
+ catch FileError as error:
+ print "File Error: " + error.message
+ suggestFileRecovery(error)
+ catch GenericError as error:
+ print "Error: " + error.message
+ if contains(appState.flags, "verbose"):
+ print "Stack trace: " + error.stackTrace
+
+method suggestCorrection(error):
+ if error.type == "INVALID_COMMAND":
+ similar = findSimilarCommands(error.attemptedCommand)
+ if not empty(similar):
+ print "Did you mean: " + join(similar, ", ") + "?"
+ else if error.type == "MISSING_ARGUMENT":
+ print "Use '" + error.commandName + " --help' for usage information"
+
+method findSimilarCommands(attempted):
+ similar = []
+ for each commandName in keys(commandRegistry):
+ if editDistance(attempted, commandName) <= 2:
+ add commandName to similar
+ return similar
+```
+
+### Test Your Understanding
+
+1. **Command Registry:**
+ - Register 5 different commands with various argument types
+ - Test argument parsing with required and optional parameters
+ - Implement command aliases for common operations
+
+2. **State Management:**
+ - Maintain note collection state across commands
+ - Cache search results for performance
+ - Track when data needs to be refreshed
+
+3. **Help System:**
+ - Generate help text from command metadata
+ - Show command-specific help with examples
+ - Handle unknown commands with suggestions
+
+**Success Criteria:**
+- Understand how to build extensible command systems with maps
+- Can parse and validate command-line arguments reliably
+- Know how to maintain application state across commands
+- Can generate helpful documentation from structured data
+
+---
+
+## Conclusion
+
+**Congratulations!** You've learned how to use fundamental data structures through building a real application. Here's what you've mastered:
+
+### Key Data Structure Concepts
+
+**Arrays/Lists:**
+- Ordered collections for sequences and groups
+- Essential operations: add, remove, search, filter, sort
+- Patterns: iteration, transformation, aggregation
+- Best for: note collections, search results, tags
+
+**Maps/Dictionaries:**
+- Key-value associations for quick lookups
+- Essential operations: get, set, contains, iterate
+- Patterns: counting, grouping, indexing, caching
+- Best for: metadata, statistics, command registries, configuration
+
+### Practical Patterns You've Learned
+
+1. **Filtering Collections** - Finding subsets that match criteria
+2. **Transformation** - Converting data from one form to another
+3. **Aggregation** - Combining data to calculate statistics
+4. **Indexing** - Building lookup structures for fast search
+5. **State Management** - Maintaining data consistency across operations
+6. **Command Parsing** - Structured handling of user input
+
+### Language Transfer
+
+The patterns you've learned work across programming languages:
+
+**Python:** Lists and dictionaries are built-in and powerful
+**Java:** ArrayList/HashMap provide dynamic resizing
+**Go:** Slices and maps offer efficient implementations
+**JavaScript:** Arrays and objects are fundamental building blocks
+
+### Next Steps
+
+With solid data structure fundamentals, you're ready for:
+- **Advanced algorithms** (searching, sorting, graph traversal)
+- **Database design** (understanding how SQL tables relate to your structures)
+- **Web development** (JSON APIs map directly to your map/array knowledge)
+- **System design** (scaling these patterns to handle millions of records)
+
+**Most importantly:** You've learned to think in terms of organizing and manipulating data - a skill that transfers to every programming challenge you'll face.
+
+The Personal Notes Manager you've built isn't just an application - it's a foundation for understanding how software manages information. Every complex system, from social media platforms to financial software, uses these same fundamental patterns at its core.
+
+Keep building, keep learning, and remember: mastering data structures is mastering the art of organized thinking in code.
\ No newline at end of file
diff --git a/MEMO.md b/MEMO.md
new file mode 100644
index 0000000..382b1d3
--- /dev/null
+++ b/MEMO.md
@@ -0,0 +1,10 @@
+# Memo - a future-proof Personal Notes Manager
+
+This is "readme" for Kris' Go version of the future-proof Notes app.
+
+Written in Go, as an example.
+
+A multi-phase educational project for managing personal notes with structured metadata.
+
+In my Go veresion, I've created a `mermaid` document with diagrams. https://github.com/kristofer/memo/blob/main/ARCHITECTURE.md
+
diff --git a/PROGRESSIVE_ENHANCEMENT_GUIDE.md b/PROGRESSIVE_ENHANCEMENT_GUIDE.md
new file mode 100644
index 0000000..793dea9
--- /dev/null
+++ b/PROGRESSIVE_ENHANCEMENT_GUIDE.md
@@ -0,0 +1,825 @@
+# Progressive Enhancement Guide: Personal Notes Manager
+
+A step-by-step guide for building a Personal Notes Manager that starts simple and grows sophisticated. Each task adds meaningful functionality while maintaining a solid foundation for future enhancements.
+
+**Target Audience:** Beginning programmers learning file I/O, data structures, and application architecture
+**Languages:** Java, Python, Go, or similar
+**Storage:** Plain text files with YAML headers (future-proof!)
+**Phase 1** This is just focused on what you need to do to thru Phase 1 in the README.md file
+
+## Overview
+
+This guide breaks down the Personal Notes Manager into 7 progressive tasks. Each task builds upon the previous one, starting with the absolute basics and gradually adding more sophisticated features. By the end, you'll have a fully functional CLI notes application with robust searching, editing, and management capabilities.
+
+**Why Progressive Enhancement?**
+- Start with working code from day one
+- Learn one concept at a time without overwhelm
+- Build confidence through incremental success
+- Create a solid foundation for future GUI and web versions
+
+---
+
+## Task 1: Basic Note Creation and Reading
+
+**Goal:** Create and read simple text files
+
+**What You'll Learn:**
+- File I/O operations
+- Basic text file handling
+- Simple data structures
+
+**Implementation Steps:**
+
+1. **Create a Note data structure**
+```pseudocode
+class Note:
+ title: string
+ content: string
+
+method createNote(title, content):
+ note = new Note()
+ note.title = title
+ note.content = content
+ return note
+```
+
+2. **Save note to file**
+```pseudocode
+method saveNoteToFile(note, filename):
+ fileContent = note.title + "\n" + "=" * length(note.title) + "\n\n" + note.content
+ writeToFile(filename, fileContent)
+```
+
+3. **Read note from file**
+```pseudocode
+method readNoteFromFile(filename):
+ content = readFromFile(filename)
+ lines = split(content, "\n")
+ title = lines[0]
+ noteContent = join(lines[3:], "\n") // Skip title and separator
+ return createNote(title, noteContent)
+```
+
+**Test Your Implementation:**
+- Create a note with title "My First Note" and content "Hello World"
+- Save it to "note1.txt"
+- Read it back and verify title and content match
+
+**Success Criteria:**
+- Can create notes with title and content
+- Can save notes to uniquely named files
+- Can read notes back from files
+- Basic error handling for missing files
+
+---
+
+## Task 2: Add YAML Header Support
+
+**Goal:** Structure notes with metadata using YAML headers
+
+**What You'll Learn:**
+- YAML parsing and generation
+- Structured data handling
+- Timestamp management
+
+**Enhanced Note Structure:**
+```pseudocode
+class Note:
+ title: string
+ content: string
+ created: datetime
+ modified: datetime
+ tags: array of strings
+
+method createNote(title, content, tags):
+ note = new Note()
+ note.title = title
+ note.content = content
+ note.created = getCurrentDateTime()
+ note.modified = note.created
+ note.tags = tags or empty array
+ return note
+```
+
+**File Format Implementation:**
+```pseudocode
+method saveNoteToFile(note, filename):
+ yamlHeader = "---\n"
+ yamlHeader += "title: " + note.title + "\n"
+ yamlHeader += "created: " + formatDateTime(note.created) + "\n"
+ yamlHeader += "modified: " + formatDateTime(note.modified) + "\n"
+ yamlHeader += "tags: [" + join(note.tags, ", ") + "]\n"
+ yamlHeader += "---\n\n"
+
+ fileContent = yamlHeader + note.content
+ writeToFile(filename, fileContent)
+```
+
+**YAML Parsing:**
+```pseudocode
+method readNoteFromFile(filename):
+ content = readFromFile(filename)
+
+ if not startsWith(content, "---"):
+ // Handle legacy format from Task 1
+ return readLegacyNote(content)
+
+ parts = split(content, "---", 2)
+ yamlSection = parts[1]
+ noteContent = parts[2].trim()
+
+ // Parse YAML (use library or simple parsing)
+ metadata = parseYAML(yamlSection)
+
+ note = new Note()
+ note.title = metadata["title"]
+ note.created = parseDateTime(metadata["created"])
+ note.modified = parseDateTime(metadata["modified"])
+ note.tags = metadata["tags"] or empty array
+ note.content = noteContent
+
+ return note
+```
+
+**Test Your Implementation:**
+- Create a note with tags ["example", "test"]
+- Verify YAML header is properly formatted
+- Read the note back and confirm all metadata is preserved
+- Test backward compatibility with Task 1 files
+
+**Success Criteria:**
+- Notes include proper YAML headers with metadata
+- Can parse both new format and old format files
+- Timestamps are in ISO 8601 format
+- Tags are properly stored and retrieved
+
+---
+
+## Task 3: Note Listing and Basic Search
+
+**Goal:** Manage multiple notes and find them quickly
+
+**What You'll Learn:**
+- Directory traversal
+- File filtering
+- Basic search algorithms
+- Data collection and sorting
+
+**Directory Management:**
+```pseudocode
+method listAllNotes(notesDirectory):
+ files = listFilesInDirectory(notesDirectory)
+ noteFiles = filter(files, file => endsWith(file, ".note") or endsWith(file, ".txt"))
+
+ notes = empty array
+ for each file in noteFiles:
+ try:
+ note = readNoteFromFile(file)
+ note.filename = file
+ add note to notes
+ catch error:
+ print "Warning: Could not read " + file
+
+ return notes
+```
+
+**Basic Search Implementation:**
+```pseudocode
+method searchNotes(notes, query):
+ results = empty array
+ queryLower = toLowerCase(query)
+
+ for each note in notes:
+ // Search in title
+ if contains(toLowerCase(note.title), queryLower):
+ add note to results
+ continue
+
+ // Search in content
+ if contains(toLowerCase(note.content), queryLower):
+ add note to results
+ continue
+
+ // Search in tags
+ for each tag in note.tags:
+ if contains(toLowerCase(tag), queryLower):
+ add note to results
+ break
+
+ return results
+```
+
+**Display Functions:**
+```pseudocode
+method displayNoteList(notes):
+ print "Found " + length(notes) + " notes:\n"
+
+ for i, note in enumerate(notes):
+ print (i+1) + ". " + note.title
+ print " Created: " + formatDateTime(note.created)
+ if not empty(note.tags):
+ print " Tags: " + join(note.tags, ", ")
+ print ""
+
+method displayNote(note):
+ print "Title: " + note.title
+ print "Created: " + formatDateTime(note.created)
+ print "Modified: " + formatDateTime(note.modified)
+ if not empty(note.tags):
+ print "Tags: " + join(note.tags, ", ")
+ print "\n" + note.content
+```
+
+**Test Your Implementation:**
+- Create 5 notes with different titles, content, and tags
+- List all notes and verify they appear correctly
+- Search for a word that appears in titles
+- Search for a word that appears in content
+- Search for a tag name
+
+**Success Criteria:**
+- Can discover and load all notes from a directory
+- Search finds notes by title, content, or tags
+- Gracefully handles corrupted or unreadable files
+- Results are displayed in a clear, readable format
+
+---
+
+## Task 4: Note Editing and Deletion
+
+**Goal:** Complete CRUD operations for note management
+
+**What You'll Learn:**
+- File modification strategies
+- Data validation
+- User input handling
+- Backup and recovery concepts
+
+**Note Selection:**
+```pseudocode
+method selectNoteInteractively(notes):
+ displayNoteList(notes)
+ print "Enter note number (1-" + length(notes) + "): "
+
+ userInput = readUserInput()
+ noteIndex = parseInt(userInput) - 1
+
+ if noteIndex < 0 or noteIndex >= length(notes):
+ print "Invalid selection"
+ return null
+
+ return notes[noteIndex]
+```
+
+**Edit Implementation:**
+```pseudocode
+method editNote(note):
+ print "Current title: " + note.title
+ print "New title (or press Enter to keep current): "
+ newTitle = readUserInput()
+ if not empty(newTitle):
+ note.title = newTitle
+
+ print "Current content:"
+ print note.content
+ print "\nEnter new content (type 'END' on a line by itself to finish):"
+
+ newContent = ""
+ while true:
+ line = readUserInput()
+ if line == "END":
+ break
+ newContent += line + "\n"
+
+ if not empty(newContent.trim()):
+ note.content = newContent.trim()
+
+ // Update tags
+ print "Current tags: " + join(note.tags, ", ")
+ print "New tags (comma-separated, or Enter to keep current): "
+ tagInput = readUserInput()
+ if not empty(tagInput):
+ note.tags = split(tagInput, ",")
+ note.tags = map(note.tags, tag => tag.trim())
+
+ note.modified = getCurrentDateTime()
+ return note
+```
+
+**Safe Deletion:**
+```pseudocode
+method deleteNote(note):
+ print "Are you sure you want to delete '" + note.title + "'? (y/N): "
+ confirmation = readUserInput()
+
+ if toLowerCase(confirmation) != "y":
+ print "Deletion cancelled"
+ return false
+
+ // Create backup before deletion
+ backupFilename = note.filename + ".backup." + formatDateTime(getCurrentDateTime())
+ copyFile(note.filename, backupFilename)
+
+ deleteFile(note.filename)
+ print "Note deleted. Backup saved as: " + backupFilename
+ return true
+```
+
+**Test Your Implementation:**
+- Edit a note's title and verify the file is updated
+- Edit a note's content using your input method
+- Add and modify tags on existing notes
+- Delete a note and confirm backup is created
+- Verify modified timestamps are updated correctly
+
+**Success Criteria:**
+- Can modify existing notes safely
+- Changes are persisted to disk immediately
+- Deletion requires confirmation and creates backups
+- Modified timestamps are updated appropriately
+- User interface is intuitive and clear
+
+---
+
+## Task 5: Advanced Search and Filtering
+
+**Goal:** Powerful search with multiple criteria and filters
+
+**What You'll Learn:**
+- Complex query parsing
+- Multiple search criteria
+- Date range operations
+- Advanced filtering techniques
+
+**Enhanced Search Structure:**
+```pseudocode
+class SearchQuery:
+ textQuery: string
+ requiredTags: array of strings
+ excludedTags: array of strings
+ dateFrom: datetime
+ dateTo: datetime
+ titleOnly: boolean
+```
+
+**Query Parser:**
+```pseudocode
+method parseSearchQuery(queryString):
+ query = new SearchQuery()
+ parts = split(queryString, " ")
+
+ textParts = empty array
+
+ for each part in parts:
+ if startsWith(part, "tag:"):
+ tagName = substring(part, 4)
+ add tagName to query.requiredTags
+ else if startsWith(part, "-tag:"):
+ tagName = substring(part, 5)
+ add tagName to query.excludedTags
+ else if startsWith(part, "after:"):
+ dateStr = substring(part, 6)
+ query.dateFrom = parseDate(dateStr)
+ else if startsWith(part, "before:"):
+ dateStr = substring(part, 7)
+ query.dateTo = parseDate(dateStr)
+ else if part == "title:":
+ query.titleOnly = true
+ else:
+ add part to textParts
+
+ query.textQuery = join(textParts, " ")
+ return query
+```
+
+**Advanced Search Implementation:**
+```pseudocode
+method advancedSearch(notes, searchQuery):
+ results = empty array
+
+ for each note in notes:
+ if not matchesQuery(note, searchQuery):
+ continue
+ add note to results
+
+ return results
+
+method matchesQuery(note, query):
+ // Text search
+ if not empty(query.textQuery):
+ queryLower = toLowerCase(query.textQuery)
+ found = false
+
+ if query.titleOnly:
+ found = contains(toLowerCase(note.title), queryLower)
+ else:
+ found = contains(toLowerCase(note.title), queryLower) or
+ contains(toLowerCase(note.content), queryLower)
+
+ if not found:
+ return false
+
+ // Required tags
+ for each requiredTag in query.requiredTags:
+ if not contains(note.tags, requiredTag):
+ return false
+
+ // Excluded tags
+ for each excludedTag in query.excludedTags:
+ if contains(note.tags, excludedTag):
+ return false
+
+ // Date range
+ if query.dateFrom and note.created < query.dateFrom:
+ return false
+ if query.dateTo and note.created > query.dateTo:
+ return false
+
+ return true
+```
+
+**Tag Management:**
+```pseudocode
+method getAllTags(notes):
+ allTags = empty set
+ for each note in notes:
+ for each tag in note.tags:
+ add tag to allTags
+ return sort(allTags)
+
+method getNotesWithTag(notes, tagName):
+ results = empty array
+ for each note in notes:
+ if contains(note.tags, tagName):
+ add note to results
+ return results
+```
+
+**Test Your Implementation:**
+- Search with `tag:example` to find notes with specific tags
+- Search with `-tag:draft` to exclude draft notes
+- Search with date ranges using `after:2024-01-01`
+- Combine multiple criteria: `meeting tag:work after:2024-01-01`
+- List all available tags
+
+**Success Criteria:**
+- Complex queries work as expected
+- Can filter by tags (include and exclude)
+- Date range filtering functions correctly
+- Multiple search criteria can be combined
+- Search results are accurate and complete
+
+---
+
+## Task 6: Statistics and Reporting
+
+**Goal:** Analyze your note collection with useful metrics
+
+**What You'll Learn:**
+- Data aggregation
+- Statistical calculations
+- Report generation
+- Data visualization (text-based)
+
+**Statistics Collection:**
+```pseudocode
+method generateStatistics(notes):
+ stats = new Statistics()
+
+ stats.totalNotes = length(notes)
+ stats.totalWords = 0
+ stats.tagCounts = empty map
+ stats.monthlyBreakdown = empty map
+
+ for each note in notes:
+ // Word counting
+ wordCount = countWords(note.content)
+ stats.totalWords += wordCount
+
+ // Tag frequency
+ for each tag in note.tags:
+ if tag in stats.tagCounts:
+ stats.tagCounts[tag] += 1
+ else:
+ stats.tagCounts[tag] = 1
+
+ // Monthly breakdown
+ monthKey = formatDate(note.created, "YYYY-MM")
+ if monthKey in stats.monthlyBreakdown:
+ stats.monthlyBreakdown[monthKey] += 1
+ else:
+ stats.monthlyBreakdown[monthKey] = 1
+
+ stats.averageWordsPerNote = stats.totalWords / stats.totalNotes
+ stats.mostUsedTags = getSortedTags(stats.tagCounts, 10)
+
+ return stats
+
+method countWords(text):
+ words = split(text.trim(), whitespace)
+ return length(filter(words, word => not empty(word)))
+```
+
+**Report Generation:**
+```pseudocode
+method displayStatistics(stats):
+ print "=== NOTES STATISTICS ==="
+ print ""
+
+ print "Overall:"
+ print " Total Notes: " + stats.totalNotes
+ print " Total Words: " + stats.totalWords
+ print " Average Words per Note: " + round(stats.averageWordsPerNote, 1)
+ print ""
+
+ print "Top Tags:"
+ for i, tagData in enumerate(stats.mostUsedTags):
+ if i >= 10: break
+ tag = tagData.tag
+ count = tagData.count
+ print " " + (i+1) + ". " + tag + " (" + count + " notes)"
+ print ""
+
+ print "Monthly Activity:"
+ sortedMonths = sort(stats.monthlyBreakdown.keys())
+ for month in sortedMonths:
+ count = stats.monthlyBreakdown[month]
+ bar = "*" * min(count, 50) // Simple text bar chart
+ print " " + month + ": " + count + " notes " + bar
+```
+
+**Advanced Analysis:**
+```pseudocode
+method findOrphanedNotes(notes):
+ // Notes with no tags
+ orphans = empty array
+ for each note in notes:
+ if empty(note.tags):
+ add note to orphans
+ return orphans
+
+method findOldestNotes(notes, count):
+ sortedNotes = sort(notes, by created ascending)
+ return take(sortedNotes, count)
+
+method findRecentActivity(notes, days):
+ cutoffDate = getCurrentDateTime() - days
+ recentNotes = empty array
+
+ for each note in notes:
+ if note.modified > cutoffDate:
+ add note to recentNotes
+
+ return sort(recentNotes, by modified descending)
+```
+
+**Test Your Implementation:**
+- Generate statistics for your note collection
+- Verify word counts are accurate
+- Check that tag frequencies are calculated correctly
+- Test monthly breakdown with notes from different months
+- Find notes that need attention (orphaned, old, etc.)
+
+**Success Criteria:**
+- Accurate counting of notes, words, and tags
+- Clear, readable statistical reports
+- Monthly activity tracking works correctly
+- Can identify notes needing attention
+- Reports help understand note collection patterns
+
+---
+
+## Task 7: Command-Line Interface Enhancement
+
+**Goal:** Professional CLI with proper argument parsing and help system
+
+**What You'll Learn:**
+- Command-line argument parsing
+- User experience design
+- Error handling and validation
+- Professional software interface design
+
+**Command Structure:**
+```pseudocode
+class Command:
+ name: string
+ description: string
+ arguments: array of Argument
+ action: function
+
+class Argument:
+ name: string
+ required: boolean
+ description: string
+ type: string
+```
+
+**CLI Framework:**
+```pseudocode
+method main(commandLineArgs):
+ commands = setupCommands()
+
+ if empty(commandLineArgs) or commandLineArgs[0] == "--help":
+ displayHelp(commands)
+ return
+
+ commandName = commandLineArgs[0]
+ commandArgs = commandLineArgs[1:]
+
+ command = findCommand(commands, commandName)
+ if command == null:
+ print "Unknown command: " + commandName
+ print "Use --help to see available commands"
+ return
+
+ try:
+ executeCommand(command, commandArgs)
+ catch error:
+ print "Error: " + error.message
+ print "Use '" + commandName + " --help' for usage information"
+
+method setupCommands():
+ commands = empty array
+
+ // Create command
+ createCmd = new Command()
+ createCmd.name = "create"
+ createCmd.description = "Create a new note"
+ createCmd.action = handleCreateCommand
+ add createCmd to commands
+
+ // List command
+ listCmd = new Command()
+ listCmd.name = "list"
+ listCmd.description = "List all notes or search with filters"
+ listCmd.arguments = [
+ new Argument("query", false, "Search query with optional filters", "string")
+ ]
+ listCmd.action = handleListCommand
+ add listCmd to commands
+
+ // Add more commands...
+
+ return commands
+```
+
+**Command Implementations:**
+```pseudocode
+method handleCreateCommand(args):
+ print "Creating new note..."
+ print "Title: "
+ title = readUserInput()
+
+ if empty(title.trim()):
+ print "Error: Title cannot be empty"
+ return
+
+ print "Tags (comma-separated, optional): "
+ tagInput = readUserInput()
+ tags = empty array
+ if not empty(tagInput.trim()):
+ tags = split(tagInput, ",")
+ tags = map(tags, tag => tag.trim())
+
+ print "Enter content (type 'END' on a line by itself to finish):"
+ content = ""
+ while true:
+ line = readUserInput()
+ if line == "END":
+ break
+ content += line + "\n"
+
+ note = createNote(title, content.trim(), tags)
+ filename = generateUniqueFilename(title)
+ saveNoteToFile(note, filename)
+
+ print "Note created: " + filename
+
+method handleListCommand(args):
+ notes = listAllNotes(getNotesDirectory())
+
+ if empty(args):
+ displayNoteList(notes)
+ return
+
+ query = join(args, " ")
+ searchQuery = parseSearchQuery(query)
+ results = advancedSearch(notes, searchQuery)
+
+ print "Search: " + query
+ displayNoteList(results)
+
+method handleEditCommand(args):
+ if empty(args):
+ print "Usage: edit "
+ return
+
+ query = join(args, " ")
+ notes = listAllNotes(getNotesDirectory())
+ results = advancedSearch(notes, parseSearchQuery(query))
+
+ if empty(results):
+ print "No notes found matching: " + query
+ return
+
+ if length(results) > 1:
+ print "Multiple notes found. Please be more specific:"
+ displayNoteList(results)
+ return
+
+ note = results[0]
+ editedNote = editNote(note)
+ saveNoteToFile(editedNote, note.filename)
+ print "Note updated: " + note.filename
+```
+
+**Help System:**
+```pseudocode
+method displayHelp(commands):
+ print "Personal Notes Manager"
+ print ""
+ print "Usage: notes [options]"
+ print ""
+ print "Commands:"
+
+ for each command in commands:
+ print " " + padRight(command.name, 12) + command.description
+
+ print ""
+ print "Examples:"
+ print " notes create # Create a new note interactively"
+ print " notes list # List all notes"
+ print " notes list meeting # Search for notes containing 'meeting'"
+ print " notes list tag:work # Find notes tagged with 'work'"
+ print " notes list after:2024-01-01 # Find notes created after Jan 1, 2024"
+ print " notes edit project # Edit note containing 'project'"
+ print " notes delete draft # Delete note containing 'draft'"
+ print " notes stats # Show collection statistics"
+ print ""
+ print "For more help on a specific command: notes --help"
+```
+
+**Test Your Implementation:**
+- Run `notes --help` and verify help displays correctly
+- Test each command with various arguments
+- Verify error messages are helpful and specific
+- Test edge cases like missing arguments
+- Ensure the interface feels professional and intuitive
+
+**Success Criteria:**
+- Clean, professional command-line interface
+- Comprehensive help system
+- Proper error handling with helpful messages
+- All major functionality accessible via CLI
+- Commands follow standard CLI conventions
+
+---
+
+## Final Integration and Testing
+
+**Comprehensive Testing Checklist:**
+
+1. **Basic Functionality**
+ - [ ] Create notes with various titles and content
+ - [ ] Read notes back correctly
+ - [ ] Edit existing notes
+ - [ ] Delete notes with confirmation
+
+2. **YAML and Metadata**
+ - [ ] YAML headers are properly formatted
+ - [ ] Timestamps are in correct ISO format
+ - [ ] Tags are stored and retrieved correctly
+ - [ ] Backward compatibility with earlier formats
+
+3. **Search and Filtering**
+ - [ ] Text search in titles and content
+ - [ ] Tag-based filtering (include/exclude)
+ - [ ] Date range filtering
+ - [ ] Complex query combinations
+
+4. **File Management**
+ - [ ] Handles large numbers of notes efficiently
+ - [ ] Graceful error handling for corrupted files
+ - [ ] Proper backup creation before deletions
+ - [ ] Unique filename generation
+
+5. **Statistics and Reporting**
+ - [ ] Accurate counts and calculations
+ - [ ] Tag frequency analysis
+ - [ ] Monthly activity tracking
+ - [ ] Useful insights and recommendations
+
+6. **Command-Line Interface**
+ - [ ] All commands work as documented
+ - [ ] Help system is comprehensive
+ - [ ] Error messages are clear and actionable
+ - [ ] Professional user experience
+
+**Next Steps:**
+Once you complete all 7 tasks, you'll have a robust CLI notes application. From here, you can:
+- Add the GUI interface (Task 8+)
+- Build the web version with REST API (Task 12+)
+- Implement advanced features like encryption, sync, or collaboration
+- Optimize performance for large note collections
+
+**Congratulations!** You've built a complete, professional-grade notes management system while learning fundamental programming concepts that will serve you throughout your development career.
\ No newline at end of file
diff --git a/README.md b/README.md
index 17e73d2..a8cd754 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,49 @@
+# Memo - a future-proof Personal Notes Manager
+
+This is "readme" for Kris' Go version of the future-proof Notes app.
+
+Written in Go, as an example.
+
+A multi-phase educational project for managing personal notes with structured metadata.
+
+In my Go veresion, I've created a `mermaid` document with diagrams. https://github.com/kristofer/memo/blob/main/ARCHITECTURE.md
+
+ > Original Readme below
+
# future-proof
# Build a Personal Notes Manager
A multi-phase educational project for managing personal notes with structured metadata.
+It is also the first time we consider the idea of **future-proof** software.
+
## Overview
Personal Notes Manager is a text-based note-taking system that stores notes as UTF-8 text files with YAML headers for metadata. This project serves as an educational tool for students to learn file manipulation, parsing, and progressively more advanced application architectures.
-https://zcw.guru/kristofer/javapygitignore
+https://zcw.guru/kristofer/javapygitignore (you might need this for your project?)
+
+## Future-Proof software
+
+"Future-proof" software is code designed to remain useful, maintainable, and adaptable as technology evolves and requirements change over time. Think of it like building a house with a strong foundation that can support renovations and additions years later.
+
+**Why future-proofing matters:**
+When you write software, you're not just solving today's problem - you're creating something that will likely need to change. User needs evolve, new technologies emerge, and business requirements shift. Future-proof code makes these inevitable changes easier and less expensive.
+
+**Key principles for future-proof software:**
+
+**Modularity and separation of concerns** - Break your code into distinct, focused pieces that do one thing well. If you need to change how data is stored, you shouldn't have to rewrite your user interface code too. If you store your notes in text files now, but a database in the next version, how do you make sure you can move amongst different storage tech?
+
+**Use established standards and conventions** - Following widely-adopted patterns means other developers can understand your code, and it's more likely to work well with future tools and libraries. And text files, based on _UNICODE_, is one of the most well-respected storage modalities. (So is _Sqlite3_, see [Library of Congress; Recommended Formats Statement](https://www.loc.gov/preservation/resources/rfs/data.html))
+
+**Avoid hard-coding values** - Instead of writing `if (users.length > 100)`, use a configurable constant like `if (users.length > MAX_USERS)`. This makes it easy to adjust limits without hunting through code. These items can be kept together, so that they can be reviewed and changed as needed in Future YEARS.
+
+**Choose stable, well-supported technologies** - While it's tempting to use the newest framework, _mature technologies_ with strong communities tend to have better long-term support.
+
+**Write clear documentation and tests** - Future you (or other developers) will thank you when they need to modify code written months or years ago. So, **unit tests** and _test-driven development_ is a critical part of this project. Test early and often. And make sure your _Future You_ knows how to run the test and read the comments and PLAN and SPEC documents.
+
+The goal isn't to predict the future perfectly, but to write code that bends rather than breaks when change inevitably comes. It's about making smart trade-offs between solving immediate needs and maintaining flexibility for tomorrow's challenges.
## Project Phases
@@ -54,7 +89,7 @@ You can include lists:
- Item 2
- Item 3
-And other simple markup as needed.
+And other simple markdown markup as needed.
```
## YAML Header Specification
@@ -73,6 +108,8 @@ The YAML header is delimited by triple dashes (`---`) and contains metadata abou
Additional custom fields can be added as needed.
+There a few sample note files in [test-notes/](./test-notes)
+
## Phase 1: CLI Implementation
### Command Reference
@@ -85,7 +122,7 @@ notes list --tag "coursework" # List notes with specific tag
notes read # Display a specific note
notes edit # Edit a specific note
notes delete # Delete a specific note
-notes search "query" # Search notes for text
+notes search "query" # Search notes for text (title, tags, content)
notes stats # Display statistics about your notes
```
@@ -93,11 +130,15 @@ notes stats # Display statistics about your notes
- Java 11+ or Python 3.11+
- YAML parser library
-- File system access
-- Text editor integration
-
+- Filesystem access & manipulation
+- Text editor integration (nano?)
+- Extra Credit: github integration for the notes storage
+- Extra Credit: note encryption (and key management)
+
## Phase 2: GUI Implementation
+Maybe this week, maybe next time we come back around to this project.
+
### Features
- Note browser panel (folder structure or tag-based)
@@ -116,6 +157,10 @@ notes stats # Display statistics about your notes
## Phase 3: REST Server Implementation
+Maybe instead of Phase 2? Maybe this week, maybe in the future.
+
+What is this stuff anyway? Well, Ask your friendly, globe-girdling AI.
+
### API Endpoints
```
@@ -133,9 +178,9 @@ GET /api/search?q=query # Search notes
- Express.js/Flask/Django REST framework (python)
- Spring/Springboot
-- VanillaJS
-- Frontend: React/Vue/Angular
-- Authentication system
+- **Frontend:** VanillaJS (first)
+- Frontend: React/Vue/Angular?
+- Authentication system (don't want just anyone reading your notes)
- Database integration (optional, should still use file system)
- API documentation (Swagger/OpenAPI)
@@ -221,6 +266,176 @@ ClassCompass
MindfulNotes
LearnLogger
+## But I wanna do a databse "NOW!!!"
+
+**Why Build Your Note-Taking App with the Filesystem?**
+
+Using the filesystem instead of a database or cloud service for your note-taking app isn't just a simpler starting point—it's a masterclass in fundamental programming concepts that every developer needs to understand.
+
+**You're learning the foundation underneath everything**
+
+Every piece of software ultimately interacts with files. Databases? They store data in files. Web servers? They serve files. Cloud applications? They manage files across distributed systems. By working directly with the filesystem, you're learning the bedrock skills that power everything else.
+
+**Essential skills you'll develop:**
+
+**File I/O operations** - You'll master reading from and writing to files, which is core to virtually every application. Whether you're processing configuration files, importing data, or generating reports, these skills transfer everywhere.
+
+**Data serialization and parsing** - Converting your note objects into text (JSON, Markdown, XML) and back again teaches you how data moves between memory and storage. This same concept applies when sending data over networks or saving user preferences.
+
+**Directory structure and organization** - Deciding how to organize notes (by date? by topic? in nested folders?) teaches you about data modeling and hierarchical thinking. These concepts directly apply to database design, URL routing in web apps, and organizing any complex system.
+
+**Error handling and edge cases** - What happens when a file is corrupted? When the disk is full? When two processes try to write the same file? Filesystem programming forces you to think about real-world failure scenarios.
+
+**Performance considerations** - You'll discover why reading one large file might be faster than many small ones, or why scanning hundreds of files can be slow. These insights apply to database queries, API calls, and system optimization everywhere.
+
+**Practical examples from your note-taking app:**
+
+**File naming and collision handling** - When a user creates two notes with the same title, how do you handle it? Do you append timestamps? Create numbered versions? This teaches you about unique identifiers and conflict resolution.
+
+**Incremental loading** - If you have thousands of notes, you can't load them all into memory. You'll learn to read file lists, load metadata first, and fetch full content on demand—the same patterns used in web pagination and lazy loading.
+
+**Search without databases** - Building search functionality by scanning files teaches you about indexing, text processing, and performance trade-offs. Later, when you use a database, you'll understand what it's doing behind the scenes.
+
+**Backup and versioning** - How do you prevent data loss? How do you track changes? You might implement simple versioning by copying files or creating timestamps, learning the concepts behind version control systems.
+
+**Cross-platform compatibility** - Different operating systems handle file paths differently. Learning to work with `/` vs `\` separators and absolute vs relative paths teaches you about portability and system-level programming.
+
+**The learning progression:**
+
+You'll start simple—just saving text to a file. Then you'll want better organization, so you'll create folders. Then you'll want metadata, so you'll learn JSON. Then you'll want search, so you'll read multiple files efficiently. Each step builds naturally on the last.
+
+**Why this beats starting with a database:**
+
+Databases hide complexity behind convenient APIs. While that's great for building applications quickly, it means you miss fundamental concepts about how data is actually stored and retrieved. When you eventually use databases (and you will), you'll understand the problems they're solving because you've faced those problems yourself.
+
+**Real-world relevance:**
+
+Many successful applications started as filesystem-based tools. Git manages millions of files efficiently. Static site generators like Jekyll process thousands of markdown files. Log analysis tools parse massive text files. Configuration management systems organize files across servers.
+
+**Skills that transfer everywhere:**
+
+The file handling patterns you learn will apply when you're processing CSV data, managing configuration files, building deployment scripts, or working with any system that stores and retrieves information. You're not just building a note-taking app—you're learning to think like a systems programmer.
+
+Starting with the filesystem gives you a deep understanding of how computers actually work with data. It's messier and more complex than using a database, but that complexity teaches you invaluable problem-solving skills. Every challenge you solve—from handling concurrent access to organizing large amounts of data—prepares you for the real-world complexities you'll face as a professional developer.
+
+## Ahem, about that Test-Driven Development thingie
+
+**What is Test-Driven Development (TDD)?**
+
+Test-Driven Development is a programming approach where you write tests *before* writing the actual code. It follows a simple cycle called "Red-Green-Refactor":
+
+1. **Red**: Write a failing test that describes what you want your code to do
+2. **Green**: Write the minimal code needed to make that test pass
+3. **Refactor**: Clean up and improve the code while keeping tests passing
+
+Think of it like writing a recipe backwards - you first describe what the finished dish should taste like, then figure out how to cook it.
+
+**Why beginners should embrace TDD:**
+
+**Clarifies your thinking** - Writing tests first forces you to think through exactly what your function should do before you get lost in implementation details. It's like sketching before painting.
+
+**Catches bugs early** - Tests act as a safety net. When you change code later, your tests immediately tell you if you broke something that was previously working.
+
+**Improves code design** - Code that's easy to test tends to be well-organized and modular. TDD naturally pushes you toward better architecture.
+
+**Builds confidence** - You can refactor and improve your code fearlessly, knowing your tests will catch any mistakes.
+
+**Serves as documentation** - Good tests show other developers (including future you) how your code is supposed to work.
+
+**Need to Make Sure** - There are 3-4 test _For Every Method!!!_ Lots o'tests, many, many tests. So Many Tests!!
+
+**How to do TDD:**
+
+These test examples are in **JavaScript**, so use them only as conceptual examples.
+You will write this app in either Java or Python, and use the standard filesystem (using text files as your data format) for your _storage infrastructure_.
+Someday soon, you may get the chance (or motivation) to store the notes in a database of some kind, but for now, text files.
+
+Let's say you're building a function to validate note titles. Here's the TDD process:
+
+**Step 1 (Red)**: Write a failing test
+```javascript
+test('should reject empty note titles', () => {
+ expect(validateNoteTitle('')).toBe(false);
+});
+```
+
+**Step 2 (Green)**: Write minimal code to pass
+```javascript
+function validateNoteTitle(title) {
+ return title !== '';
+}
+```
+
+**Step 3 (Refactor)**: Improve while keeping tests green
+```javascript
+function validateNoteTitle(title) {
+ return typeof title === 'string' && title.trim().length > 0;
+}
+```
+
+Then repeat: add more tests for edge cases, implement features incrementally.
+**Start small, with a tiny core of functionality.**
+And then _build outwards_, add capabilities (and TESTs Tests tests) as you go!
+
+**TDD in your note-taking CLI application:**
+
+Here are specific areas where TDD would be valuable:
+
+**Note creation and validation**
+- Test that notes require titles
+- Test that notes can have optional tags
+- Test that duplicate titles are handled appropriately
+- Test that special characters in titles are escaped properly
+
+**Search functionality**
+- Test searching by keywords finds correct notes
+- Test case-insensitive search works
+- Test searching with multiple tags
+- Test handling of search terms with special characters
+
+**File operations**
+- Test that notes are saved to the correct file format
+- Test that corrupted files are handled gracefully
+- Test that backup creation works
+- Test that notes can be exported to different formats
+
+**Command parsing**
+- Test that CLI commands are parsed correctly (`add`, `search`, `delete`)
+- Test that invalid commands show helpful error messages
+- Test that command flags work as expected (`--tag`, `--date`)
+
+**Example TDD workflow for your CLI app:**
+
+```javascript
+// Test first
+test('should create note with title and content', () => {
+ const note = createNote('My First Note', 'This is the content');
+ expect(note.title).toBe('My First Note');
+ expect(note.content).toBe('This is the content');
+ expect(note.createdAt).toBeInstanceOf(Date);
+});
+
+// Then implement
+function createNote(title, content) {
+ return {
+ title: title,
+ content: content,
+ createdAt: new Date(),
+ id: generateId()
+ };
+}
+```
+
+**Getting started:**
+1. Choose a testing framework (`junit` for Java, `pytest` and/or `unittest` for Python, etc.)
+2. Start with simple, pure functions (like validation, method correctness or formatting)
+3. Write one test, make it pass, then add another
+4. Don't try to test everything at once - build up gradually
+
+TDD might feel slow initially, but it pays huge dividends as your application grows.
+You'll spend less time debugging mysterious bugs and more time confidently adding new features.
+For a personal project like this note-taking app, it also helps you think through the user experience before getting caught up in implementation details.
+
## Finally
_Why does choosing to use text files in a standard directory structure using Markdown as a note format, make the project "future proof"?_
diff --git a/cmd/command.go b/cmd/command.go
new file mode 100644
index 0000000..2352096
--- /dev/null
+++ b/cmd/command.go
@@ -0,0 +1,27 @@
+package cmd
+
+import (
+ "memo/internal/note"
+ "memo/internal/storage"
+)
+
+// Command interface defines the contract for all CLI commands
+type Command interface {
+ Execute(args []string) error
+}
+
+// CommandContext provides shared dependencies for all commands
+type CommandContext struct {
+ Storage *storage.FileStorage
+ CurrentListing []*note.Note
+}
+
+// SetCurrentListing updates the current listing (used by list command)
+func (ctx *CommandContext) SetCurrentListing(notes []*note.Note) {
+ ctx.CurrentListing = notes
+}
+
+// GetCurrentListing returns the current listing
+func (ctx *CommandContext) GetCurrentListing() []*note.Note {
+ return ctx.CurrentListing
+}
\ No newline at end of file
diff --git a/cmd/commands.go b/cmd/commands.go
new file mode 100644
index 0000000..99ac3fd
--- /dev/null
+++ b/cmd/commands.go
@@ -0,0 +1,68 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "memo/internal/storage"
+ "memo/internal/ui"
+)
+
+type App struct {
+ ctx *CommandContext
+ commands map[string]Command
+}
+
+func NewApp() *App {
+ ctx := &CommandContext{
+ Storage: storage.NewFileStorage(),
+ }
+
+ app := &App{
+ ctx: ctx,
+ commands: make(map[string]Command),
+ }
+
+ // Register all commands
+ app.registerCommands()
+
+ return app
+}
+
+func (app *App) registerCommands() {
+ app.commands["create"] = NewCreateCommand(app.ctx)
+ app.commands["list"] = NewListCommand(app.ctx)
+ app.commands["read"] = NewReadCommand(app.ctx)
+ app.commands["edit"] = NewEditCommand(app.ctx)
+ app.commands["delete"] = NewDeleteCommand(app.ctx)
+ app.commands["search"] = NewSearchCommand(app.ctx)
+ app.commands["stats"] = NewStatsCommand(app.ctx)
+ app.commands["help"] = NewHelpCommand(app.ctx)
+ app.commands["--help"] = NewHelpCommand(app.ctx)
+ app.commands["-h"] = NewHelpCommand(app.ctx)
+}
+
+func (app *App) Run() {
+ if len(os.Args) < 2 {
+ ui.PrintHelp()
+ return
+ }
+
+ commandName := os.Args[1]
+ args := []string{}
+ if len(os.Args) > 2 {
+ args = os.Args[2:]
+ }
+
+ command, exists := app.commands[commandName]
+ if !exists {
+ fmt.Printf("Unknown command: %s\n", commandName)
+ ui.PrintHelp()
+ return
+ }
+
+ err := command.Execute(args)
+ if err != nil {
+ fmt.Printf("Error: %v\n", err)
+ }
+}
\ No newline at end of file
diff --git a/cmd/create_command.go b/cmd/create_command.go
new file mode 100644
index 0000000..90c2a92
--- /dev/null
+++ b/cmd/create_command.go
@@ -0,0 +1,46 @@
+package cmd
+
+import (
+ "fmt"
+ "strings"
+
+ "memo/internal/note"
+ "memo/internal/ui"
+)
+
+type CreateCommand struct {
+ ctx *CommandContext
+}
+
+func NewCreateCommand(ctx *CommandContext) *CreateCommand {
+ return &CreateCommand{ctx: ctx}
+}
+
+func (c *CreateCommand) Execute(args []string) error {
+ title := ui.PromptForInput("Enter note title: ")
+ if title == "" {
+ return fmt.Errorf("title is required")
+ }
+
+ content := ui.PromptForInput("Enter note content: ")
+
+ tagsInput := ui.PromptForInput("Enter tags (comma-separated, optional): ")
+ var tags []string
+ if tagsInput != "" {
+ for _, tag := range strings.Split(tagsInput, ",") {
+ tags = append(tags, strings.TrimSpace(tag))
+ }
+ }
+
+ noteID := c.ctx.Storage.GenerateNoteID()
+ n := note.New(title, content, tags)
+ n.SetFilePath(c.ctx.Storage.GenerateNoteFilePath(noteID))
+
+ err := c.ctx.Storage.SaveNote(n)
+ if err != nil {
+ return fmt.Errorf("error creating note: %w", err)
+ }
+
+ fmt.Printf("Note created successfully: %s\n", noteID)
+ return nil
+}
\ No newline at end of file
diff --git a/cmd/delete_command.go b/cmd/delete_command.go
new file mode 100644
index 0000000..18f06e6
--- /dev/null
+++ b/cmd/delete_command.go
@@ -0,0 +1,66 @@
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "memo/internal/ui"
+)
+
+type DeleteCommand struct {
+ ctx *CommandContext
+}
+
+func NewDeleteCommand(ctx *CommandContext) *DeleteCommand {
+ return &DeleteCommand{ctx: ctx}
+}
+
+func (c *DeleteCommand) Execute(args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("note-id or number required\nUsage: memo delete ")
+ }
+
+ identifier := args[0]
+ noteID, err := c.resolveNoteID(identifier)
+ if err != nil {
+ return err
+ }
+
+ n, err := c.ctx.Storage.FindNoteByID(noteID)
+ if err != nil {
+ return err
+ }
+
+ prompt := fmt.Sprintf("Are you sure you want to delete note '%s'? (y/N): ", n.Metadata.Title)
+ if !ui.ConfirmAction(prompt) {
+ fmt.Println("Deletion cancelled.")
+ return nil
+ }
+
+ err = c.ctx.Storage.DeleteNote(noteID)
+ if err != nil {
+ return fmt.Errorf("error deleting note: %w", err)
+ }
+
+ fmt.Println("Note deleted successfully!")
+ return nil
+}
+
+func (c *DeleteCommand) resolveNoteID(identifier string) (string, error) {
+ if num, err := strconv.Atoi(identifier); err == nil {
+ if c.ctx.CurrentListing == nil || len(c.ctx.CurrentListing) == 0 {
+ return "", fmt.Errorf("no current note listing. Please run 'memo list' first")
+ }
+
+ if num < 1 || num > len(c.ctx.CurrentListing) {
+ return "", fmt.Errorf("number %d is out of range. Valid range: 1-%d", num, len(c.ctx.CurrentListing))
+ }
+
+ n := c.ctx.CurrentListing[num-1]
+ return strings.TrimSuffix(filepath.Base(n.FilePath), ".note"), nil
+ }
+
+ return identifier, nil
+}
\ No newline at end of file
diff --git a/cmd/edit_command.go b/cmd/edit_command.go
new file mode 100644
index 0000000..d1b110d
--- /dev/null
+++ b/cmd/edit_command.go
@@ -0,0 +1,79 @@
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "memo/internal/ui"
+)
+
+type EditCommand struct {
+ ctx *CommandContext
+}
+
+func NewEditCommand(ctx *CommandContext) *EditCommand {
+ return &EditCommand{ctx: ctx}
+}
+
+func (c *EditCommand) Execute(args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("note-id or number required\nUsage: memo edit ")
+ }
+
+ identifier := args[0]
+ noteID, err := c.resolveNoteID(identifier)
+ if err != nil {
+ return err
+ }
+
+ n, err := c.ctx.Storage.FindNoteByID(noteID)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("Editing note: %s\n", n.Metadata.Title)
+ fmt.Printf("Current content:\n%s\n\n", n.Content)
+
+ newContent := ui.PromptForInput("Enter new content (leave empty to keep current): ")
+ if newContent != "" {
+ n.UpdateContent(newContent)
+ }
+
+ currentTags := strings.Join(n.Metadata.Tags, ", ")
+ fmt.Printf("Current tags: %s\n", currentTags)
+ newTags := ui.PromptForInput("Enter new tags (comma-separated, leave empty to keep current): ")
+ if newTags != "" {
+ var tags []string
+ for _, tag := range strings.Split(newTags, ",") {
+ tags = append(tags, strings.TrimSpace(tag))
+ }
+ n.UpdateTags(tags)
+ }
+
+ err = c.ctx.Storage.SaveNote(n)
+ if err != nil {
+ return fmt.Errorf("error saving note: %w", err)
+ }
+
+ fmt.Println("Note updated successfully!")
+ return nil
+}
+
+func (c *EditCommand) resolveNoteID(identifier string) (string, error) {
+ if num, err := strconv.Atoi(identifier); err == nil {
+ if c.ctx.CurrentListing == nil || len(c.ctx.CurrentListing) == 0 {
+ return "", fmt.Errorf("no current note listing. Please run 'memo list' first")
+ }
+
+ if num < 1 || num > len(c.ctx.CurrentListing) {
+ return "", fmt.Errorf("number %d is out of range. Valid range: 1-%d", num, len(c.ctx.CurrentListing))
+ }
+
+ n := c.ctx.CurrentListing[num-1]
+ return strings.TrimSuffix(filepath.Base(n.FilePath), ".note"), nil
+ }
+
+ return identifier, nil
+}
\ No newline at end of file
diff --git a/cmd/help_command.go b/cmd/help_command.go
new file mode 100644
index 0000000..e595843
--- /dev/null
+++ b/cmd/help_command.go
@@ -0,0 +1,16 @@
+package cmd
+
+import "memo/internal/ui"
+
+type HelpCommand struct {
+ ctx *CommandContext
+}
+
+func NewHelpCommand(ctx *CommandContext) *HelpCommand {
+ return &HelpCommand{ctx: ctx}
+}
+
+func (c *HelpCommand) Execute(args []string) error {
+ ui.PrintHelp()
+ return nil
+}
\ No newline at end of file
diff --git a/cmd/list_command.go b/cmd/list_command.go
new file mode 100644
index 0000000..9b2abee
--- /dev/null
+++ b/cmd/list_command.go
@@ -0,0 +1,53 @@
+package cmd
+
+import (
+ "fmt"
+
+ "memo/internal/note"
+ "memo/internal/ui"
+)
+
+type ListCommand struct {
+ ctx *CommandContext
+}
+
+func NewListCommand(ctx *CommandContext) *ListCommand {
+ return &ListCommand{ctx: ctx}
+}
+
+func (c *ListCommand) Execute(args []string) error {
+ var tagFilter string
+ if len(args) >= 2 && args[0] == "--tag" {
+ tagFilter = args[1]
+ } else if len(args) >= 1 && args[0] == "--tag" {
+ return fmt.Errorf("tag value required\nUsage: memo list --tag ")
+ }
+
+ var notes []*note.Note
+ var err error
+
+ if tagFilter != "" {
+ notes, err = c.ctx.Storage.FilterNotesByTag(tagFilter)
+ if err != nil {
+ return fmt.Errorf("error filtering notes by tag: %w", err)
+ }
+ fmt.Printf("Notes with tag '%s':\n", tagFilter)
+ } else {
+ notes, err = c.ctx.Storage.GetAllNotes()
+ if err != nil {
+ return fmt.Errorf("error listing notes: %w", err)
+ }
+ fmt.Println("All notes:")
+ }
+
+ if len(notes) == 0 {
+ fmt.Println("No notes found.")
+ return nil
+ }
+
+ // Update current listing for number-based access
+ c.ctx.SetCurrentListing(notes)
+ ui.DisplayNotesWithPagination(notes)
+
+ return nil
+}
\ No newline at end of file
diff --git a/cmd/read_command.go b/cmd/read_command.go
new file mode 100644
index 0000000..8ded3dc
--- /dev/null
+++ b/cmd/read_command.go
@@ -0,0 +1,55 @@
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "memo/internal/ui"
+)
+
+type ReadCommand struct {
+ ctx *CommandContext
+}
+
+func NewReadCommand(ctx *CommandContext) *ReadCommand {
+ return &ReadCommand{ctx: ctx}
+}
+
+func (c *ReadCommand) Execute(args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("note-id or number required\nUsage: memo read ")
+ }
+
+ identifier := args[0]
+ noteID, err := c.resolveNoteID(identifier)
+ if err != nil {
+ return err
+ }
+
+ n, err := c.ctx.Storage.FindNoteByID(noteID)
+ if err != nil {
+ return err
+ }
+
+ ui.DisplayNote(n)
+ return nil
+}
+
+func (c *ReadCommand) resolveNoteID(identifier string) (string, error) {
+ if num, err := strconv.Atoi(identifier); err == nil {
+ if c.ctx.CurrentListing == nil || len(c.ctx.CurrentListing) == 0 {
+ return "", fmt.Errorf("no current note listing. Please run 'memo list' first")
+ }
+
+ if num < 1 || num > len(c.ctx.CurrentListing) {
+ return "", fmt.Errorf("number %d is out of range. Valid range: 1-%d", num, len(c.ctx.CurrentListing))
+ }
+
+ n := c.ctx.CurrentListing[num-1]
+ return strings.TrimSuffix(filepath.Base(n.FilePath), ".note"), nil
+ }
+
+ return identifier, nil
+}
\ No newline at end of file
diff --git a/cmd/search_command.go b/cmd/search_command.go
new file mode 100644
index 0000000..23eaedb
--- /dev/null
+++ b/cmd/search_command.go
@@ -0,0 +1,30 @@
+package cmd
+
+import (
+ "fmt"
+
+ "memo/internal/ui"
+)
+
+type SearchCommand struct {
+ ctx *CommandContext
+}
+
+func NewSearchCommand(ctx *CommandContext) *SearchCommand {
+ return &SearchCommand{ctx: ctx}
+}
+
+func (c *SearchCommand) Execute(args []string) error {
+ if len(args) < 1 {
+ return fmt.Errorf("search query required\nUsage: memo search ")
+ }
+
+ query := args[0]
+ notes, err := c.ctx.Storage.SearchNotes(query)
+ if err != nil {
+ return fmt.Errorf("error searching notes: %w", err)
+ }
+
+ ui.DisplaySearchResults(notes, query)
+ return nil
+}
\ No newline at end of file
diff --git a/cmd/stats_command.go b/cmd/stats_command.go
new file mode 100644
index 0000000..456adf2
--- /dev/null
+++ b/cmd/stats_command.go
@@ -0,0 +1,25 @@
+package cmd
+
+import (
+ "fmt"
+
+ "memo/internal/ui"
+)
+
+type StatsCommand struct {
+ ctx *CommandContext
+}
+
+func NewStatsCommand(ctx *CommandContext) *StatsCommand {
+ return &StatsCommand{ctx: ctx}
+}
+
+func (c *StatsCommand) Execute(args []string) error {
+ notes, err := c.ctx.Storage.GetAllNotes()
+ if err != nil {
+ return fmt.Errorf("error loading notes: %w", err)
+ }
+
+ ui.DisplayStats(notes)
+ return nil
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..df9a40a
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module memo
+
+go 1.24.1
+
+require gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a62c313
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/note/note.go b/internal/note/note.go
new file mode 100644
index 0000000..1db2c90
--- /dev/null
+++ b/internal/note/note.go
@@ -0,0 +1,72 @@
+package note
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "gopkg.in/yaml.v3"
+)
+
+type Metadata struct {
+ Title string `yaml:"title"`
+ Created time.Time `yaml:"created"`
+ Modified time.Time `yaml:"modified"`
+ Tags []string `yaml:"tags,omitempty"`
+ Author string `yaml:"author,omitempty"`
+ Status string `yaml:"status,omitempty"`
+ Priority int `yaml:"priority,omitempty"`
+}
+
+type Note struct {
+ Metadata Metadata
+ Content string
+ FilePath string
+}
+
+func New(title, content string, tags []string) *Note {
+ now := time.Now()
+ return &Note{
+ Metadata: Metadata{
+ Title: title,
+ Created: now,
+ Modified: now,
+ Tags: tags,
+ },
+ Content: content,
+ }
+}
+
+func (n *Note) SetFilePath(path string) {
+ n.FilePath = path
+}
+
+func (n *Note) UpdateContent(content string) {
+ n.Content = content
+ n.Metadata.Modified = time.Now()
+}
+
+func (n *Note) UpdateTags(tags []string) {
+ n.Metadata.Tags = tags
+ n.Metadata.Modified = time.Now()
+}
+
+func (n *Note) ToFileContent() (string, error) {
+ n.Metadata.Modified = time.Now()
+
+ yamlData, err := yaml.Marshal(&n.Metadata)
+ if err != nil {
+ return "", fmt.Errorf("error marshaling metadata: %w", err)
+ }
+
+ return fmt.Sprintf("---\n%s---\n\n%s", string(yamlData), n.Content), nil
+}
+
+func (n *Note) Save() error {
+ content, err := n.ToFileContent()
+ if err != nil {
+ return err
+ }
+
+ return os.WriteFile(n.FilePath, []byte(content), 0644)
+}
\ No newline at end of file
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
new file mode 100644
index 0000000..51756d2
--- /dev/null
+++ b/internal/storage/storage.go
@@ -0,0 +1,181 @@
+package storage
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "gopkg.in/yaml.v3"
+ "memo/internal/note"
+)
+
+const (
+ DefaultNotesDir = ".memo-notes"
+ DefaultNoteExtension = ".note"
+)
+
+type FileStorage struct {
+ notesDir string
+ noteExtension string
+}
+
+func NewFileStorage() *FileStorage {
+ return &FileStorage{
+ notesDir: DefaultNotesDir,
+ noteExtension: DefaultNoteExtension,
+ }
+}
+
+func NewFileStorageWithConfig(notesDir, noteExtension string) *FileStorage {
+ return &FileStorage{
+ notesDir: notesDir,
+ noteExtension: noteExtension,
+ }
+}
+
+func (fs *FileStorage) EnsureNotesDir() error {
+ if _, err := os.Stat(fs.notesDir); os.IsNotExist(err) {
+ return os.MkdirAll(fs.notesDir, 0755)
+ }
+ return nil
+}
+
+func (fs *FileStorage) GenerateNoteID() string {
+ return fmt.Sprintf("note_%d", time.Now().Unix())
+}
+
+func (fs *FileStorage) GenerateNoteFilePath(noteID string) string {
+ return filepath.Join(fs.notesDir, noteID+fs.noteExtension)
+}
+
+func (fs *FileStorage) ParseNote(filePath string) (*note.Note, error) {
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("error reading file: %w", err)
+ }
+
+ contentStr := string(content)
+
+ if !strings.HasPrefix(contentStr, "---\n") {
+ return nil, fmt.Errorf("note file must start with YAML front matter")
+ }
+
+ parts := strings.Split(contentStr, "\n---\n")
+ if len(parts) < 2 {
+ return nil, fmt.Errorf("invalid note format: missing YAML front matter delimiter")
+ }
+
+ yamlContent := parts[0][4:] // Remove the first "---\n"
+ noteContent := strings.Join(parts[1:], "\n---\n")
+
+ var metadata note.Metadata
+ err = yaml.Unmarshal([]byte(yamlContent), &metadata)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing YAML metadata: %w", err)
+ }
+
+ n := ¬e.Note{
+ Metadata: metadata,
+ Content: strings.TrimSpace(noteContent),
+ FilePath: filePath,
+ }
+
+ return n, nil
+}
+
+func (fs *FileStorage) SaveNote(n *note.Note) error {
+ if err := fs.EnsureNotesDir(); err != nil {
+ return fmt.Errorf("error ensuring notes directory: %w", err)
+ }
+
+ return n.Save()
+}
+
+func (fs *FileStorage) GetAllNotes() ([]*note.Note, error) {
+ if err := fs.EnsureNotesDir(); err != nil {
+ return nil, fmt.Errorf("error ensuring notes directory: %w", err)
+ }
+
+ files, err := filepath.Glob(filepath.Join(fs.notesDir, "*"+fs.noteExtension))
+ if err != nil {
+ return nil, fmt.Errorf("error finding note files: %w", err)
+ }
+
+ var notes []*note.Note
+ for _, file := range files {
+ n, err := fs.ParseNote(file)
+ if err != nil {
+ fmt.Printf("Warning: failed to parse note %s: %v\n", file, err)
+ continue
+ }
+ notes = append(notes, n)
+ }
+
+ return notes, nil
+}
+
+func (fs *FileStorage) FindNoteByID(noteID string) (*note.Note, error) {
+ notePath := fs.GenerateNoteFilePath(noteID)
+ if _, err := os.Stat(notePath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("note with ID '%s' not found", noteID)
+ }
+ return fs.ParseNote(notePath)
+}
+
+func (fs *FileStorage) DeleteNote(noteID string) error {
+ notePath := fs.GenerateNoteFilePath(noteID)
+ if _, err := os.Stat(notePath); os.IsNotExist(err) {
+ return fmt.Errorf("note with ID '%s' not found", noteID)
+ }
+ return os.Remove(notePath)
+}
+
+func (fs *FileStorage) SearchNotes(query string) ([]*note.Note, error) {
+ notes, err := fs.GetAllNotes()
+ if err != nil {
+ return nil, err
+ }
+
+ var matches []*note.Note
+ queryLower := strings.ToLower(query)
+
+ for _, n := range notes {
+ if strings.Contains(strings.ToLower(n.Metadata.Title), queryLower) ||
+ strings.Contains(strings.ToLower(n.Content), queryLower) {
+ matches = append(matches, n)
+ continue
+ }
+
+ for _, tag := range n.Metadata.Tags {
+ if strings.Contains(strings.ToLower(tag), queryLower) {
+ matches = append(matches, n)
+ break
+ }
+ }
+ }
+
+ return matches, nil
+}
+
+func (fs *FileStorage) FilterNotesByTag(tag string) ([]*note.Note, error) {
+ notes, err := fs.GetAllNotes()
+ if err != nil {
+ return nil, err
+ }
+
+ var matches []*note.Note
+ tagLower := strings.ToLower(tag)
+
+ for _, n := range notes {
+ for _, noteTag := range n.Metadata.Tags {
+ if strings.ToLower(noteTag) == tagLower {
+ matches = append(matches, n)
+ break
+ }
+ }
+ }
+
+ return matches, nil
+}
\ No newline at end of file
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
new file mode 100644
index 0000000..b6fad9a
--- /dev/null
+++ b/internal/ui/ui.go
@@ -0,0 +1,189 @@
+package ui
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "memo/internal/note"
+)
+
+func PromptForInput(prompt string) string {
+ fmt.Print(prompt)
+ scanner := bufio.NewScanner(os.Stdin)
+ scanner.Scan()
+ return scanner.Text()
+}
+
+func PrintHelp() {
+ fmt.Println("Memo - Personal Notes Manager")
+ fmt.Println("")
+ fmt.Println("Usage:")
+ fmt.Println(" memo create Create a new note")
+ fmt.Println(" memo list List all notes (with numbered references)")
+ fmt.Println(" memo list --tag List notes with specific tag")
+ fmt.Println(" memo read Display a specific note")
+ fmt.Println(" memo edit Edit a specific note")
+ fmt.Println(" memo delete Delete a specific note")
+ fmt.Println(" memo search Search notes for text")
+ fmt.Println(" memo stats Display statistics about your notes")
+ fmt.Println(" memo --help Display this help information")
+ fmt.Println("")
+ fmt.Println("Note: After running 'memo list', you can use numbers 1-N to reference notes")
+ fmt.Println(" instead of the full note ID (e.g., 'memo read 3' or 'memo edit 5')")
+}
+
+func DisplayNotesWithPagination(notes []*note.Note) {
+ const pageSize = 10
+ startIndex := 0
+
+ for {
+ endIndex := startIndex + pageSize
+ if endIndex > len(notes) {
+ endIndex = len(notes)
+ }
+
+ fmt.Printf("\nShowing notes %d-%d of %d:\n", startIndex+1, endIndex, len(notes))
+ fmt.Println("========================================")
+
+ for i := startIndex; i < endIndex; i++ {
+ n := notes[i]
+ noteID := strings.TrimSuffix(filepath.Base(n.FilePath), ".note")
+ listNumber := i + 1
+
+ fmt.Printf("%2d. %s | Created: %s\n",
+ listNumber,
+ n.Metadata.Title,
+ n.Metadata.Created.Format("2006-01-02 15:04"))
+
+ if len(n.Metadata.Tags) > 0 {
+ fmt.Printf(" Tags: %s\n", strings.Join(n.Metadata.Tags, ", "))
+ }
+ fmt.Printf(" ID: %s\n", noteID)
+ fmt.Println()
+ }
+
+ if endIndex >= len(notes) {
+ fmt.Println("End of notes.")
+ break
+ }
+
+ fmt.Printf("Show next %d notes? (y/N): ", pageSize)
+ response := PromptForInput("")
+
+ if strings.ToLower(response) != "y" && strings.ToLower(response) != "yes" {
+ break
+ }
+
+ startIndex = endIndex
+ }
+
+ fmt.Println("\nTip: Use 'memo read ' or 'memo edit ' with numbers 1-" + strconv.Itoa(len(notes)) + " from this listing.")
+}
+
+func DisplayNote(n *note.Note) {
+ fmt.Printf("Title: %s\n", n.Metadata.Title)
+ fmt.Printf("Created: %s\n", n.Metadata.Created.Format("2006-01-02 15:04:05"))
+ fmt.Printf("Modified: %s\n", n.Metadata.Modified.Format("2006-01-02 15:04:05"))
+
+ if len(n.Metadata.Tags) > 0 {
+ fmt.Printf("Tags: %s\n", strings.Join(n.Metadata.Tags, ", "))
+ }
+
+ if n.Metadata.Author != "" {
+ fmt.Printf("Author: %s\n", n.Metadata.Author)
+ }
+
+ if n.Metadata.Status != "" {
+ fmt.Printf("Status: %s\n", n.Metadata.Status)
+ }
+
+ if n.Metadata.Priority > 0 {
+ fmt.Printf("Priority: %d\n", n.Metadata.Priority)
+ }
+
+ fmt.Println("\nContent:")
+ fmt.Println("--------")
+ fmt.Println(n.Content)
+}
+
+func DisplaySearchResults(notes []*note.Note, query string) {
+ if len(notes) == 0 {
+ fmt.Printf("No notes found matching '%s'\n", query)
+ return
+ }
+
+ fmt.Printf("Found %d note(s) matching '%s':\n\n", len(notes), query)
+
+ for _, n := range notes {
+ noteID := strings.TrimSuffix(filepath.Base(n.FilePath), ".note")
+ fmt.Printf("ID: %s | Title: %s\n", noteID, n.Metadata.Title)
+
+ preview := n.Content
+ if len(preview) > 100 {
+ preview = preview[:100] + "..."
+ }
+ fmt.Printf("Preview: %s\n", preview)
+ fmt.Println("--------")
+ }
+}
+
+func DisplayStats(notes []*note.Note) {
+ if len(notes) == 0 {
+ fmt.Println("No notes found.")
+ return
+ }
+
+ fmt.Println("Note Statistics:")
+ fmt.Printf("Total notes: %d\n", len(notes))
+
+ tagCount := make(map[string]int)
+ var totalWords int
+ var oldestNote, newestNote *note.Note
+
+ for i, n := range notes {
+ words := strings.Fields(n.Content)
+ totalWords += len(words)
+
+ if i == 0 {
+ oldestNote = n
+ newestNote = n
+ } else {
+ if n.Metadata.Created.Before(oldestNote.Metadata.Created) {
+ oldestNote = n
+ }
+ if n.Metadata.Created.After(newestNote.Metadata.Created) {
+ newestNote = n
+ }
+ }
+
+ for _, tag := range n.Metadata.Tags {
+ tagCount[tag]++
+ }
+ }
+
+ fmt.Printf("Total words: %d\n", totalWords)
+ fmt.Printf("Average words per note: %.1f\n", float64(totalWords)/float64(len(notes)))
+
+ if oldestNote != nil {
+ fmt.Printf("Oldest note: %s (%s)\n", oldestNote.Metadata.Title, oldestNote.Metadata.Created.Format("2006-01-02"))
+ }
+ if newestNote != nil {
+ fmt.Printf("Newest note: %s (%s)\n", newestNote.Metadata.Title, newestNote.Metadata.Created.Format("2006-01-02"))
+ }
+
+ if len(tagCount) > 0 {
+ fmt.Printf("\nTag usage:\n")
+ for tag, count := range tagCount {
+ fmt.Printf(" %s: %d\n", tag, count)
+ }
+ }
+}
+
+func ConfirmAction(prompt string) bool {
+ response := PromptForInput(prompt)
+ return strings.ToLower(response) == "y" || strings.ToLower(response) == "yes"
+}
\ No newline at end of file
diff --git a/java/README.md b/java/README.md
deleted file mode 100644
index 8fd184a..0000000
--- a/java/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Future Proof Notes - Java
-
-## Reading
-
-### Good Code Examples
-### Bad Code Examples
-
-## Using data structures
-
-Notes and Labs on this.
-
-## File manipulation
-
-Notes and Labs on this.
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..b417c7a
--- /dev/null
+++ b/main.go
@@ -0,0 +1,8 @@
+package main
+
+import "memo/cmd"
+
+func main() {
+ app := cmd.NewApp()
+ app.Run()
+}
\ No newline at end of file
diff --git a/memo b/memo
new file mode 100755
index 0000000..2e12c73
Binary files /dev/null and b/memo differ
diff --git a/python/README.md b/python/README.md
deleted file mode 100644
index 3a9dcda..0000000
--- a/python/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Future Proof Notes - Python
-
-## Reading
-
-### Good Code Examples
-### Bad Code Examples
-
-## Using data structures
-
-Notes and Labs on this.
-
-## File manipulation
-
-Notes and Labs on this.