using NozCli.Core.Client; using NozCli.Core.Config; using NozCli.Core.Models; namespace NozCli.Repl; /// Shared mutable state across the REPL session. /// Navigation model: /// ~ → root, FoldersInView() returns distinct topics /// ~/psychologie → topic view, notes filtered by topic /// ~/biosysteme/subfolder → slug subfolder within a topic public class ReplContext { public NozApiClient Api { get; } public LocalConfig Config { get; } public List NoteCache { get; set; } = []; public bool Running { get; set; } = true; // Navigation state public string? CurrentTopic { get; set; } = null; // null = root public string? CurrentSubPath { get; set; } = null; // sub-path within topic public string CurrentPath => CurrentTopic is null ? "~" : CurrentSubPath is null ? $"~/{CurrentTopic}" : $"~/{CurrentTopic}/{CurrentSubPath}"; public ReplContext(NozApiClient api, LocalConfig config) { Api = api; Config = config; } /// Notes filtered to the current topic (all notes at root). public IEnumerable NotesInView() { if (CurrentTopic is null) return NoteCache; var topicNotes = NoteCache.Where(n => string.Equals(n.Topic, CurrentTopic, StringComparison.OrdinalIgnoreCase)); if (CurrentSubPath is null) return topicNotes; // Within a subfolder: filter by the slug sub-path relative to topic root return topicNotes.Where(n => { var rel = SlugRelative(n.Slug); return rel.StartsWith(CurrentSubPath + "/") || rel == CurrentSubPath; }); } /// Slug of a note relative to the topic root folder. /// "biosysteme/bio-regenerative-systeme/hydroponik" under topic "biosysteme" /// → "bio-regenerative-systeme/hydroponik" public string SlugRelative(string slug) { if (CurrentTopic is null) return slug; var prefix = CurrentTopic + "/"; return slug.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ? slug[prefix.Length..] : slug; } /// Immediate sub-folders at the current navigation level. /// At root: returns distinct topic names. /// Within a topic: returns slug sub-segments one level below CurrentSubPath. public IEnumerable FoldersInView() { if (CurrentTopic is null) return NoteCache .Select(n => n.Topic) .Where(t => !string.IsNullOrEmpty(t)) .Distinct(StringComparer.OrdinalIgnoreCase)!; var depth = CurrentSubPath is null ? 0 : CurrentSubPath.Split('/').Length; return NotesInView() .Where(n => SlugRelative(n.Slug).Split('/').Length > depth + 1) .Select(n => SlugRelative(n.Slug).Split('/').ElementAtOrDefault(depth)) .Where(s => s is not null) .Distinct(StringComparer.OrdinalIgnoreCase)!; } /// Navigate into a topic or sub-folder segment. public bool NavigateTo(string segment) { if (CurrentTopic is null) { // At root: segment must be a known topic var topic = NoteCache .Select(n => n.Topic) .FirstOrDefault(t => string.Equals(t, segment, StringComparison.OrdinalIgnoreCase)); if (topic is null) return false; CurrentTopic = topic; CurrentSubPath = null; return true; } // Within a topic: segment must be a valid slug sub-folder var newSub = CurrentSubPath is null ? segment : $"{CurrentSubPath}/{segment}"; var hasNotes = NoteCache .Where(n => string.Equals(n.Topic, CurrentTopic, StringComparison.OrdinalIgnoreCase)) .Any(n => { var rel = SlugRelative(n.Slug); return rel.StartsWith(newSub + "/") || rel == newSub; }); if (!hasNotes) return false; CurrentSubPath = newSub; return true; } /// Navigate up one level. public void NavigateUp() { if (CurrentSubPath is not null) { var parts = CurrentSubPath.Split('/'); CurrentSubPath = parts.Length > 1 ? string.Join('/', parts[..^1]) : null; } else { CurrentTopic = null; } } /// Resolve a short name to a full slug for note commands (cat, edit, rm). public string ResolveSlug(string nameOrSlug) { nameOrSlug = nameOrSlug.TrimEnd('/'); // Already a full known slug if (NoteCache.Any(n => n.Slug == nameOrSlug)) return nameOrSlug; if (CurrentTopic is null) return nameOrSlug; // Try reconstructing the full slug from relative name var topicPrefix = CurrentTopic + "/"; var sub = CurrentSubPath is null ? nameOrSlug : $"{CurrentSubPath}/{nameOrSlug}"; var full = topicPrefix + sub; if (NoteCache.Any(n => n.Slug == full)) return full; // Slug without topic prefix (note stored at root of noosphere) if (NoteCache.Any(n => n.Slug == sub)) return sub; return nameOrSlug; } /// Build a slug for a NEW note that doesn't exist yet. public string ResolveNewSlug(string nameOrSlug) { nameOrSlug = nameOrSlug.TrimEnd('/'); if (nameOrSlug.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) nameOrSlug = nameOrSlug[..^3]; if (nameOrSlug.Contains('/')) return nameOrSlug; if (CurrentTopic is null) return nameOrSlug; var sub = CurrentSubPath is null ? nameOrSlug : $"{CurrentSubPath}/{nameOrSlug}"; return CurrentTopic + "/" + sub; } }