diff --git a/Examples/mdv/Program.cs b/Examples/mdv/Program.cs deleted file mode 100644 index 0fb5953860..0000000000 --- a/Examples/mdv/Program.cs +++ /dev/null @@ -1,396 +0,0 @@ -// mdv — A Terminal.Gui Markdown viewer -// -// Usage: -// mdv [file2.md ...] Full-screen interactive mode (default) -// mdv --print [file2.md ...] Print mode: renders to terminal and exits - -using System.Collections.ObjectModel; -using System.CommandLine; -using System.CommandLine.Help; -using System.CommandLine.Invocation; -using System.Drawing; -using System.Reflection; -using Terminal.Gui.App; -using Terminal.Gui.Configuration; -using Terminal.Gui.Drawing; -using Terminal.Gui.Drivers; -using Terminal.Gui.Input; -using Terminal.Gui.ViewBase; -using Terminal.Gui.Views; -using TextMateSharp.Grammars; -using Command = Terminal.Gui.Input.Command; - -// ReSharper disable AccessToDisposedClosure - -// Capture the terminal dimensions before any driver or output can change them -int terminalWidth = Console.WindowWidth; -int terminalHeight = Console.WindowHeight; - -Option printOption = new ("--print") { Description = "Print mode: renders markdown to the terminal and exits." }; -printOption.Aliases.Add ("-p"); - -Option themeOption = new ("--theme") -{ - Description = $"The syntax-highlighting theme to use. Available: {string.Join (", ", Enum.GetNames ())}", - DefaultValueFactory = _ => ThemeName.DarkPlus -}; -themeOption.Aliases.Add ("-t"); - -Argument filesArgument = new ("files") -{ - Description = "One or more markdown file paths (glob patterns supported).", Arity = ArgumentArity.OneOrMore -}; - -RootCommand rootCommand = new ("mdv — A Terminal.Gui Markdown viewer") { printOption, themeOption, filesArgument }; - -// Override --help to render the embedded README.md in print mode -HelpOption helpOption = rootCommand.Options.OfType ().First (); -helpOption.Action = new MarkdownHelpAction (() => RenderMarkdown (ReadEmbeddedReadme (), ThemeName.DarkPlus, terminalWidth, terminalHeight)); - -rootCommand.SetAction (parseResult => - { - bool print = parseResult.GetValue (printOption); - ThemeName syntaxTheme = parseResult.GetValue (themeOption); - string [] filePatterns = parseResult.GetValue (filesArgument) ?? []; - - List files = ExpandFiles ([.. filePatterns]); - - if (files.Count == 0) - { - Console.Error.WriteLine ("No matching files found."); - - return; - } - - ConfigurationManager.Enable (ConfigLocations.All); - - if (print) - { - RenderMarkdown (string.Join ("\n\n---\n\n", files.Select (File.ReadAllText)), syntaxTheme, terminalWidth, terminalHeight); - } - else - { - RunFullScreen (files, syntaxTheme); - } - }); - -System.CommandLine.ParseResult parsed = rootCommand.Parse (args); - -// If there are errors, show README first, then print diagnostics underneath. -if (parsed.Errors.Count > 0) -{ - RenderMarkdown (ReadEmbeddedReadme (), ThemeName.DarkPlus, terminalWidth, terminalHeight); - - foreach (System.CommandLine.Parsing.ParseError error in parsed.Errors) - { - Console.Error.WriteLine (error.Message); - } - - return 1; -} - -return parsed.Invoke (); - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -static List ExpandFiles (List patterns) -{ - List result = []; - - foreach (string pattern in patterns) - { - if (pattern.Contains ('*') || pattern.Contains ('?')) - { - string directory = Path.GetDirectoryName (pattern) is { Length: > 0 } dir ? dir : "."; - string filePattern = Path.GetFileName (pattern); - - if (Directory.Exists (directory)) - { - result.AddRange (Directory.GetFiles (directory, filePattern)); - } - } - else if (File.Exists (pattern)) - { - result.Add (Path.GetFullPath (pattern)); - } - else - { - Console.Error.WriteLine ($"Warning: File not found: {pattern}"); - } - } - - return result; -} - -static string FormatFileSize (long bytes) -{ - string [] sizes = ["B", "KB", "MB", "GB", "TB"]; - var order = 0; - double size = bytes; - - while (size >= 1024 && order < sizes.Length - 1) - { - order++; - size /= 1024; - } - - return $"{size:0.##} {sizes [order]}"; -} - -// --------------------------------------------------------------------------- -// Print mode — render markdown content into the scrollback buffer, then exit -// --------------------------------------------------------------------------- - -static void RenderMarkdown (string markdown, ThemeName syntaxTheme, int width, int height) -{ - // Prevent the ANSI driver from trying to read/write real terminal size or capabilities, - // since we're just emitting ANSI and exiting immediately. - Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1"); - IApplication app = Application.Create (); - app.Init (DriverRegistry.Names.ANSI); - - // Use the actual terminal width (no -1 fudge factor) - app.Driver?.SetScreenSize (width, height); - - Markdown markdownView = new () - { - App = app, - SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme), - UseThemeBackground = true, - ShowCopyButtons = false, - Width = Dim.Fill (), - Height = Dim.Fill (), - Text = markdown - }; - - // Layout to get natural content height - markdownView.SetRelativeLayout (app.Screen.Size); - markdownView.Layout (); - - // Resize to the full content height but keep the terminal width - int contentHeight = markdownView.GetContentHeight (); - app.Driver?.SetScreenSize (width, contentHeight); - markdownView.SetRelativeLayout (app.Screen.Size); - - markdownView.Frame = app.Screen with { X = 0, Y = 0 }; - markdownView.Layout (); - - // Ensure the contents are clear - app.Driver?.ClearContents (); - - markdownView.Draw (); - Console.WriteLine (app.Driver?.ToAnsi ()); -} - -static string ReadEmbeddedReadme () -{ - var assembly = Assembly.GetExecutingAssembly (); - string resourceName = assembly.GetManifestResourceNames ().First (n => n.EndsWith ("README.md", StringComparison.Ordinal)); - - using Stream stream = assembly.GetManifestResourceStream (resourceName)!; - using StreamReader reader = new (stream); - - return reader.ReadToEnd (); -} - -// --------------------------------------------------------------------------- -// Full-screen mode — interactive viewer with StatusBar -// --------------------------------------------------------------------------- - -static void RunFullScreen (List files, ThemeName syntaxTheme) -{ - IApplication app = Application.Create ().Init (); - - Runnable window = new () { Title = "TUI Markdown Viewer", Width = Dim.Fill (), Height = Dim.Fill () }; - - Markdown markdownView = new () - { - Width = Dim.Fill (), - Height = Dim.Fill (1), // leave room for StatusBar - SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme) - }; - - // Vertical scrollbar is already enabled by MarkdownView constructor - markdownView.ViewportSettings |= ViewportSettingsFlags.HasHorizontalScrollBar; - - // ----------------------------------------------------------------------- - // StatusBar items (mirrors the Deepdives scenario) - // ----------------------------------------------------------------------- - - var updatingContentWidth = false; - - NumericUpDown contentWidthUpDown = new () { Value = 80 }; - - contentWidthUpDown.ValueChanging += (_, changeArgs) => - { - if (updatingContentWidth) - { - return; - } - - int newWidth = changeArgs.NewValue; - - if (newWidth < 1) - { - changeArgs.Handled = true; - - return; - } - - markdownView.SetContentWidth (newWidth); - }; - - Shortcut contentWidthShortcut = new () { CommandView = contentWidthUpDown, HelpText = "Content Width" }; - - Shortcut lineCountShortcut = new () { Title = "0 lines", MouseHighlightStates = MouseState.None, Enabled = false }; - - Shortcut fileSizeShortcut = new () { Title = "0 B", MouseHighlightStates = MouseState.None, Enabled = false }; - - Shortcut statusShortcut = new (Key.Empty, "Ready", null); - - SpinnerView spinner = new () { Style = new SpinnerStyle.Aesthetic (), Width = 8, AutoSpin = false, Visible = false }; - - Shortcut spinnerShortcut = new () { CommandView = spinner, Title = "" }; - - // ----------------------------------------------------------------------- - // MarkdownView event wiring - // ----------------------------------------------------------------------- - - markdownView.LinkClicked += (_, e) => - { - statusShortcut.Title = e.Url; - e.Handled = true; - }; - - markdownView.SubViewsLaidOut += (_, _) => { lineCountShortcut.Title = $"{markdownView.LineCount} lines"; }; - - markdownView.ViewportChanged += (_, e) => - { - if (e.NewViewport.Size == e.OldViewport.Size) - { - return; - } - - updatingContentWidth = true; - contentWidthUpDown.Value = markdownView.Viewport.Width; - updatingContentWidth = false; - }; - - // ----------------------------------------------------------------------- - // Build the StatusBar - // ----------------------------------------------------------------------- - - List statusItems = [new (Application.GetDefaultKey (Command.Quit), "Quit", window.RequestStop), contentWidthShortcut]; - - // Theme selector - DropDownList themeDropDown = new () { Value = syntaxTheme, CanFocus = false }; - - themeDropDown.ValueChanged += (_, e) => - { - if (e.Value is not { } themeName) - { - return; - } - - markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); - }; - - statusItems.Add (new Shortcut { Title = "Theme", CommandView = themeDropDown }); - - // Auto-select a light or dark syntax theme based on the terminal's actual background color. - app.Driver!.DefaultAttributeChanged += (_, e) => - { - if (e.NewValue is not { } attr) - { - return; - } - - ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); - markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); - themeDropDown.Value = autoTheme; - }; - - // Theme background toggle - CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.Checked }; - - themeBgCheckBox.ValueChanged += (_, e) => - { - markdownView.UseThemeBackground = e.NewValue == CheckState.Checked; - }; - - statusItems.Add (new Shortcut { CommandView = themeBgCheckBox }); - - statusItems.AddRange ([lineCountShortcut, fileSizeShortcut, statusShortcut, spinnerShortcut]); - - // File selector when multiple files are provided - if (files.Count > 1) - { - List fileNames = [.. files.Select (Path.GetFileName)]; - ObservableCollection fileNamesOc = new (fileNames!); - - DropDownList fileSelector = - new () { Source = new ListWrapper (fileNamesOc), ReadOnly = true, Text = fileNames [0] ?? string.Empty, Width = 30 }; - - fileSelector.ValueChanged += (_, _) => - { - string selectedName = fileSelector.Text; - int index = fileNames.IndexOf (selectedName); - - if (index < 0 || index >= files.Count) - { - return; - } - - LoadFile (files [index]); - }; - - Shortcut fileSelectorShortcut = new () { CommandView = fileSelector, HelpText = "File" }; - statusItems.Insert (1, fileSelectorShortcut); - } - - StatusBar statusBar = new (statusItems) { AlignmentModes = AlignmentModes.IgnoreFirstOrLast }; - - window.Add (markdownView, statusBar); - - //Load & Sync content-width control after initial layout - window.Initialized += (_, _) => - { - // Load the first file - LoadFile (files [0]); - updatingContentWidth = true; - contentWidthUpDown.Value = markdownView.Viewport.Width; - updatingContentWidth = false; - }; - - app.Run (window); - window.Dispose (); - app.Dispose (); - - return; - - void LoadFile (string filePath) - { - string content = File.ReadAllText (filePath); - markdownView.Text = content; - - FileInfo fileInfo = new (filePath); - fileSizeShortcut.Title = FormatFileSize (fileInfo.Length); - statusShortcut.Title = Path.GetFileName (filePath); - } -} - -// --------------------------------------------------------------------------- -// Custom help action — renders the embedded README.md via the print pipeline -// --------------------------------------------------------------------------- - -internal sealed class MarkdownHelpAction (Action renderHelp) : SynchronousCommandLineAction -{ - public override int Invoke (ParseResult parseResult) - { - renderHelp (); - - return 0; - } -} diff --git a/Examples/mdv/Properties/launchSettings.json b/Examples/mdv/Properties/launchSettings.json deleted file mode 100644 index c1461be4fa..0000000000 --- a/Examples/mdv/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "mdv": { - "commandName": "Project", - "commandLineArgs": "../../../../../docfx/docs/cursor.md" - } - } -} \ No newline at end of file diff --git a/Examples/mdv/README.md b/Examples/mdv/README.md deleted file mode 100644 index 0dd3eb969b..0000000000 --- a/Examples/mdv/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# mdv - A Terminal.Gui-based Markdown viewer - -Opens an interative TUI markdown viewer with rendered Markdown with auto scrollbars (vertical + horizontal), a **StatusBar** with Quit, Content Width control, line count, file size, status, and spinner, and a **File selector** dropdown when viewing multiple files. - -When run with the `--print` option, it renders the markdown to the terminal and exits, without launching the interactive viewer. - -Wildcards are supported: `mdv *.md`, `mdv docs/*.md`. - -## Supported Markdown Features - -- Headings (`#`, `##`, etc.) -- Paragraphs and line breaks -- Emphasis (`*italic*`, `**bold**`, `~~strikethrough~~`) -- Links (`[text](url)`) -- Images (`![alt](url)`) -- Code blocks (fenced with ```` ``` ````) -- Inline code (`` `code` ``) -- Blockquotes (`> quote`) -- Lists (ordered and unordered) -- Tables -- Horizontal rules (`---`) -- Syntax highlighting for code blocks (using ColorCode with various themes) - -## Usage - -``` -mdv [file2.md ...] # Full-screen interactive mode (default) -mdv --print [file2.md ...] # Print mode: renders to terminal and exits -mdv -t [file2.md ...] # Specify syntax-highlighting theme -mdv --help # Show this help message (Renders this README as formatted markdown) -``` - -### Examples - -```bash -# View a single file in full-screen mode (default) -mdv README.md -``` - -```bash -# Print rendered markdown to terminal and exit -mdv --print README.md -``` - -```bash -# View multiple files with a file selector dropdown -mdv *.md -``` - -```bash -# Print with a specific theme -mdv -p -t Monokai README.md -``` - -## Supported Themes (use -t or --theme) - -`AtomOneDark`, `AtomOneLight`, `Dark`, `DarkPlus` (Default), `DimmedMonokai`, `Dracula`, `HighContrastDark`, `HighContrastLight`, `KimbieDark`, `Light`, `LightPlus`, `Monokai`, `OneDark`, `QuietLight`, `Red`, `SolarizedDark`, `SolarizedLight`, `TomorrowNightBlue`, `VisualStudioDark`, `VisualStudioLight`, `SolarizedLight`, `TomorrowNightBlue`, `VisualStudioDark`, `VisualStudioLight` diff --git a/Examples/mdv/mdv.csproj b/Examples/mdv/mdv.csproj deleted file mode 100644 index 98e6fca9b7..0000000000 --- a/Examples/mdv/mdv.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - Exe - enable - latest - - - - - - - - - - - diff --git a/Terminal.sln b/Terminal.sln index 4449548c27..8196f36ae1 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -161,8 +161,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui.Analyzers.Inte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AI", "Examples\AI\AI.csproj", "{1737CFE6-456F-B41B-70D0-2F9EC6BE554F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mdv", "Examples\mdv\mdv.csproj", "{F752BB8A-7703-4D55-9639-2FBB78CBEB57}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -473,18 +471,7 @@ Global {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x64.Build.0 = Release|Any CPU {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x86.ActiveCfg = Release|Any CPU {1737CFE6-456F-B41B-70D0-2F9EC6BE554F}.Release|x86.Build.0 = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|x64.ActiveCfg = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|x64.Build.0 = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|x86.ActiveCfg = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Debug|x86.Build.0 = Debug|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|Any CPU.Build.0 = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|x64.ActiveCfg = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|x64.Build.0 = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|x86.ActiveCfg = Release|Any CPU - {F752BB8A-7703-4D55-9639-2FBB78CBEB57}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -516,7 +503,6 @@ Global {70802F77-F259-44C6-9522-46FCE2FD754E} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {3116547F-A8F2-4189-BC22-0B47C757164C} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {1737CFE6-456F-B41B-70D0-2F9EC6BE554F} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} - {F752BB8A-7703-4D55-9639-2FBB78CBEB57} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03} diff --git a/docfx/docs/showcase.md b/docfx/docs/showcase.md index b46fab13a8..02f6e36682 100644 --- a/docfx/docs/showcase.md +++ b/docfx/docs/showcase.md @@ -35,6 +35,8 @@ - **[TermKeyVault](https://github.com/MaciekWin3/TermKeyVault)** - Terminal based password manager built with F# and Terminal.Gui. ![TermKeyVault](https://github.com/user-attachments/assets/c40e17ed-2614-4ad4-8547-e93c1b1d8937) +- **[mdv](https://github.com/gui-cs/mdv)** - A Terminal.Gui-based Markdown viewer for the terminal. Supports full-screen interactive mode and print-to-terminal mode. + ## Examples - **[See all Examples here](https://github.com/gui-cs/Terminal.Gui/tree/master/Examples)** \ No newline at end of file