diff --git a/docs/neo-tui/README.md b/docs/neo-tui/README.md new file mode 100644 index 000000000..d84075e60 --- /dev/null +++ b/docs/neo-tui/README.md @@ -0,0 +1,71 @@ +# neo-tui + +Cross-platform console UI for **neo-cli** (Windows, Linux, macOS). + +It starts the same `MainService` node as `neo-cli`, loads plugins from `Plugins/` the same way, then shows the live **show state** dashboard. + +- Live **node status** (block height, mempool, peers, sync) — same data as `show state` +- **CONNECTED PEERS** from `LocalNode` (Up/Down to scroll when there are more than five) +- Command **output popup** on this console with **Please wait** until the command finishes, then **Rerun command** or **Close** +- Category **menus** (`M` / Enter from the status screen) +- **Command palette** of every `[ConsoleCommand]`, including plugin commands +- **Line editor** with history, Tab complete, Ctrl+A/E/U +- Direct `System.CommandLine` subcommands (`neo-tui help`, `neo-tui show block 1`, …) + +## Run (from repo) + +```bash +dotnet run --project src/Neo.ConsoleUI +``` + +Or after build: + +```bash +dotnet build src/Neo.ConsoleUI/Neo.ConsoleUI.csproj +# output: src/Neo.ConsoleUI/bin/Debug/net10.0/neo-tui.dll +dotnet src/Neo.ConsoleUI/bin/Debug/net10.0/neo-tui.dll +``` + +Startup flags match neo-cli: `--config`/`-c`, `--wallet`/`-w`, `--password`/`-p`, `--db-engine`, `--db-path`, `--plugins`, `--noverify`. + +The working directory is the exe folder so `Plugins/`, `config.json`, and auto-install zips resolve the same way as neo-cli. On build, each plugin is copied next to the binary: `bin/$(Configuration)/$(TargetFramework)/Plugins//` (the folder is created if it is missing). Extra plugins can still be installed at runtime (`--plugins RpcServer` or the `install` command). + +## Screenshots + +Wide status dashboard — 2×2 metrics, sync bar, **CONNECTED PEERS** on the right (title on the time row, table aligned with BLOCKCHAIN STATUS): + +![Wide status with connected peers](status-wide.png) + +Peer list scrolled (`6-10/10`): + +![Status with peers scrolled](status-peers-scroll.png) + +Narrow window — peers drop below the 2×2: + +![Narrow status layout](status-narrow.png) + +Command menu (`M` / Enter): + +![Command menu](menu.png) + +Plugin commands: + +![Plugin commands](plugin-commands.png) + +Please wait while a command runs: + +![Please wait](please-wait.png) + +Command output popup — **Rerun command** / **Close**: + +![plugins output](command-plugins.png) + +![log block 1 output](command-log-block.png) + +Menu command parameters: + +![Command parameters](command-params.png) + +Command line (`C`) — same `neo>` prompt as neo-cli: + +![Command line](command-clear.png) diff --git a/docs/neo-tui/command-clear.png b/docs/neo-tui/command-clear.png new file mode 100644 index 000000000..755ea2abb Binary files /dev/null and b/docs/neo-tui/command-clear.png differ diff --git a/docs/neo-tui/command-log-block.png b/docs/neo-tui/command-log-block.png new file mode 100644 index 000000000..90fc5b6d8 Binary files /dev/null and b/docs/neo-tui/command-log-block.png differ diff --git a/docs/neo-tui/command-params.png b/docs/neo-tui/command-params.png new file mode 100644 index 000000000..d8db725c0 Binary files /dev/null and b/docs/neo-tui/command-params.png differ diff --git a/docs/neo-tui/command-plugins.png b/docs/neo-tui/command-plugins.png new file mode 100644 index 000000000..3c11d1a1a Binary files /dev/null and b/docs/neo-tui/command-plugins.png differ diff --git a/docs/neo-tui/menu.png b/docs/neo-tui/menu.png new file mode 100644 index 000000000..dfd879be3 Binary files /dev/null and b/docs/neo-tui/menu.png differ diff --git a/docs/neo-tui/please-wait.png b/docs/neo-tui/please-wait.png new file mode 100644 index 000000000..777fc5f10 Binary files /dev/null and b/docs/neo-tui/please-wait.png differ diff --git a/docs/neo-tui/plugin-commands.png b/docs/neo-tui/plugin-commands.png new file mode 100644 index 000000000..3356d591d Binary files /dev/null and b/docs/neo-tui/plugin-commands.png differ diff --git a/docs/neo-tui/status-narrow.png b/docs/neo-tui/status-narrow.png new file mode 100644 index 000000000..718b94fba Binary files /dev/null and b/docs/neo-tui/status-narrow.png differ diff --git a/docs/neo-tui/status-peers-scroll.png b/docs/neo-tui/status-peers-scroll.png new file mode 100644 index 000000000..738092f57 Binary files /dev/null and b/docs/neo-tui/status-peers-scroll.png differ diff --git a/docs/neo-tui/status-wide.png b/docs/neo-tui/status-wide.png new file mode 100644 index 000000000..5c5b351ac Binary files /dev/null and b/docs/neo-tui/status-wide.png differ diff --git a/neo-node.sln b/neo-node.sln index 729012297..05313e8e8 100644 --- a/neo-node.sln +++ b/neo-node.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11201.2 @@ -73,140 +73,414 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.Storage.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.DeferredRelay.Tests", "tests\Neo.Plugins.DeferredRelay.Tests\Neo.Plugins.DeferredRelay.Tests.csproj", "{8F4BAC23-5C6E-7F90-B012-34567890BCDE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.ConsoleUI", "src\Neo.ConsoleUI\Neo.ConsoleUI.csproj", "{9B429DB4-922F-460E-9A6A-1541FCAA866C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|x64.Build.0 = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Debug|x86.Build.0 = Debug|Any CPU {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|Any CPU.Build.0 = Release|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|x64.ActiveCfg = Release|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|x64.Build.0 = Release|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|x86.ActiveCfg = Release|Any CPU + {900CA179-AEF0-43F3-9833-5DB060272D8E}.Release|x86.Build.0 = Release|Any CPU {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|x64.Build.0 = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Debug|x86.Build.0 = Debug|Any CPU {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|Any CPU.Build.0 = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|x64.ActiveCfg = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|x64.Build.0 = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|x86.ActiveCfg = Release|Any CPU + {1CF672B6-B5A1-47D2-8CE9-C54BC05FA6E7}.Release|x86.Build.0 = Release|Any CPU {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|x64.Build.0 = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Debug|x86.Build.0 = Debug|Any CPU {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.ActiveCfg = Release|Any CPU {CC845558-D7C2-412D-8014-15699DFBA530}.Release|Any CPU.Build.0 = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|x64.ActiveCfg = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|x64.Build.0 = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|x86.ActiveCfg = Release|Any CPU + {CC845558-D7C2-412D-8014-15699DFBA530}.Release|x86.Build.0 = Release|Any CPU {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|x64.Build.0 = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Debug|x86.Build.0 = Debug|Any CPU {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|Any CPU.Build.0 = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|x64.ActiveCfg = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|x64.Build.0 = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|x86.ActiveCfg = Release|Any CPU + {8D2BC669-11AC-42DB-BE75-FD53FA2475C6}.Release|x86.Build.0 = Release|Any CPU {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|x64.ActiveCfg = Debug|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|x64.Build.0 = Debug|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|x86.ActiveCfg = Debug|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Debug|x86.Build.0 = Debug|Any CPU {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|Any CPU.Build.0 = Release|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|x64.ActiveCfg = Release|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|x64.Build.0 = Release|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|x86.ActiveCfg = Release|Any CPU + {B12C3400-F3E0-DDEF-D272-4C2FF1FF2E8B}.Release|x86.Build.0 = Release|Any CPU {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|x64.Build.0 = Debug|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Debug|x86.Build.0 = Debug|Any CPU {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|Any CPU.Build.0 = Release|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|x64.ActiveCfg = Release|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|x64.Build.0 = Release|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|x86.ActiveCfg = Release|Any CPU + {CD0F1C4D-977C-8B51-A623-083D15776E5E}.Release|x86.Build.0 = Release|Any CPU {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|x64.ActiveCfg = Debug|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|x64.Build.0 = Debug|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|x86.ActiveCfg = Debug|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Debug|x86.Build.0 = Debug|Any CPU {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|Any CPU.ActiveCfg = Release|Any CPU {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|Any CPU.Build.0 = Release|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|x64.ActiveCfg = Release|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|x64.Build.0 = Release|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|x86.ActiveCfg = Release|Any CPU + {538500ED-2C8B-3174-BA9C-98A35024F32B}.Release|x86.Build.0 = Release|Any CPU {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|x64.ActiveCfg = Debug|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|x64.Build.0 = Debug|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|x86.ActiveCfg = Debug|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Debug|x86.Build.0 = Debug|Any CPU {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|Any CPU.ActiveCfg = Release|Any CPU {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|Any CPU.Build.0 = Release|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|x64.ActiveCfg = Release|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|x64.Build.0 = Release|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|x86.ActiveCfg = Release|Any CPU + {7129EDEA-C8B3-E574-C1FB-C58226873860}.Release|x86.Build.0 = Release|Any CPU {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|x64.Build.0 = Debug|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Debug|x86.Build.0 = Debug|Any CPU {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|Any CPU.Build.0 = Release|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|x64.ActiveCfg = Release|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|x64.Build.0 = Release|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|x86.ActiveCfg = Release|Any CPU + {D554C319-B2F5-8F95-C29C-2EACC34F7FC7}.Release|x86.Build.0 = Release|Any CPU {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|x64.Build.0 = Debug|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Debug|x86.Build.0 = Debug|Any CPU {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|Any CPU.ActiveCfg = Release|Any CPU {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|Any CPU.Build.0 = Release|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|x64.ActiveCfg = Release|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|x64.Build.0 = Release|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|x86.ActiveCfg = Release|Any CPU + {13243D40-CC80-7017-7C4B-898B07AA67EA}.Release|x86.Build.0 = Release|Any CPU {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|x64.Build.0 = Debug|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Debug|x86.Build.0 = Debug|Any CPU {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|Any CPU.Build.0 = Release|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|x64.ActiveCfg = Release|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|x64.Build.0 = Release|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|x86.ActiveCfg = Release|Any CPU + {FF12A836-AE0D-A788-C6A8-15F020597130}.Release|x86.Build.0 = Release|Any CPU {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|x64.Build.0 = Debug|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Debug|x86.Build.0 = Debug|Any CPU {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|Any CPU.Build.0 = Release|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|x64.ActiveCfg = Release|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|x64.Build.0 = Release|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|x86.ActiveCfg = Release|Any CPU + {E6E8300F-3A1C-8A89-E44B-1DE8AA3F2D34}.Release|x86.Build.0 = Release|Any CPU {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|x64.Build.0 = Debug|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Debug|x86.Build.0 = Debug|Any CPU {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|Any CPU.Build.0 = Release|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|x64.ActiveCfg = Release|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|x64.Build.0 = Release|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|x86.ActiveCfg = Release|Any CPU + {1F8D9015-2CE7-9F58-9F4B-94D0924FA82E}.Release|x86.Build.0 = Release|Any CPU {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|x64.ActiveCfg = Debug|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|x64.Build.0 = Debug|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|x86.ActiveCfg = Debug|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Debug|x86.Build.0 = Debug|Any CPU {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|Any CPU.ActiveCfg = Release|Any CPU {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|Any CPU.Build.0 = Release|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|x64.ActiveCfg = Release|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|x64.Build.0 = Release|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|x86.ActiveCfg = Release|Any CPU + {295FE1B7-BFBF-3390-3080-997DAF8EFE80}.Release|x86.Build.0 = Release|Any CPU {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|x64.Build.0 = Debug|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Debug|x86.Build.0 = Debug|Any CPU {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|Any CPU.Build.0 = Release|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|x64.ActiveCfg = Release|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|x64.Build.0 = Release|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|x86.ActiveCfg = Release|Any CPU + {A2CF46E9-C584-9E94-7BF9-38C33F83795E}.Release|x86.Build.0 = Release|Any CPU {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|x64.Build.0 = Debug|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Debug|x86.Build.0 = Debug|Any CPU {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|Any CPU.Build.0 = Release|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|x64.ActiveCfg = Release|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|x64.Build.0 = Release|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|x86.ActiveCfg = Release|Any CPU + {24DA3784-5D2E-648F-771E-193CF0B3D6FF}.Release|x86.Build.0 = Release|Any CPU {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|x64.Build.0 = Debug|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Debug|x86.Build.0 = Debug|Any CPU {C0327365-D644-C32A-1AEF-B7004899339B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0327365-D644-C32A-1AEF-B7004899339B}.Release|Any CPU.Build.0 = Release|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Release|x64.ActiveCfg = Release|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Release|x64.Build.0 = Release|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Release|x86.ActiveCfg = Release|Any CPU + {C0327365-D644-C32A-1AEF-B7004899339B}.Release|x86.Build.0 = Release|Any CPU {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|x64.Build.0 = Debug|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Debug|x86.Build.0 = Debug|Any CPU {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|Any CPU.Build.0 = Release|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|x64.ActiveCfg = Release|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|x64.Build.0 = Release|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|x86.ActiveCfg = Release|Any CPU + {6D1FE94A-0769-61C6-D870-B32919AE3881}.Release|x86.Build.0 = Release|Any CPU {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|x64.Build.0 = Debug|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Debug|x86.Build.0 = Debug|Any CPU {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|Any CPU.Build.0 = Release|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|x64.ActiveCfg = Release|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|x64.Build.0 = Release|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|x86.ActiveCfg = Release|Any CPU + {AF8E770D-07F7-CD2A-6F59-88A5AC52EC34}.Release|x86.Build.0 = Release|Any CPU {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|x64.Build.0 = Debug|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Debug|x86.Build.0 = Debug|Any CPU {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|Any CPU.Build.0 = Release|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|x64.ActiveCfg = Release|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|x64.Build.0 = Release|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|x86.ActiveCfg = Release|Any CPU + {7E3A9C12-4B5D-6E8F-A901-23456789ABCD}.Release|x86.Build.0 = Release|Any CPU {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|x64.Build.0 = Debug|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Debug|x86.Build.0 = Debug|Any CPU {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|Any CPU.Build.0 = Release|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|x64.ActiveCfg = Release|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|x64.Build.0 = Release|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|x86.ActiveCfg = Release|Any CPU + {1A6EB5BA-2FCD-3056-1C01-FC6FA24EF09C}.Release|x86.Build.0 = Release|Any CPU {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|x64.ActiveCfg = Debug|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|x64.Build.0 = Debug|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|x86.ActiveCfg = Debug|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Debug|x86.Build.0 = Debug|Any CPU {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|Any CPU.ActiveCfg = Release|Any CPU {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|Any CPU.Build.0 = Release|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|x64.ActiveCfg = Release|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|x64.Build.0 = Release|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|x86.ActiveCfg = Release|Any CPU + {60E16A49-06EB-6F80-7228-21A7793B8250}.Release|x86.Build.0 = Release|Any CPU {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|x64.Build.0 = Debug|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Debug|x86.Build.0 = Debug|Any CPU {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|x64.ActiveCfg = Release|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|x64.Build.0 = Release|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|x86.ActiveCfg = Release|Any CPU + {8C5DFE38-01CC-DF44-54DF-D2D67D5AB30E}.Release|x86.Build.0 = Release|Any CPU {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|x64.Build.0 = Debug|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Debug|x86.Build.0 = Debug|Any CPU {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|Any CPU.Build.0 = Release|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|x64.ActiveCfg = Release|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|x64.Build.0 = Release|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|x86.ActiveCfg = Release|Any CPU + {5B0634E5-484B-E9F7-02D5-F97D36BE88EF}.Release|x86.Build.0 = Release|Any CPU {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|x64.Build.0 = Debug|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Debug|x86.Build.0 = Debug|Any CPU {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|Any CPU.Build.0 = Release|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|x64.ActiveCfg = Release|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|x64.Build.0 = Release|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|x86.ActiveCfg = Release|Any CPU + {6B77BE88-212C-D04B-3C5A-169E38443777}.Release|x86.Build.0 = Release|Any CPU {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|x64.Build.0 = Debug|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Debug|x86.Build.0 = Debug|Any CPU {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|Any CPU.Build.0 = Release|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|x64.ActiveCfg = Release|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|x64.Build.0 = Release|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|x86.ActiveCfg = Release|Any CPU + {A7FE2B30-11F8-E88D-D5BF-AF1B11EFEC8E}.Release|x86.Build.0 = Release|Any CPU {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|x64.Build.0 = Debug|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Debug|x86.Build.0 = Debug|Any CPU {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|Any CPU.Build.0 = Release|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|x64.ActiveCfg = Release|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|x64.Build.0 = Release|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|x86.ActiveCfg = Release|Any CPU + {D65E9D81-A7B0-696C-11F1-E923CAD972B3}.Release|x86.Build.0 = Release|Any CPU {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|x64.Build.0 = Debug|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Debug|x86.Build.0 = Debug|Any CPU {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|Any CPU.Build.0 = Release|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|x64.ActiveCfg = Release|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|x64.Build.0 = Release|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|x86.ActiveCfg = Release|Any CPU + {F1088B9A-BEE5-CB9D-8B6B-70CE4CAD8E46}.Release|x86.Build.0 = Release|Any CPU {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|x64.ActiveCfg = Debug|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|x64.Build.0 = Debug|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|x86.ActiveCfg = Debug|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Debug|x86.Build.0 = Debug|Any CPU {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|Any CPU.ActiveCfg = Release|Any CPU {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|Any CPU.Build.0 = Release|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|x64.ActiveCfg = Release|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|x64.Build.0 = Release|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|x86.ActiveCfg = Release|Any CPU + {099504A6-0275-8DA3-D52C-06726AD8E77D}.Release|x86.Build.0 = Release|Any CPU {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|x64.Build.0 = Debug|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Debug|x86.Build.0 = Debug|Any CPU {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|Any CPU.Build.0 = Release|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|x64.ActiveCfg = Release|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|x64.Build.0 = Release|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|x86.ActiveCfg = Release|Any CPU + {834D4327-7DDF-4D6A-1624-B074700964F0}.Release|x86.Build.0 = Release|Any CPU {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|x64.Build.0 = Debug|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Debug|x86.Build.0 = Debug|Any CPU {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|Any CPU.Build.0 = Release|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|x64.ActiveCfg = Release|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|x64.Build.0 = Release|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|x86.ActiveCfg = Release|Any CPU + {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9}.Release|x86.Build.0 = Release|Any CPU {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|x64.Build.0 = Debug|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Debug|x86.Build.0 = Debug|Any CPU {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|Any CPU.Build.0 = Release|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|x64.ActiveCfg = Release|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|x64.Build.0 = Release|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|x86.ActiveCfg = Release|Any CPU + {8F4BAC23-5C6E-7F90-B012-34567890BCDE}.Release|x86.Build.0 = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|x64.ActiveCfg = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|x64.Build.0 = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|x86.ActiveCfg = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Debug|x86.Build.0 = Debug|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|Any CPU.Build.0 = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|x64.ActiveCfg = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|x64.Build.0 = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|x86.ActiveCfg = Release|Any CPU + {9B429DB4-922F-460E-9A6A-1541FCAA866C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -244,6 +518,7 @@ Global {834D4327-7DDF-4D6A-1624-B074700964F0} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9} {52D6F4D3-8AC9-DEA4-1D6F-FAF6943EE3D9} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9} {8F4BAC23-5C6E-7F90-B012-34567890BCDE} = {62F4DC79-BE3D-4E60-B402-8D5F9C4BB2D9} + {9B429DB4-922F-460E-9A6A-1541FCAA866C} = {705EBADA-05F7-45D1-9D63-D399E87525DB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6C1293A1-8EC4-44E8-9EE9-67892696FE26} diff --git a/plugins/Directory.Build.props b/plugins/Directory.Build.props index b22096bb9..0fbde6a60 100644 --- a/plugins/Directory.Build.props +++ b/plugins/Directory.Build.props @@ -28,4 +28,31 @@ + + + true + + + + + <_CliTargetDir>$(MSBuildThisFileDirectory)..\src\Neo.CLI\bin\$(Configuration)\$(TargetFramework)\ + <_TuiTargetDir>$(MSBuildThisFileDirectory)..\src\Neo.ConsoleUI\bin\$(Configuration)\$(TargetFramework)\ + <_CliPlugins>$(_CliTargetDir)Plugins\$(MSBuildProjectName)\ + <_TuiPlugins>$(_TuiTargetDir)Plugins\$(MSBuildProjectName)\ + + + + <_AllDll Include="$(TargetDir)*.dll" /> + <_PluginCopyCli Include="@(_AllDll)" Condition="'%(Filename)' == '$(MSBuildProjectName)' OR (!Exists('$(_CliTargetDir)%(Filename)%(Extension)') AND !Exists('$(MSBuildThisFileDirectory)%(Filename)\%(Filename).csproj'))" /> + <_PluginCopyTui Include="@(_AllDll)" Condition="'%(Filename)' == '$(MSBuildProjectName)' OR (!Exists('$(_TuiTargetDir)%(Filename)%(Extension)') AND !Exists('$(MSBuildThisFileDirectory)%(Filename)\%(Filename).csproj'))" /> + <_PluginCopyCli Include="$(TargetDir)$(MSBuildProjectName).json" Condition="Exists('$(TargetDir)$(MSBuildProjectName).json')" /> + <_PluginCopyTui Include="$(TargetDir)$(MSBuildProjectName).json" Condition="Exists('$(TargetDir)$(MSBuildProjectName).json')" /> + <_PluginRuntime Include="$(TargetDir)runtimes\**\*" /> + + + + + + + diff --git a/plugins/MPTTrie/MPTTrie.csproj b/plugins/MPTTrie/MPTTrie.csproj index 852671a0a..344b4d004 100644 --- a/plugins/MPTTrie/MPTTrie.csproj +++ b/plugins/MPTTrie/MPTTrie.csproj @@ -4,6 +4,7 @@ Neo.Cryptography.MPT Neo.Cryptography.MPTTrie enable + false diff --git a/plugins/RpcClient/RpcClient.csproj b/plugins/RpcClient/RpcClient.csproj index 21dee2dd8..9c0615bd4 100644 --- a/plugins/RpcClient/RpcClient.csproj +++ b/plugins/RpcClient/RpcClient.csproj @@ -3,6 +3,7 @@ Neo.Network.RPC.RpcClient Neo.Network.RPC + false diff --git a/src/CopyNodePlugins.targets b/src/CopyNodePlugins.targets new file mode 100644 index 000000000..00080869f --- /dev/null +++ b/src/CopyNodePlugins.targets @@ -0,0 +1,50 @@ + + + + <_PluginRoot>$(MSBuildThisFileDirectory)..\plugins + <_PluginOut>$(TargetDir)Plugins + + + <_PluginAllDll Include="$(_PluginRoot)\%(NodePlugin.Identity)\bin\$(Configuration)\$(TargetFramework)\*.dll"> + %(NodePlugin.Identity) + + <_PluginJson Include="$(_PluginRoot)\%(NodePlugin.Identity)\bin\$(Configuration)\$(TargetFramework)\%(NodePlugin.Identity).json"> + %(NodePlugin.Identity) + + <_PluginRuntime Include="$(_PluginRoot)\%(NodePlugin.Identity)\bin\$(Configuration)\$(TargetFramework)\runtimes\**\*"> + %(NodePlugin.Identity) + + + + + <_PluginOwnDll Include="@(_PluginAllDll)" Condition="Exists('%(FullPath)') AND '%(Filename)' == '%(PluginName)'" /> + + <_PluginDepDll Include="@(_PluginAllDll)" Condition="Exists('%(FullPath)') AND '%(Filename)' != '%(PluginName)' AND !Exists('$(TargetDir)%(Filename)%(Extension)') AND !Exists('$(_PluginRoot)\%(Filename)\%(Filename).csproj')" /> + <_PluginJsonExisting Include="@(_PluginJson)" Condition="Exists('%(FullPath)')" /> + <_PluginRuntimeExisting Include="@(_PluginRuntime)" Condition="Exists('%(FullPath)')" /> + + + + + + + + + + + <_HostRid>$([System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier) + <_CopyRid>$(_HostRid) + <_CopyRid Condition="!Exists('$(_PluginRoot)\LevelDBStore\bin\$(Configuration)\$(TargetFramework)\runtimes\$(_CopyRid)\native')">win-x64 + + + <_HostNative Include="$(_PluginRoot)\LevelDBStore\bin\$(Configuration)\$(TargetFramework)\runtimes\$(_CopyRid)\native\*.*" /> + <_HostNative Include="$(_PluginRoot)\RocksDBStore\bin\$(Configuration)\$(TargetFramework)\runtimes\$(_CopyRid)\native\*.*" /> + <_HostNative Include="$(_PluginRoot)\SQLiteWallet\bin\$(Configuration)\$(TargetFramework)\runtimes\$(_CopyRid)\native\*.*" /> + <_HostNativeExisting Include="@(_HostNative)" Condition="Exists('%(FullPath)')" /> + <_LevelDbNative Include="$(_PluginRoot)\LevelDBStore\bin\$(Configuration)\$(TargetFramework)\runtimes\**\*" Condition="Exists('$(_PluginRoot)\LevelDBStore\bin\$(Configuration)\$(TargetFramework)\runtimes')" /> + + + + + + diff --git a/src/Neo.CLI/CLI/MainService.Node.cs b/src/Neo.CLI/CLI/MainService.Node.cs index 7d4801480..8125c48a4 100644 --- a/src/Neo.CLI/CLI/MainService.Node.cs +++ b/src/Neo.CLI/CLI/MainService.Node.cs @@ -51,7 +51,7 @@ private void OnShowPoolCommand(bool verbose = false) Console.WriteLine($"total: {NeoSystem.MemPool.Count}, verified: {verifiedCount}, unverified: {unverifiedCount}"); } - private Task CreateBroadcastTask(CancellationToken cancellationToken) + internal Task CreateBroadcastTask(CancellationToken cancellationToken) { return Task.Run(async () => { @@ -633,7 +633,7 @@ private static ConsoleColor GetColorForValue(int value, int lowThreshold, int hi return ConsoleColor.Red; } - private uint GetMaxPeerBlockHeight() + internal uint GetMaxPeerBlockHeight() { var nodes = LocalNode.GetRemoteNodes().ToArray(); if (nodes.Length == 0) return 0; diff --git a/src/Neo.CLI/CLI/MainService.cs b/src/Neo.CLI/CLI/MainService.cs index f0a50c6f9..650713e5f 100644 --- a/src/Neo.CLI/CLI/MainService.cs +++ b/src/Neo.CLI/CLI/MainService.cs @@ -68,6 +68,10 @@ public LocalNode LocalNode private set => _localNode = value; } + private readonly TaskCompletionSource _startCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously); + + internal Task WhenStarted => _startCompleted.Task; + protected override string Prompt => "neo"; public override string ServiceName => "NEO-CLI"; @@ -329,6 +333,24 @@ void DisplayError(string primaryMessage, string? secondaryMessage = null) } public async void Start(CommandLineOptions options) + { + try + { + await StartCore(options).ConfigureAwait(false); + if (_neoSystem is null) + { + _startCompleted.TrySetException(new InvalidOperationException( + "NEO node failed to start. See console output for details.")); + return; + } + } + catch (Exception ex) + { + _startCompleted.TrySetException(ex); + } + } + + private async Task StartCore(CommandLineOptions options) { if (NeoSystem != null) return; bool verifyImport = !(options.NoVerify ?? false); diff --git a/src/Neo.CLI/Neo.CLI.csproj b/src/Neo.CLI/Neo.CLI.csproj index 387151cf8..32e46c98a 100644 --- a/src/Neo.CLI/Neo.CLI.csproj +++ b/src/Neo.CLI/Neo.CLI.csproj @@ -20,6 +20,13 @@ + + + + + + + diff --git a/src/Neo.ConsoleService/Neo.ConsoleService.csproj b/src/Neo.ConsoleService/Neo.ConsoleService.csproj index d66bce6a8..c0f0346e3 100644 --- a/src/Neo.ConsoleService/Neo.ConsoleService.csproj +++ b/src/Neo.ConsoleService/Neo.ConsoleService.csproj @@ -4,4 +4,9 @@ + + + + + diff --git a/src/Neo.ConsoleUI/CommandCatalog.cs b/src/Neo.ConsoleUI/CommandCatalog.cs new file mode 100644 index 000000000..97cd168db --- /dev/null +++ b/src/Neo.ConsoleUI/CommandCatalog.cs @@ -0,0 +1,67 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// CommandCatalog.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.CLI; +using Neo.ConsoleService; +using Neo.Plugins; +using System.Reflection; + +namespace Neo.ConsoleUI; + +internal sealed record CommandInfo( + string Key, + string Category, + string Description, + MethodInfo Method, + object Instance); + +internal static class CommandCatalog +{ + public static IReadOnlyList Load(MainService service) + { + var list = new List(); + AddCommands(list, service, service); + AddCommands(list, service, service, typeof(ConsoleServiceBase)); + foreach (var plugin in Plugin.Plugins) + AddCommands(list, plugin, plugin); + + return list + .OrderBy(c => c.Category, StringComparer.Ordinal) + .ThenBy(c => c.Key, StringComparer.Ordinal) + .ToArray(); + } + + private static void AddCommands(List list, object instance, object owner, Type? type = null) + { + type ??= instance.GetType(); + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (var attribute in method.GetCustomAttributes(inherit: true)) + { + var key = string.Join(' ', attribute.Verbs); + if (list.Any(c => c.Key == key && c.Method == method)) + continue; + list.Add(new CommandInfo( + key, + string.IsNullOrWhiteSpace(attribute.Category) ? "Commands" : attribute.Category, + attribute.Description, + method, + owner)); + } + } + } + + public static IEnumerable Categories(IReadOnlyList commands) + => commands.Select(c => c.Category).Distinct(StringComparer.Ordinal); + + public static IReadOnlyList Completions(IReadOnlyList commands) + => commands.Select(c => c.Key).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); +} diff --git a/src/Neo.ConsoleUI/CommandLineApp.cs b/src/Neo.ConsoleUI/CommandLineApp.cs new file mode 100644 index 000000000..5aeefea7d --- /dev/null +++ b/src/Neo.ConsoleUI/CommandLineApp.cs @@ -0,0 +1,122 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// CommandLineApp.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.CommandLine; + +namespace Neo.ConsoleUI; + +internal sealed class CommandLineApp +{ + public RootCommand Root { get; } + public Option Config { get; } + public Option Wallet { get; } + public Option Password { get; } + public Option DbEngine { get; } + public Option DbPath { get; } + public Option Plugins { get; } + public Option NoVerify { get; } + + public CommandLineApp(IReadOnlyList commands, Func invoke) + { + Root = new RootCommand("NEO cross-platform console UI (menus, hotkeys, line editing). Same commands as neo-cli."); + Config = NewOption("--config", "Specifies the config file.", "-c", "/config"); + Wallet = NewOption("--wallet", "The path of the neo3 wallet [*.json].", "-w", "/wallet"); + Password = NewOption("--password", "Password to decrypt the wallet.", "-p", "/password"); + DbEngine = NewOption("--db-engine", "Specify the db engine.", "/db-engine"); + DbPath = NewOption("--db-path", "Specify the db path.", "/db-path"); + Plugins = new Option("--plugins") + { + Description = "Plugins to install if not already present [plugin1 plugin2].", + Arity = ArgumentArity.ZeroOrMore, + AllowMultipleArgumentsPerToken = true + }; + Plugins.Aliases.Add("/plugins"); + NoVerify = NewOption("--noverify", "Skip block verification when importing.", "/noverify"); + + foreach (var option in new Option[] { Config, Wallet, Password, DbEngine, DbPath, Plugins, NoVerify }) + Root.Options.Add(option); + + // Root must have an action so `neo-tui` with only options (or none) is valid. + Root.SetAction(static _ => { }); + + foreach (var info in commands) + AddCommand(Root, info, invoke); + } + + public string[] ToMainServiceArgs(ParseResult result) + { + var args = new List(); + Add(result, Config, args); + Add(result, Wallet, args); + Add(result, Password, args); + Add(result, DbEngine, args); + Add(result, DbPath, args); + var plugins = result.GetValue(Plugins); + if (plugins is { Length: > 0 }) + { + args.Add("--plugins"); + args.AddRange(plugins); + } + if (result.GetValue(NoVerify)) + args.Add("--noverify"); + return [.. args]; + } + + private static void Add(ParseResult result, Option option, List args) + { + var value = result.GetValue(option); + if (string.IsNullOrEmpty(value)) + return; + args.Add(option.Name); + args.Add(value); + } + + private static void AddCommand(Command parent, CommandInfo info, Func invoke) + { + var current = parent; + foreach (var verb in info.Key.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + var existing = current.Subcommands.FirstOrDefault(c => c.Name == verb); + if (existing is null) + { + existing = new Command(verb); + current.Subcommands.Add(existing); + } + current = existing; + } + + current.Description = string.IsNullOrWhiteSpace(info.Description) ? info.Key : info.Description; + + if (current.Arguments.All(a => a.Name != "args")) + { + var rest = new Argument("args") + { + Description = "Command arguments (same syntax as neo-cli).", + Arity = ArgumentArity.ZeroOrMore + }; + current.Arguments.Add(rest); + current.SetAction(parseResult => + { + var extra = parseResult.GetValue(rest) ?? []; + var line = extra.Length == 0 ? info.Key : $"{info.Key} {string.Join(' ', extra)}"; + invoke(line); + }); + } + } + + private static Option NewOption(string name, string description, params string[] aliases) + { + var option = new Option(name) { Description = description }; + foreach (var alias in aliases) + option.Aliases.Add(alias); + return option; + } +} diff --git a/src/Neo.ConsoleUI/CommandOutputPopup.cs b/src/Neo.ConsoleUI/CommandOutputPopup.cs new file mode 100644 index 000000000..8d897f862 --- /dev/null +++ b/src/Neo.ConsoleUI/CommandOutputPopup.cs @@ -0,0 +1,128 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// CommandOutputPopup.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Spectre.Console; + +namespace Neo.ConsoleUI; + +/// +/// Captures neo-cli command output and shows it in a panel on this console. +/// +internal static class CommandOutputPopup +{ + public static bool Run(string commandLine, Func invoke) + { + var found = true; + while (true) + { + var result = Execute(commandLine, invoke); + found = result.Found; + if (!Show(commandLine, result.Output, result.Ok)) + return found; + } + } + + private static (bool Found, bool Ok, string Output) Execute(string commandLine, Func invoke) + { + var buffer = new StringWriter(); + var stdout = Console.Out; + var stderr = Console.Error; + var found = true; + var ok = true; + + AnsiConsole.Clear(); + AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .SpinnerStyle(Style.Parse("green")) + .Start($"Please wait — running {commandLine}", _ => + { + try + { + Console.SetOut(buffer); + Console.SetError(buffer); + found = invoke(commandLine); + if (!found) + buffer.WriteLine("Command not found"); + } + catch (Exception ex) + { + ok = false; + buffer.WriteLine(ex.InnerException?.Message ?? ex.Message); + } + finally + { + Console.SetOut(stdout); + Console.SetError(stderr); + } + }); + + return (found, ok && found, buffer.ToString()); + } + + /// to rerun the command. + private static bool Show(string commandLine, string output, bool ok) + { + AnsiConsole.Clear(); + var width = Math.Max(40, Console.WindowWidth - 2); + var innerWidth = Math.Max(20, width - 4); + var maxLines = Math.Max(6, Console.WindowHeight - 8); + var body = string.IsNullOrWhiteSpace(output) ? "(no output)" : output.TrimEnd(); + body = WrapOutput(body, innerWidth, maxLines); + + var title = commandLine.Length > 60 + ? string.Concat(commandLine.AsSpan(0, 57), "…") + : commandLine; + var panel = new Panel(new Text(body)) + .Header($" neo> {title} ") + .Border(BoxBorder.Rounded) + .BorderColor(ok ? Color.Green : Color.Red) + .Padding(1, 0, 1, 0) + .Expand(); + AnsiConsole.Write(panel); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[grey]Command finished[/]") + .AddChoices("Rerun command", "Close")); + return choice == "Rerun command"; + } + + private static string WrapOutput(string output, int width, int maxLines) + { + output = output + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n') + .Replace('\t', ' '); + var wrapped = new List(); + foreach (var raw in output.Split('\n')) + { + var line = raw; + if (line.Length == 0) + { + wrapped.Add(string.Empty); + continue; + } + + while (line.Length > width) + { + wrapped.Add(line[..width]); + line = line[width..]; + } + + wrapped.Add(line); + } + + if (wrapped.Count <= maxLines) + return string.Join('\n', wrapped); + return string.Join('\n', wrapped.Take(maxLines - 1)) + + $"\n… ({wrapped.Count - maxLines + 1} more lines)"; + } +} diff --git a/src/Neo.ConsoleUI/InteractiveShell.cs b/src/Neo.ConsoleUI/InteractiveShell.cs new file mode 100644 index 000000000..2cc12220e --- /dev/null +++ b/src/Neo.ConsoleUI/InteractiveShell.cs @@ -0,0 +1,272 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// InteractiveShell.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.CLI; +using Spectre.Console; +using System.Reflection; + +namespace Neo.ConsoleUI; + +internal sealed class InteractiveShell +{ + private readonly MainService _service; + private readonly IReadOnlyList _commands; + private readonly LineEditor _editor; + private readonly Func _invoke; + + public InteractiveShell(MainService service, IReadOnlyList commands, Func invoke) + { + _service = service; + _commands = commands; + _invoke = invoke; + _editor = new LineEditor(CommandCatalog.Completions(commands)); + } + + public void Run() + { + using var broadcastCancel = new CancellationTokenSource(); + Task broadcast = Task.CompletedTask; + try + { + if (_service.NeoSystem is not null) + broadcast = _service.CreateBroadcastTask(broadcastCancel.Token); + } + catch + { + // Node failed to start; still show the status screen. + } + + try + { + while (true) + { + var next = StateScreen.Run(_service); + if (next == StatusAction.Quit) + break; + if (next == StatusAction.Help) + { + AnsiConsole.Clear(); + PrintHotkeys(); + AnsiConsole.MarkupLine("[grey]Press any key to return to status[/]"); + Console.ReadKey(intercept: true); + continue; + } + if (next == StatusAction.CommandLine) + { + AnsiConsole.Clear(); + RunCommandLine(); + continue; + } + + AnsiConsole.Clear(); + if (!RunMenu()) + break; + } + } + finally + { + broadcastCancel.Cancel(); + try { broadcast.Wait(TimeSpan.FromSeconds(2)); } catch { /* cancelled */ } + } + } + + /// when the user chose Exit. + private bool RunMenu() + { + WriteStatus(); + var menu = new[] { "Back to status", "Command line", "Command palette" } + .Concat(CommandCatalog.Categories(_commands)) + .Concat(["Help", "Exit"]); + var action = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[green]Select an action[/] (search with typing)") + .PageSize(15) + .EnableSearch() + .AddChoices(menu)); + + if (action == "Exit") + return false; + if (action == "Back to status") + return true; + if (action == "Help") + { + PrintHotkeys(); + RunLine("help"); + return true; + } + if (action == "Command line") + { + RunCommandLine(); + return true; + } + if (action == "Command palette") + { + RunPalette(); + return true; + } + + RunCategory(action); + return true; + } + + private void RunCategory(string category) + { + var items = _commands.Where(c => c.Category == category).ToArray(); + if (items.Length == 0) + return; + + var labels = items.Select(FormatChoice).Concat([".. Back"]).ToArray(); + var picked = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[yellow]{category}[/]") + .PageSize(20) + .EnableSearch() + .AddChoices(labels)); + if (picked == ".. Back") + return; + + var index = Array.IndexOf(labels, picked); + if (index >= 0 && index < items.Length) + { + if (items[index].Key.Equals("show state", StringComparison.OrdinalIgnoreCase)) + return; + RunCommand(items[index]); + } + } + + private void RunPalette() + { + var labels = _commands.Select(FormatChoice).ToArray(); + var picked = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[yellow]All commands[/] (type to search)") + .PageSize(20) + .EnableSearch() + .AddChoices(labels)); + var index = Array.IndexOf(labels, picked); + if (index >= 0) + { + if (_commands[index].Key.Equals("show state", StringComparison.OrdinalIgnoreCase)) + return; + RunCommand(_commands[index]); + } + } + + private void RunCommandLine() + { + AnsiConsole.MarkupLine("[grey]Tab complete · arrows move · Up/Down history · Esc back · Ctrl+C cancel[/]"); + while (true) + { + var line = _editor.Read("neo> "); + if (line is null) + return; + if (string.IsNullOrWhiteSpace(line)) + continue; + if (line.Equals("exit", StringComparison.OrdinalIgnoreCase) || + line.Equals("quit", StringComparison.OrdinalIgnoreCase)) + return; + if (line.Trim().Equals("show state", StringComparison.OrdinalIgnoreCase)) + return; + RunLine(line.Trim()); + } + } + + private void RunCommand(CommandInfo command) + { + var parameters = command.Method.GetParameters(); + var parts = new List { command.Key }; + foreach (var parameter in parameters) + { + var hint = parameter.HasDefaultValue + ? $"optional, default {parameter.DefaultValue ?? "null"}" + : "required"; + var title = $"[green]{parameter.Name}[/] ({parameter.ParameterType.Name}, {hint})"; + if (IsPassword(parameter)) + { + var secret = AnsiConsole.Prompt(new TextPrompt(title).Secret()); + parts.Add(Quote(secret)); + continue; + } + + if (parameter.HasDefaultValue) + { + var value = AnsiConsole.Prompt( + new TextPrompt(title + " [[empty = default]]") + .AllowEmpty()); + if (string.IsNullOrEmpty(value)) + continue; + parts.Add(Quote(value)); + } + else + { + var value = AnsiConsole.Prompt(new TextPrompt(title)); + parts.Add(Quote(value)); + } + } + + var line = string.Join(' ', parts); + RunLine(line); + } + + private bool RunLine(string line) + => CommandOutputPopup.Run(line, _invoke); + + private void WriteStatus() + { + var wallet = _service.CurrentWallet is null ? "closed" : _service.CurrentWallet.Name; + string height; + try + { + height = _service.NeoSystem is null ? "-" : "?"; + if (_service.NeoSystem is not null) + height = Neo.SmartContract.Native.NativeContract.Ledger.CurrentIndex(_service.NeoSystem.StoreView).ToString(); + } + catch + { + height = "-"; + } + + AnsiConsole.Write(new Rule($"[green]wallet[/] {Markup.Escape(wallet)} [green]height[/] {height}").RuleStyle("grey")); + } + + private static void PrintHotkeys() + { + var table = new Table().Border(TableBorder.Rounded); + table.AddColumn("Key"); + table.AddColumn("Action"); + table.AddRow("M / Enter (status)", "Open command menu"); + table.AddRow("C (status)", "Command line"); + table.AddRow("H (status)", "Help"); + table.AddRow("↑ ↓ (status)", "Scroll connected peers"); + table.AddRow("Q / Esc (status)", "Quit"); + table.AddRow("Type to search", "Filter the current menu"); + table.AddRow("Enter", "Select"); + table.AddRow("Esc (command line)", "Back to menu"); + table.AddRow("Tab (command line)", "Complete command"); + table.AddRow("↑ ↓ (command line)", "History"); + table.AddRow("Ctrl+A / Ctrl+E", "Line start / end"); + table.AddRow("Ctrl+U", "Clear line"); + table.AddRow("Ctrl+C", "Cancel line / later Exit"); + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + } + + private static string FormatChoice(CommandInfo command) + => string.IsNullOrWhiteSpace(command.Description) + ? command.Key + : $"{command.Key} [grey]{Markup.Escape(command.Description)}[/]"; + + private static bool IsPassword(ParameterInfo parameter) + => parameter.Name is not null && parameter.Name.Contains("password", StringComparison.OrdinalIgnoreCase); + + private static string Quote(string value) + => value.Contains(' ', StringComparison.Ordinal) ? $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"" : value; +} diff --git a/src/Neo.ConsoleUI/LineEditor.cs b/src/Neo.ConsoleUI/LineEditor.cs new file mode 100644 index 000000000..d5dfe900b --- /dev/null +++ b/src/Neo.ConsoleUI/LineEditor.cs @@ -0,0 +1,184 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// LineEditor.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Spectre.Console; + +namespace Neo.ConsoleUI; + +/// +/// Cross-platform line editor: arrows, Home/End, history, Tab complete, Ctrl+U/A/E. +/// +internal sealed class LineEditor +{ + private readonly List _history = []; + private readonly IReadOnlyList _completions; + private const int HistorySize = 200; + + public LineEditor(IReadOnlyList completions) + { + _completions = completions; + } + + public string? Read(string prompt) + { + var input = new System.Text.StringBuilder(); + var cursor = 0; + var historyIndex = -1; + + void Redraw() + { + AnsiConsole.Cursor.Show(); + var line = prompt + input; + AnsiConsole.Write("\r" + line + new string(' ', Math.Max(0, Console.WindowWidth - line.Length - 1))); + var left = Math.Min(prompt.Length + cursor, Math.Max(0, Console.WindowWidth - 1)); + try + { + Console.SetCursorPosition(left, Console.CursorTop); + } + catch + { + // Some redirected consoles reject SetCursorPosition. + } + } + + Redraw(); + while (true) + { + var key = Console.ReadKey(intercept: true); + if (key.Key == ConsoleKey.Enter) + { + AnsiConsole.WriteLine(); + var result = input.ToString(); + if (!string.IsNullOrWhiteSpace(result)) + { + _history.Add(result); + if (_history.Count > HistorySize) + _history.RemoveAt(0); + } + return result; + } + + if (key.Key == ConsoleKey.Escape) + { + AnsiConsole.WriteLine(); + return null; + } + + if (key.Key == ConsoleKey.C && key.Modifiers.HasFlag(ConsoleModifiers.Control)) + return null; + + if (key.Key == ConsoleKey.LeftArrow) + { + if (cursor > 0) cursor--; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.RightArrow) + { + if (cursor < input.Length) cursor++; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.Home || (key.Key == ConsoleKey.A && key.Modifiers.HasFlag(ConsoleModifiers.Control))) + { + cursor = 0; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.End || (key.Key == ConsoleKey.E && key.Modifiers.HasFlag(ConsoleModifiers.Control))) + { + cursor = input.Length; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.U && key.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + input.Clear(); + cursor = 0; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.Backspace) + { + if (cursor > 0) + { + input.Remove(cursor - 1, 1); + cursor--; + } + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.Delete) + { + if (cursor < input.Length) + input.Remove(cursor, 1); + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.UpArrow) + { + if (_history.Count == 0) continue; + historyIndex = historyIndex < 0 ? _history.Count - 1 : Math.Max(0, historyIndex - 1); + input.Clear(); + input.Append(_history[historyIndex]); + cursor = input.Length; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.DownArrow) + { + if (historyIndex < 0) continue; + historyIndex++; + input.Clear(); + if (historyIndex >= _history.Count) + { + historyIndex = -1; + } + else + { + input.Append(_history[historyIndex]); + } + cursor = input.Length; + Redraw(); + continue; + } + + if (key.Key == ConsoleKey.Tab) + { + var prefix = input.ToString(); + var match = _completions.FirstOrDefault(c => c.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + if (match is not null) + { + input.Clear(); + input.Append(match); + cursor = input.Length; + Redraw(); + } + continue; + } + + if (!char.IsControl(key.KeyChar)) + { + input.Insert(cursor, key.KeyChar); + cursor++; + Redraw(); + } + } + } +} diff --git a/src/Neo.ConsoleUI/Neo.ConsoleUI.csproj b/src/Neo.ConsoleUI/Neo.ConsoleUI.csproj new file mode 100644 index 000000000..0cdea7085 --- /dev/null +++ b/src/Neo.ConsoleUI/Neo.ConsoleUI.csproj @@ -0,0 +1,36 @@ + + + + Neo.ConsoleUI + neo-tui + Exe + Cross-platform console UI for neo-cli (menus, hotkeys, line editing). + Neo.ConsoleUI + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + PreserveNewest + %(Filename)%(Extension) + + + + diff --git a/src/Neo.ConsoleUI/Program.cs b/src/Neo.ConsoleUI/Program.cs new file mode 100644 index 000000000..327cb824c --- /dev/null +++ b/src/Neo.ConsoleUI/Program.cs @@ -0,0 +1,101 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// Program.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.CLI; +using Spectre.Console; +using System.CommandLine; + +namespace Neo.ConsoleUI; + +static class Program +{ + static int Main(string[] args) + { + // Match neo-cli: Plugins/, config.json, and auto-install zips are resolved from the exe directory. + Directory.SetCurrentDirectory(AppContext.BaseDirectory); + + var service = new MainService(); + var bootstrap = new CommandLineApp([], static _ => true); + bootstrap.Root.TreatUnmatchedTokensAsErrors = false; + var parseResult = bootstrap.Root.Parse(args); + if (parseResult.Errors.Count > 0) + { + foreach (var error in parseResult.Errors) + AnsiConsole.MarkupLine($"[red]{Markup.Escape(error.Message)}[/]"); + return 1; + } + + var startArgs = bootstrap.ToMainServiceArgs(parseResult); + if (!startArgs.Contains("--config", StringComparer.OrdinalIgnoreCase)) + { + var besideExe = Path.Combine(AppContext.BaseDirectory, "config.json"); + if (File.Exists(besideExe)) + startArgs = [.. startArgs, "--config", besideExe]; + } + + if (!service.OnStart(startArgs)) + return 1; + + try + { + service.WhenStarted.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex); + service.OnStop(); + return 1; + } + + bool Invoke(string line) + => service.OnCommand(line); + + var commands = CommandCatalog.Load(service); + var app = new CommandLineApp(commands, Invoke); + var fullParse = app.Root.Parse(args); + if (fullParse.Errors.Count > 0) + { + foreach (var error in fullParse.Errors) + AnsiConsole.MarkupLine($"[red]{Markup.Escape(error.Message)}[/]"); + service.OnStop(); + return 1; + } + + try + { + if (fullParse.CommandResult.Command != app.Root && !IsShowState(fullParse)) + { + fullParse.Invoke(); + return 0; + } + + new InteractiveShell(service, commands, Invoke).Run(); + Invoke("exit"); + return 0; + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex); + return 1; + } + finally + { + service.OnStop(); + } + } + + private static bool IsShowState(ParseResult parse) + { + var command = parse.CommandResult.Command; + return command.Name == "state" && + command.Parents.OfType().Any(c => c.Name == "show"); + } +} diff --git a/src/Neo.ConsoleUI/StateScreen.cs b/src/Neo.ConsoleUI/StateScreen.cs new file mode 100644 index 000000000..eee572f9d --- /dev/null +++ b/src/Neo.ConsoleUI/StateScreen.cs @@ -0,0 +1,325 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// StateScreen.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.CLI; +using Neo.Network.P2P; +using Neo.Plugins; +using Neo.SmartContract.Native; +using Spectre.Console; +using Spectre.Console.Rendering; +using System.Diagnostics; + +namespace Neo.ConsoleUI; + +internal enum StatusAction +{ + Quit, + Menu, + CommandLine, + Help +} + +/// +/// Live dashboard matching neo-cli show state. +/// +internal static class StateScreen +{ + private static readonly DateTime StartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime(); + private static int _peerScroll; + private const int PeerPageSize = 5; + + public static StatusAction Run(MainService service) + { + var action = StatusAction.Quit; + AnsiConsole.Clear(); + AnsiConsole.Live(Build(service)) + .AutoClear(true) + .Overflow(VerticalOverflow.Crop) + .Start(ctx => + { + while (true) + { + ctx.UpdateTarget(Build(service)); + if (Console.KeyAvailable) + { + var key = Console.ReadKey(intercept: true); + switch (key.Key) + { + case ConsoleKey.Q: + case ConsoleKey.Escape: + action = StatusAction.Quit; + return; + case ConsoleKey.M: + case ConsoleKey.Enter: + action = StatusAction.Menu; + return; + case ConsoleKey.C: + action = StatusAction.CommandLine; + return; + case ConsoleKey.H: + action = StatusAction.Help; + return; + case ConsoleKey.DownArrow: + case ConsoleKey.J: + case ConsoleKey.PageDown: + _peerScroll++; + break; + case ConsoleKey.UpArrow: + case ConsoleKey.K: + case ConsoleKey.PageUp: + _peerScroll--; + break; + case ConsoleKey.Home: + _peerScroll = 0; + break; + case ConsoleKey.End: + _peerScroll = int.MaxValue; + break; + } + } + + Thread.Sleep(100); + } + }); + return action; + } + + internal static IRenderable Build(MainService service) + { + if (Console.WindowHeight < 23 || Console.WindowWidth < 70) + { + return new Panel("[red]Console window too small (Need at least 70x23 visible)...[/]") + .BorderColor(Color.Red) + .Header("NEO NODE STATUS"); + } + + NeoSystem? system = null; + try + { + system = service.NeoSystem; + } + catch + { + // Node still starting. + } + + if (system is null) + { + return new Panel("[yellow]Node is starting…[/]") + .BorderColor(Color.Yellow) + .Header("NEO NODE STATUS"); + } + + var now = DateTime.UtcNow; + var uptime = now - StartTime; + uint height; + uint headerHeight; + int txPoolSize; + int verified; + int unverified; + int connected; + int unconnected; + uint maxPeer; + RemoteNode[] peers; + try + { + height = NativeContract.Ledger.CurrentIndex(system.StoreView); + headerHeight = system.HeaderCache.Last?.Index ?? height; + txPoolSize = system.MemPool.Count; + verified = system.MemPool.VerifiedCount; + unverified = system.MemPool.UnVerifiedCount; + connected = service.LocalNode.ConnectedCount; + unconnected = service.LocalNode.UnconnectedCount; + maxPeer = service.GetMaxPeerBlockHeight(); + peers = [.. service.LocalNode.GetRemoteNodes()]; + } + catch (Exception ex) + { + return new Panel($"[red]{Markup.Escape(ex.Message)}[/]") + .BorderColor(Color.Red) + .Header("NEO NODE STATUS"); + } + + var memoryMb = GC.GetTotalMemory(false) / (1024 * 1024); + var cpu = GetCpuUsage(uptime); + var wallet = service.CurrentWallet is null ? "closed" : service.CurrentWallet.Name; + var plugins = Plugin.Plugins.Count == 0 + ? "(none)" + : string.Join(", ", Plugin.Plugins.Select(p => p.Name)); + + var layout = new Grid(); + layout.AddColumn(); + layout.AddRow(new Rule("[bold green]NEO NODE STATUS[/]").RuleStyle("green")); + + var timeText = + $"[grey]Current Time:[/] {now:yyyy-MM-dd HH:mm:ss} [grey]Uptime:[/] {uptime.Days}d {uptime.Hours:D2}h {uptime.Minutes:D2}m {uptime.Seconds:D2}s"; + + var columns = new Grid(); + columns.AddColumn(); + columns.AddColumn(); + columns.AddRow( + MetricPanel("BLOCKCHAIN STATUS", Color.Aqua, [ + ("Block Height", $"{height,10}"), + ("Header Height", $"{headerHeight,10}") + ]), + MetricPanel("SYSTEM RESOURCES", Color.Aqua, [ + ("Memory Usage", $"{memoryMb,10} MB"), + ("CPU Usage", $"{cpu,10:F1} %") + ])); + columns.AddRow( + MetricPanel("TRANSACTION POOL", TxColor(txPoolSize), [ + ("Total Txs", $"{txPoolSize,10}"), + ("Verified Txs", $"{verified,10}"), + ("Unverified Txs", $"{unverified,10}") + ]), + MetricPanel("NETWORK STATUS", Color.Green, [ + ("Connected", $"{connected,10}"), + ("Unconnected", $"{unconnected,10}"), + ("Max Block Height", $"{maxPeer,8}") + ])); + + var wide = Console.WindowWidth >= 100; + if (wide) + { + var head = new Table().HideHeaders().NoBorder().Expand(); + head.AddColumn(new TableColumn("t").Width(80).NoWrap().Padding(0, 0, 1, 0)); + head.AddColumn(new TableColumn("p").Padding(1, 0, 0, 0)); + head.AddRow(new Markup(timeText), Align.Center(new Markup("[green]CONNECTED PEERS[/]"))); + layout.AddRow(head); + + var split = new Table().HideHeaders().NoBorder().Expand(); + split.AddColumn(new TableColumn("metrics").Width(80).NoWrap().Padding(0, 0, 1, 0)); + split.AddColumn(new TableColumn("peers").Padding(1, 0, 0, 0)); + split.AddRow(columns, PeersPanel(peers, showTitle: false)); + layout.AddRow(split); + } + else + { + layout.AddRow(new Markup(timeText)); + layout.AddRow(columns); + layout.AddRow(PeersPanel(peers, showTitle: true)); + } + + if (height < maxPeer && maxPeer > 0) + layout.AddRow(SyncBar(height, maxPeer, Math.Min(70, Console.WindowWidth - 4))); + + layout.AddRow(new Markup( + $"[grey]wallet[/] {Markup.Escape(wallet)} [grey]plugins[/] {Markup.Escape(plugins)}")); + layout.AddRow(new Rule( + "[grey]M[/] menu [grey]C[/] command line [grey]H[/] help [grey]Up/Down[/] peers [grey]Q[/] quit") + .RuleStyle("darkgreen")); + return layout; + } + + private static IRenderable PeersPanel(RemoteNode[] peers, bool showTitle) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Green) + .Expand(); + if (showTitle) + table.Title("[green] CONNECTED PEERS [/]"); + table.AddColumn(new TableColumn("Address").NoWrap()); + table.AddColumn(new TableColumn("Height").RightAligned().NoWrap()); + table.AddColumn(new TableColumn("Agent")); + + var ordered = peers.OrderByDescending(peer => peer.LastBlockIndex).ToArray(); + var maxScroll = Math.Max(0, ordered.Length - PeerPageSize); + if (_peerScroll > maxScroll) + _peerScroll = maxScroll; + if (_peerScroll < 0) + _peerScroll = 0; + + var page = ordered.Skip(_peerScroll).Take(PeerPageSize).ToArray(); + foreach (var peer in page) + { + var port = peer.ListenerTcpPort > 0 ? peer.ListenerTcpPort : peer.Remote.Port; + var address = $"{peer.Remote.Address}:{port}"; + var agent = peer.Version?.UserAgent ?? "-"; + if (agent.Length > 36) + agent = string.Concat(agent.AsSpan(0, 35), "…"); + table.AddRow( + Markup.Escape(address), + peer.LastBlockIndex.ToString(), + Markup.Escape(agent)); + } + + if (page.Length == 0) + table.AddRow("[grey](none)[/]", "", ""); + for (var i = page.Length == 0 ? 1 : page.Length; i < PeerPageSize; i++) + table.AddRow(" ", " ", " "); + + if (ordered.Length > PeerPageSize) + { + var last = _peerScroll + page.Length; + var more = ordered.Length > last ? " ↓" : ""; + var less = _peerScroll > 0 ? "↑ " : " "; + table.Caption($"[grey]{less}{_peerScroll + 1}–{last}/{ordered.Length}{more}[/]"); + } + else + table.Caption($"[grey]{ordered.Length} connected[/]"); + + return table; + } + + private static Panel MetricPanel(string title, Color color, params (string Label, string Value)[] rows) + { + var table = new Table().HideHeaders().NoBorder().Expand(); + table.AddColumn(new TableColumn("k").PadRight(1)); + table.AddColumn(new TableColumn("v").RightAligned()); + foreach (var (label, value) in rows) + table.AddRow($" {label}", value); + return new Panel(table) + .Header($" {title} ") + .BorderColor(color) + .Padding(1, 0, 1, 0); + } + + private static Color TxColor(int txPoolSize) + { + if (txPoolSize < 100) return Color.Green; + if (txPoolSize < 500) return Color.Yellow; + return Color.Red; + } + + private static Markup SyncBar(uint height, uint maxPeer, int boxWidth) + { + var syncPercentage = (double)height / maxPeer * 100; + var progressBarWidth = Math.Max(10, boxWidth - 25); + var filledWidth = (int)Math.Round(progressBarWidth * syncPercentage / 100); + if (filledWidth > progressBarWidth) filledWidth = progressBarWidth; + var bar = new string('█', filledWidth) + new string('░', progressBarWidth - filledWidth); + return new Markup( + $"[yellow]Syncing:[/] {Markup.Escape($"[{bar}]")} {syncPercentage:F2}% ({height}/{maxPeer})"); + } + + private static double GetCpuUsage(TimeSpan uptime) + { + try + { + var currentProcess = Process.GetCurrentProcess(); + if (uptime.TotalMilliseconds > 0 && Environment.ProcessorCount > 0) + { + var cpuUsage = Math.Round(currentProcess.TotalProcessorTime.TotalMilliseconds / + (Environment.ProcessorCount * uptime.TotalMilliseconds) * 100, 1); + if (cpuUsage < 0) cpuUsage = 0; + if (cpuUsage > 100) cpuUsage = 100; + return cpuUsage; + } + } + catch + { + // Ignore CPU usage calculation errors. + } + + return 0; + } +} diff --git a/src/NodePlugins.props b/src/NodePlugins.props new file mode 100644 index 000000000..88e72d525 --- /dev/null +++ b/src/NodePlugins.props @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + false + false + + + false + false + + +