#:package Spectre.Console@0.50.0 using Spectre.Console; using Spectre.Console.Rendering; using System.Threading.Channels; // Entry point var app = new ConsoleApp(); app.Render(new App()); // Example Counter Component public class Counter : Component, IInputHandler { public Counter(Dictionary? props = null) : base(props) { // No state initialization in constructor } public override void ComponentDidMount() { // Move initial state setup here SetState("count", GetProp("initialCount", 0)); } private void Increment() { var count = GetState("count") ?? 0; SetState("count", count + 1); } private void Decrement() { var count = GetState("count") ?? 0; SetState("count", count - 1); } public void HandleInput(ConsoleKeyInfo keyInfo) { switch (keyInfo.KeyChar) { case 'i': Increment(); break; case 'd': Decrement(); break; case 'q': Environment.Exit(0); break; } } public override object Render() { var count = GetState("count") ?? 0; var title = GetProp("title") ?? "Counter"; var content = Layout.Rows( new Markup($"[bold]Current Count: [green]{count}[/][/]"), new Markup(""), new Markup("[dim]Press 'i' to increment, 'd' to decrement, 'q' to quit[/]") ); return Layout.Panel(title, content, Color.Blue); } } // Example Dashboard Component public class Dashboard : Component, IInputHandler { public Dashboard(Dictionary? props = null) : base(props) { // No state initialization in constructor } public override void ComponentDidMount() { // Move initial state setup here SetState("selectedTab", 0); } public void HandleInput(ConsoleKeyInfo keyInfo) { var selectedTab = GetState("selectedTab") ?? 0; var tabs = new[] { "Home", "Stats", "Settings" }; switch (keyInfo.Key) { case ConsoleKey.LeftArrow: SetState("selectedTab", Math.Max(0, selectedTab - 1)); break; case ConsoleKey.RightArrow: SetState("selectedTab", Math.Min(tabs.Length - 1, selectedTab + 1)); break; } } public override object Render() { var selectedTab = GetState("selectedTab") ?? 0; var tabs = new[] { "Home", "Stats", "Settings" }; var tabContent = selectedTab switch { 0 => new Markup("[green]Welcome to the Dashboard![/]\n\nThis is the home tab."), 1 => new Markup("[yellow]Statistics[/]\n\nUsers: 1,234\nSessions: 5,678"), 2 => new Markup("[blue]Settings[/]\n\nTheme: Dark\nLanguage: English"), _ => new Markup("Unknown tab") }; // Create tab headers var tabHeaders = tabs.Select((tab, index) => index == selectedTab ? $"[bold blue on white] {tab} [/]" : $"[dim] {tab} [/]" ); var dashboard = Layout.Rows( new Markup(string.Join(" ", tabHeaders)), Layout.Rule(), tabContent, Layout.Rule(), new Markup("[dim]Use ← → arrows to navigate tabs, 'q' to quit[/]") ); return Layout.Panel("Dashboard", dashboard, Color.Blue); } } // Example Todo List Component public class TodoList : Component, IInputHandler { public TodoList(Dictionary? props = null) : base(props) { // No state initialization in constructor } public override void ComponentDidMount() { // Move initial state setup here SetState("todos", new List<(string text, bool completed)> { ("Learn React-like patterns", true), ("Build console framework", false), ("Create awesome apps", false) }); SetState("inputMode", false); SetState("newTodo", ""); } public void HandleInput(ConsoleKeyInfo keyInfo) { var inputMode = GetState("inputMode") ?? false; var newTodo = GetState("newTodo") ?? ""; var currentTodos = GetState>("todos") ?? new List<(string, bool)>(); if (inputMode) { if (keyInfo.Key == ConsoleKey.Enter && !string.IsNullOrWhiteSpace(newTodo)) { currentTodos.Add((newTodo, false)); SetState("todos", currentTodos); SetState("inputMode", false); SetState("newTodo", ""); } else if (keyInfo.Key == ConsoleKey.Escape) { SetState("inputMode", false); SetState("newTodo", ""); } else if (keyInfo.Key == ConsoleKey.Backspace && newTodo.Length > 0) { SetState("newTodo", newTodo.Substring(0, newTodo.Length - 1)); } else if (char.IsLetterOrDigit(keyInfo.KeyChar) || char.IsPunctuation(keyInfo.KeyChar) || keyInfo.KeyChar == ' ') { SetState("newTodo", newTodo + keyInfo.KeyChar); } } else { switch (keyInfo.KeyChar) { case 'a': SetState("inputMode", true); break; case 't': for (int i = 0; i < currentTodos.Count; i++) { if (!currentTodos[i].Item2) { currentTodos[i] = (currentTodos[i].Item1, true); SetState("todos", currentTodos); break; } } break; } } } public override object Render() { var todos = GetState>("todos") ?? new List<(string, bool)>(); var inputMode = GetState("inputMode") ?? false; var newTodo = GetState("newTodo") ?? ""; var todoItems = todos.Select((todo, index) => { var checkbox = todo.Item2 ? "[green]✓[/]" : "[ ]"; var text = todo.Item2 ? $"[dim strikethrough]{todo.Item1}[/]" : todo.Item1; return new Markup($"{checkbox} {text}"); }).Cast().ToArray(); var content = new List(); content.AddRange(todoItems); if (inputMode) { content.Add(new Markup("")); content.Add(new Markup($"[yellow]New todo: {newTodo}_[/]")); content.Add(new Markup("[dim]Type your todo, Enter to add, Esc to cancel[/]")); } else { content.Add(new Markup("")); content.Add(new Markup("[dim]Press 'a' to add todo, 't' to toggle first incomplete[/]")); } return Layout.Panel("Todo List", Layout.Rows(content.ToArray()), Color.Green); } } // Example App Component that combines multiple components public class App : Component, IInputHandler { public App() : base() { SetState("currentView", "counter"); } private Component? _activeChild; public void HandleInput(ConsoleKeyInfo keyInfo) { switch (keyInfo.KeyChar) { case '1': SetState("currentView", "counter"); _activeChild = new Counter(new Dictionary { { "title", "My Counter" }, { "initialCount", 5 } }); break; case '2': SetState("currentView", "dashboard"); _activeChild = new Dashboard(); break; case '3': SetState("currentView", "todo"); _activeChild = new TodoList(); break; case 'q': Environment.Exit(0); break; } (_activeChild as IInputHandler)?.HandleInput(keyInfo); } public override void ComponentDidMount() { // Set initial view and create initial component SetState("currentView", "counter"); _activeChild = new Counter(new Dictionary { { "title", "My Counter" }, { "initialCount", 5 } }); _activeChild.ComponentDidMount(); } public override object Render() { var currentView = GetState("currentView") ?? "counter"; // Ensure we have an active child component if (_activeChild == null) { _activeChild = currentView switch { "counter" => new Counter(new Dictionary { { "title", "My Counter" }, { "initialCount", 5 } }), "dashboard" => new Dashboard(), "todo" => new TodoList(), _ => null }; if (_activeChild != null) { _activeChild.ComponentDidMount(); } } object currentComponent = _activeChild?.Render() ?? new Markup("[red]Unknown view[/]"); var navigation = new Markup("[bold]Navigation:[/] [blue]1[/] Counter | [blue]2[/] Dashboard | [blue]3[/] Todo | [blue]q[/] Quit"); return Layout.Rows( Layout.Panel("React-like Console Framework", navigation, Color.Yellow), currentComponent ); } } // Base Component class - similar to React.Component public abstract class Component { protected Dictionary State { get; private set; } = new(); protected Dictionary Props { get; private set; } = new(); public bool ShouldRerender { get; protected set; } = true; public Component(Dictionary? props = null) { Props = props ?? new Dictionary(); } // setState equivalent protected void SetState(Dictionary newState) { foreach (var kvp in newState) { State[kvp.Key] = kvp.Value; } ShouldRerender = true; ConsoleApp.Instance?.ScheduleRerender(); } protected void SetState(string key, object? value) { State[key] = value; ShouldRerender = true; ConsoleApp.Instance?.ScheduleRerender(); } protected T? GetState(string key, T? defaultValue = default(T)) { return State.ContainsKey(key) ? (T?)State[key] : defaultValue; } protected T? GetProp(string key, T? defaultValue = default(T)) { return Props.ContainsKey(key) ? (T?)Props[key] : defaultValue; } // Lifecycle methods public virtual void ComponentDidMount() { } public virtual void ComponentWillUnmount() { } public virtual bool ShouldComponentUpdate() => ShouldRerender; // Render method - must be implemented by subclasses public abstract object Render(); // Reset rerender flag after rendering public void MarkAsRendered() { ShouldRerender = false; } } // Hook-like functionality for functional components public static class Hooks { private static Dictionary _state = new(); private static Dictionary> _effects = new(); public static (T? value, Action setValue) UseState(string key, T? initialValue = default(T)) { if (!_state.ContainsKey(key)) { _state[key] = initialValue; } return ((T?)_state[key], (newValue) => { _state[key] = newValue; ConsoleApp.Instance?.ScheduleRerender(); } ); } public static void UseEffect(string key, Action effect, object[]? dependencies = null) { if (!_effects.ContainsKey(key)) { _effects[key] = new List(); } _effects[key].Add(effect); // For simplicity, we'll run effects immediately // In a real implementation, you'd track dependencies effect(); } } // Main App class - similar to ReactDOM public class ConsoleApp { public static ConsoleApp? Instance { get; private set; } private Component? _rootComponent; private LiveDisplayContext? _liveContext; private readonly Channel _inputChannel; private readonly ChannelWriter _inputWriter; private readonly ChannelReader _inputReader; private CancellationTokenSource _cancellationTokenSource = new(); public ConsoleApp() { Instance = this; _inputChannel = Channel.CreateUnbounded(); _inputWriter = _inputChannel.Writer; _inputReader = _inputChannel.Reader; } public void Render(Component component) { _rootComponent = component; _rootComponent.ComponentDidMount(); // Start input thread var inputTask = Task.Run(InputLoop, _cancellationTokenSource.Token); StartLiveDisplay(); } public void ScheduleRerender() { UpdateDisplay(); } private void StartLiveDisplay() { if (_rootComponent == null) { AnsiConsole.WriteLine("Error: Root component is null!"); return; } AnsiConsole.WriteLine("Rendering root component..."); var initialRenderable = _rootComponent.Render() as IRenderable; if (initialRenderable == null) { AnsiConsole.WriteLine("Error: Component render returned null!"); initialRenderable = new Markup("Error rendering component"); } else { AnsiConsole.WriteLine("Component rendered successfully!"); } AnsiConsole.Live(initialRenderable) .AutoClear(false) .Start(ctx => { _liveContext = ctx; AnsiConsole.WriteLine("Live context started!"); // Start input on background thread Task.Run(async () => await InputLoop()); // Process input on main thread ProcessInputLoop(); }); } private void UpdateDisplay() { if (_liveContext == null || _rootComponent == null) return; var renderable = _rootComponent.Render() as IRenderable ?? new Markup("Error rendering component"); _liveContext.UpdateTarget(renderable); _rootComponent.MarkAsRendered(); } private async Task InputLoop() { try { while (!_cancellationTokenSource.Token.IsCancellationRequested) { if (Console.KeyAvailable) { var keyInfo = Console.ReadKey(true); await _inputWriter.WriteAsync(keyInfo, _cancellationTokenSource.Token); } await Task.Delay(10, _cancellationTokenSource.Token); // Small delay to prevent busy wait } } catch (OperationCanceledException) { // Expected when cancellation is requested } } private void ProcessInputLoop() { try { while (!_cancellationTokenSource.Token.IsCancellationRequested) { if (_inputReader.TryRead(out var keyInfo)) { // Handle global input if (keyInfo.KeyChar == 'q' && keyInfo.Modifiers == ConsoleModifiers.Control) { Environment.Exit(0); } // Pass input to component if (_rootComponent != null) { (_rootComponent as IInputHandler)?.HandleInput(keyInfo); UpdateDisplay(); } } Thread.Sleep(10); // Small delay to prevent busy wait } } catch (OperationCanceledException) { // Expected when cancellation is requested } } } // Interface for components that handle input public interface IInputHandler { void HandleInput(ConsoleKeyInfo keyInfo); } // Layout components public static class Layout { public static Panel Panel(string title, object content, Color? borderColor = null) { IRenderable renderableContent = content switch { IRenderable renderable => renderable, string text => new Markup(text), _ => new Markup(content?.ToString() ?? "") }; var panel = new Panel(renderableContent) .Header(title) .BorderColor(borderColor ?? Color.Blue); return panel; } public static Rows Rows(params object[] children) { var renderables = children.Select(child => child switch { IRenderable renderable => renderable, string text => new Markup(text), _ => new Markup(child?.ToString() ?? "") }); return new Rows(renderables); } public static Columns Columns(params object[] children) { var renderables = children.Select(child => child switch { IRenderable renderable => renderable, string text => new Markup(text), _ => new Markup(child?.ToString() ?? "") }); return new Columns(renderables); } public static Rule Rule(string text = "") { return new Rule(text); } }