diff --git a/.claude/rules/testing-patterns.md b/.claude/rules/testing-patterns.md index b9c3b10ec1..768ac1bf1f 100644 --- a/.claude/rules/testing-patterns.md +++ b/.claude/rules/testing-patterns.md @@ -31,7 +31,7 @@ - Make them parallelizable and add them to `UnitTestsParallelizable` 5. **Avoid static dependencies** - - DO NOT use the legacy/static `Application` API or `ConfigurationManager` in tests unless the tests explicitly test related functionality + - DO NOT use the legacy/static `Application` API or mutate process-wide `*Settings` facades in tests unless the tests explicitly test related functionality 6. **Don't use `[AutoInitShutdown]` or `[SetupFakeApplication]`** - Legacy pattern, being phased out diff --git a/.claude/tasks/build-app.md b/.claude/tasks/build-app.md index 713a95f1b8..e31a24c8eb 100644 --- a/.claude/tasks/build-app.md +++ b/.claude/tasks/build-app.md @@ -33,7 +33,7 @@ using Terminal.Gui.Views; // All controls (Button, Label, etc.) using Terminal.Gui.ViewBase; // View, Pos, Dim using Terminal.Gui.Drawing; // Colors, Attribute, LineStyle using Terminal.Gui.Input; // Key, KeyCode, MouseFlags -using Terminal.Gui.Configuration; // ConfigurationManager (optional) +using Terminal.Gui.Configuration; // TuiConfigurationBuilder, Settings POCOs (optional) ``` ## Application Structure diff --git a/.github/workflows/analyze-hang-dump.yml b/.github/workflows/analyze-hang-dump.yml index c23c1cf715..98ff2983c6 100644 --- a/.github/workflows/analyze-hang-dump.yml +++ b/.github/workflows/analyze-hang-dump.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: | 8.0.x diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index b6c05c5f02..3e00784a3b 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -26,7 +26,7 @@ jobs: fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' @@ -71,7 +71,7 @@ jobs: - name: Setup Go if: steps.view-docs.outputs.should_generate == 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: stable diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index 7dc967f5d8..4371cacf13 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -31,7 +31,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml index 81ebe28b07..6c20d22ca8 100644 --- a/.github/workflows/finalize-release.yml +++ b/.github/workflows/finalize-release.yml @@ -33,10 +33,18 @@ jobs: - name: Extract release info from branch name id: release + env: + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" + BRANCH="$HEAD_REF" # Branch name is release/vX.Y.Z or release/vX.Y.Z-beta TAG="${BRANCH#release/}" + + if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Unexpected release branch format: $BRANCH" + exit 1 + fi + SEMVER="${TAG#v}" # Determine if this is a pre-release @@ -82,8 +90,10 @@ jobs: $PRERELEASE_FLAG - name: Delete release branch + env: + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" + BRANCH="$HEAD_REF" git push origin --delete "$BRANCH" || true echo "Deleted branch: $BRANCH" diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 718fb3a62f..9fa78b5757 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -28,7 +28,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: ga diff --git a/.github/workflows/perf-gate.yml b/.github/workflows/perf-gate.yml index cc7a3d2626..5fee72ef2b 100644 --- a/.github/workflows/perf-gate.yml +++ b/.github/workflows/perf-gate.yml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 # GitVersion needs full history - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' @@ -78,7 +78,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4c64c8af5e..08b9748693 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,7 +43,7 @@ jobs: id: gitversion # step id used as reference for output values - name: Setup dotnet - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/quick-build.yml b/.github/workflows/quick-build.yml index b996f7658c..0b3410e67b 100644 --- a/.github/workflows/quick-build.yml +++ b/.github/workflows/quick-build.yml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v7 - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/stress-tests.yml b/.github/workflows/stress-tests.yml index 8ca7cf5680..c8cba430cf 100644 --- a/.github/workflows/stress-tests.yml +++ b/.github/workflows/stress-tests.yml @@ -26,7 +26,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' @@ -71,7 +71,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 8a46d0613a..80756c33a0 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' @@ -90,7 +90,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.github/workflows/validate-doc-snippets.yml b/.github/workflows/validate-doc-snippets.yml index 730e63dd79..8d0e085b81 100644 --- a/.github/workflows/validate-doc-snippets.yml +++ b/.github/workflows/validate-doc-snippets.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 # GitVersion.MsBuild needs full history - name: Setup .NET Core - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: 10.x dotnet-quality: 'ga' diff --git a/.tg-docs/INDEX.md b/.tg-docs/INDEX.md index fc4ad09e84..9c5b2bc376 100644 --- a/.tg-docs/INDEX.md +++ b/.tg-docs/INDEX.md @@ -22,7 +22,7 @@ Instead of embedding descriptions, it points to actual source files that agents |IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning. Read files when needed. |.:{ModuleInitializers.cs} |App:{Application.cs,ApplicationImpl.cs,ApplicationImpl.Driver.cs,ApplicationImpl.Lifecycle.cs,ApplicationImpl.Run.cs,ApplicationImpl.Screen.cs,ApplicationModelUsage.cs,ApplicationNavigation.cs,ApplicationPopover.cs,ApplicationToolTip.cs,AppModel.cs,IApplication.cs,Logging.cs,NotInitializedException.cs} -|App/Clipboard:{Clipboard.cs,ClipboardBase.cs,ClipboardProcessRunner.cs,IClipboard.cs} +|App/Clipboard:{Clipboard.cs,ClipboardBase.cs,ClipboardProcessRunner.cs,ClipboardProcessRunnerImpl.cs,IClipboard.cs,IClipboardProcessRunner.cs} |App/CWP:{CancelEventArgs.cs,CWPEventHelper.cs,CWPPropertyHelper.cs,CWPWorkflowHelper.cs,EventArgs.cs,ResultEventArgs.cs,ValueChangedEventArgs.cs,ValueChangingEventArgs.cs} |App/Keyboard:{ApplicationKeyboard.cs,IKeyboard.cs} |App/Legacy:{Application.Clipboard.cs,Application.Driver.cs,Application.Keyboard.cs,Application.Lifecycle.cs,Application.Mouse.cs,Application.Navigation.cs,Application.Popovers.cs,Application.Run.cs,Application.Screen.cs,Application.TopRunnable.cs} @@ -32,14 +32,16 @@ Instead of embedding descriptions, it points to actual source files that agents |App/Runnable:{IRunnable.cs,SessionToken.cs,SessionTokenEventArgs.cs} |App/Timeout:{ITimedEvents.cs,LogarithmicTimeout.cs,SmoothAcceleratingTimeout.cs,TimedEvents.cs,Timeout.cs,TimeoutEventArgs.cs} |App/Tracing:{ITraceBackend.cs,ListBackend.cs,LoggingBackend.cs,NullBackend.cs,Trace.cs,TraceCategory.cs,TraceEntry.cs,TraceScope.cs} -|Configuration:{AppSettingsScope.cs,AttributeJsonConverter.cs,ColorJsonConverter.cs,ConcurrentDictionaryJsonConverter.cs,ConfigLocations.cs,ConfigProperty.cs,ConfigurationManager.cs,ConfigurationManagerEventArgs.cs,ConfigurationManagerNotEnabledException.cs,ConfigurationPropertyAttribute.cs,DeepCloner.cs,DictionaryJsonConverter.cs,KeyArrayJsonConverter.cs,KeyCodeJsonConverter.cs,KeyJsonConverter.cs,RuneJsonConverter.cs,SchemeJsonConverter.cs,SchemeManager.cs,Scope.cs,ScopeJsonConverter.cs,SettingsScope.cs,SourceGenerationContext.cs,SourcesManager.cs,ThemeManager.cs,ThemeScope.cs,TraceCategoryJsonConverter.cs} +|Configuration:{AttributeJsonConverter.cs,ColorJsonConverter.cs,ConcurrentDictionaryJsonConverter.cs,DictionaryJsonConverter.cs,KeyArrayJsonConverter.cs,KeyCodeJsonConverter.cs,KeyJsonConverter.cs,MouseFlagsArrayJsonConverter.cs,RuneJsonConverter.cs,SchemeJsonConverter.cs,SchemeManager.cs,SourceGenerationContext.cs,ThemeManager.cs,TraceCategoryJsonConverter.cs,TuiJsonErrors.cs,TuiSerializerContext.cs} +|Configuration/Settings:{ApplicationSettings.cs,ButtonSettings.cs,CharMapSettings.cs,CheckBoxSettings.cs,DialogSettings.cs,DriverSettings.cs,FileDialogSettings.cs,FileDialogStyleSettings.cs,FrameViewSettings.cs,GlyphSettings.cs,HexViewSettings.cs,ISchemeManager.cs,IThemeManager.cs,KeySettings.cs,LinearRangeSettings.cs,MecThemeManager.cs,MenuBarSettings.cs,MenuSettings.cs,MessageBoxSettings.cs,NerdFontsSettings.cs,PopoverMenuSettings.cs,SelectorBaseSettings.cs,StatusBarSettings.cs,TextFieldSettings.cs,TextViewSettings.cs,ThemeCatalog.cs,ThemeChanges.cs,ThemeSettings.cs,TraceSettings.cs,TuiConfigurationBuilder.cs,TuiConfigurationExtensions.cs,WindowSettings.cs} |Drawing:{Attribute.cs,Cell.cs,CellEventArgs.cs,FillPair.cs,Glyphs.cs,Gradient.cs,GradientFill.cs,GraphemeHelper.cs,IFill.cs,Region.cs,RegionOp.cs,Ruler.cs,Scheme.cs,Schemes.cs,SolidFill.cs,TextStyle.cs,Thickness.cs,VisualRole.cs,VisualRoleEventArgs.cs} |Drawing/Color:{AnsiColorCode.cs,Color.ColorExtensions.cs,Color.ColorName.cs,Color.ColorParseException.cs,Color.cs,Color.Formatting.cs,Color.Operators.cs,ColorModel.cs,ColorQuantizer.cs,ColorStrings.cs,IColorDistance.cs,IColorNameResolver.cs,ICustomColorFormatter.cs,StandardColor.cs,StandardColors.cs,StandardColorsNameResolver.cs} +|Drawing/Kitty:{KittyGraphicsEncoder.cs,KittyGraphicsSupportDetector.cs,KittyGraphicsSupportResult.cs} |Drawing/LineCanvas:{IntersectionDefinition.cs,IntersectionRuneType.cs,IntersectionType.cs,LineCanvas.cs,LineDirections.cs,LineStyle.cs,StraightLine.cs,StraightLineExtensions.cs} |Drawing/Markdown:{ISyntaxHighlighter.cs,MarkdownAttributeHelper.cs,MarkdownStyleRole.cs,StyledSegment.cs,TextMateSyntaxHighlighter.cs} -|Drawing/Quant:{EuclideanColorDistance.cs,IPaletteBuilder.cs,PopularityPaletteWithThreshold.cs} +|Drawing/Quant:{EuclideanColorDistance.cs,IPaletteBuilder.cs,IStaticPaletteBuilder.cs,PopularityPaletteWithThreshold.cs} |Drawing/Sixel:{SixelEncoder.cs,SixelSupportDetector.cs,SixelSupportResult.cs,SixelToRender.cs} -|Drivers:{ComponentFactoryImpl.cs,Cursor.cs,CursorStyle.cs,Driver.cs,DriverImpl.cs,DriverRegistry.cs,IComponentFactory.cs,IDriver.cs,ISizeMonitor.cs,PlatformDetection.cs,SizeDetectionMode.cs,SizeMonitorImpl.cs,TuiPlatform.cs} +|Drivers:{ComponentFactoryImpl.cs,Cursor.cs,CursorStyle.cs,Driver.cs,DriverImpl.cs,DriverRegistry.cs,IComponentFactory.cs,IDriver.cs,ISizeMonitor.cs,PlatformDetection.cs,SizeDetectionMode.cs,SizeMonitorImpl.cs,TerminalDevice.cs,TuiPlatform.cs} |Drivers/AnsiDriver:{AnsiComponentFactory.cs,AnsiInput.cs,AnsiInputProcessor.cs,AnsiOutput.cs,AnsiPlatform.cs,AnsiSizeMonitor.cs,AnsiTerminalHelper.cs,FakeClipboard.cs} |Drivers/AnsiHandling:{AnsiEscapeSequence.cs,AnsiEscapeSequenceRequest.cs,AnsiKeyboardEncoder.cs,AnsiKeyboardParser.cs,AnsiKeyboardParserPattern.cs,AnsiKeyConverter.cs,AnsiMouseEncoder.cs,AnsiMouseParser.cs,AnsiRequestScheduler.cs,AnsiResponseExpectation.cs,AnsiResponseParser.cs,AnsiResponseParserBase.cs,AnsiResponseParserState.cs,AnsiResponseParserTInputRecord.cs,AnsiStartupGate.cs,AnsiStartupQuery.cs,CsiCursorPattern.cs,CsiKeyPattern.cs,EscAsAltPattern.cs,GenericHeld.cs,IAnsiResponseParser.cs,IAnsiStartupGate.cs,IHeld.cs,KittyKeyboardCapabilities.cs,KittyKeyboardFlags.cs,KittyKeyboardPattern.cs,KittyKeyboardProtocolDetector.cs,Osc8UrlLinker.cs,ProgressIndicator.cs,ReasonCannotSend.cs,Ss3Pattern.cs,StringHeld.cs,TerminalColorDetector.cs} |Drivers/AnsiHandling/EscSeqUtils:{EscSeqReqStatus.cs,EscSeqRequests.cs,EscSeqUtils.cs} @@ -47,46 +49,47 @@ Instead of embedding descriptions, it points to actual source files that agents |Drivers/Input:{ConsoleInputSource.cs,IInput.cs,IInputProcessor.cs,IInputSource.cs,InputImpl.cs,InputProcessorImpl.cs,InputRecord.cs,ITestableInput.cs,TestInputSource.cs} |Drivers/Keyboard:{ConsoleKeyInfoExtensions.cs,ConsoleKeyMapping.cs,IKeyConverter.cs,KeyCode.cs,VK.cs} |Drivers/Mouse:{MouseButtonClickTracker.cs,MouseInterpreter.cs} -|Drivers/Output:{IOutput.cs,IOutputBuffer.cs,OutputBase.cs,OutputBufferImpl.cs} +|Drivers/Output:{IOutput.cs,IOutputBuffer.cs,OutputBase.cs,OutputBufferImpl.cs,RasterImageCommand.cs} |Drivers/TerminalEnvironment:{ColorCapabilityLevel.cs,TerminalColorCapabilities.cs,TerminalEnvironmentDetector.cs} |Drivers/UnixHelpers:{SuspendHelper.cs,UnixClipboard.cs,UnixIOHelper.cs,UnixRawModeHelper.cs,UnixTerminalHelper.cs} |Drivers/WindowsDriver:{ClipboardImpl.cs,CursorVisibility.cs,IWindowsInput.cs,WindowsComponentFactory.cs,WindowsConsole.cs,WindowsInput.cs,WindowsInputProcessor.cs,WindowsKeyboardLayout.cs,WindowsKeyConverter.cs,WindowsKeyHelper.cs,WindowsOutput.cs} |Drivers/WindowsHelpers:{NetWinVTConsole.cs,WindowsConsoleHelper.cs,WindowsVTInputHelper.cs,WindowsVTOutputHelper.cs} |FileServices:{DefaultSearchMatcher.cs,FileSystemColorProvider.cs,FileSystemIconProvider.cs,FileSystemInfoStats.cs,FileSystemTreeBuilder.cs,IFileOperations.cs,ISearchMatcher.cs} -|Input:{Command.cs,CommandBinding.cs,CommandBindingsBase.cs,CommandBridge.cs,CommandContext.cs,CommandContextExtensions.cs,CommandEventArgs.cs,CommandOutcome.cs,CommandRouting.cs,IAcceptTarget.cs,ICommandBinding.cs,ICommandContext.cs} +|Input:{Command.cs,CommandBinding.cs,CommandBindingsBase.cs,CommandBridge.cs,CommandContext.cs,CommandContextExtensions.cs,CommandEventArgs.cs,CommandOutcome.cs,CommandRouting.cs,IAcceptTarget.cs,ICommandBinding.cs,ICommandContext.cs,PastedEventArgs.cs,PasteEventArgs.cs,PastePayload.cs,PastingEventArgs.cs} |Input/Keyboard:{Bind.cs,Key.cs,KeyBinding.cs,KeyBindings.cs,KeyChangedEventArgs.cs,KeyEqualityComparer.cs,KeyEventType.cs,KeystrokeNavigatorEventArgs.cs,ModifierKey.cs,PlatformKeyBinding.cs} -|Input/Mouse:{GrabMouseEventArgs.cs,Mouse.cs,MouseBinding.cs,MouseBindings.cs,MouseFlags.cs,MouseFlagsChangedEventArgs.cs} +|Input/Mouse:{BindMouse.cs,GrabMouseEventArgs.cs,Mouse.cs,MouseBinding.cs,MouseBindings.cs,MouseFlags.cs,MouseFlagsChangedEventArgs.cs,PlatformMouseBinding.cs} |Resources:{GlobalResources.cs,ResourceManagerWrapper.cs,Strings.Designer.cs} |Testing:{IInputInjector.cs,InputInjectionEvent.cs,InputInjectionExtensions.cs,InputInjectionMode.cs,InputInjectionOptions.cs,InputInjector.cs} |Text:{NerdFonts.cs,RuneExtensions.cs,StringExtensions.cs,TextDirection.cs,TextFormatter.cs} |Time:{FuncTimeProvider.cs,ITimeProvider.cs,ITimer.cs,SystemTimeProvider.cs,VirtualTimeProvider.cs} -|ViewBase:{DrawAdornmentsEventArgs.cs,DrawContext.cs,DrawEventArgs.cs,IDesignable.cs,IValue.cs,View.Adornments.cs,View.Arrangement.cs,View.Command.cs,View.Content.cs,View.cs,View.Cursor.cs,View.Diagnostics.cs,View.Drawing.Adornments.cs,View.Drawing.Attribute.cs,View.Drawing.Clipping.cs,View.Drawing.cs,View.Drawing.LineCanvas.cs,View.Drawing.Primitives.cs,View.Drawing.Scheme.cs,View.Hierarchy.cs,View.Keyboard.cs,View.Layout.cs,View.Navigation.cs,View.NeedsDraw.cs,View.ScrollBars.cs,View.Text.cs,ViewCollectionHelpers.cs,ViewDiagnosticFlags.cs,ViewEventArgs.cs,ViewExtensions.cs,ViewportSettingsFlags.cs,WeakReferenceExtensions.cs} +|ViewBase:{DrawAdornmentsEventArgs.cs,DrawContext.cs,DrawEventArgs.cs,IDesignable.cs,IValue.cs,IValueParser.cs,View.Adornments.cs,View.Arrangement.cs,View.Command.cs,View.Content.cs,View.cs,View.Cursor.cs,View.Diagnostics.cs,View.Drawing.Adornments.cs,View.Drawing.Attribute.cs,View.Drawing.Clipping.cs,View.Drawing.cs,View.Drawing.LineCanvas.cs,View.Drawing.Primitives.cs,View.Drawing.Scheme.cs,View.Hierarchy.cs,View.Keyboard.cs,View.Layout.cs,View.Navigation.cs,View.NeedsDraw.cs,View.Paste.cs,View.ScrollBars.cs,View.Text.cs,ViewCollectionHelpers.cs,ViewDiagnosticFlags.cs,ViewEventArgs.cs,ViewExtensions.cs,ViewportSettingsFlags.cs,WeakReferenceExtensions.cs} |ViewBase/Adornment:{AdornmentImpl.cs,AdornmentView.cs,ArrangeButtons.cs,Arranger.cs,ArrangerButton.cs,Border.cs,BorderSettings.cs,BorderView.Arrangement.cs,BorderView.cs,IAdornment.cs,IAdornmentView.cs,ITitleView.cs,Margin.cs,MarginView.cs,Padding.cs,PaddingView.cs,ShadowStyles.cs,ShadowView.cs,TabLayoutContext.cs,TitleView.cs} |ViewBase/Helpers:{StackExtensions.cs} |ViewBase/Layout:{AddOrSubtract.cs,Aligner.cs,Alignment.cs,AlignmentModes.cs,Dim.cs,DimAbsolute.cs,DimAuto.cs,DimAutoStyle.cs,DimCombine.cs,Dimension.cs,DimFill.cs,DimFunc.cs,DimPercent.cs,DimPercentMode.cs,DimView.cs,LayoutEventArgs.cs,LayoutException.cs,Pos.cs,PosAbsolute.cs,PosAlign.cs,PosAnchorEnd.cs,PosCenter.cs,PosCombine.cs,PosFunc.cs,PosPercent.cs,PosView.cs,Side.cs,SizeChangedEventArgs.cs,SuperViewChangedEventArgs.cs,ViewArrangement.cs,ViewManipulator.cs} |ViewBase/Mouse:{IMouseHoldRepeater.cs,MouseHoldRepeaterImpl.cs,MouseState.cs,View.Mouse.cs} |ViewBase/Navigation:{AdvanceFocusEventArgs.cs,FocusEventArgs.cs,NavigationDirection.cs,TabBehavior.cs} |ViewBase/Orientation:{IOrientation.cs,Orientation.cs,OrientationHelper.cs} -|Views:{Bar.cs,Button.cs,CheckBox.cs,CheckState.cs,DatePicker.cs,Dialog.cs,DialogTResult.cs,DropDownList.cs,DropDownListTEnum.cs,FrameView.cs,HexView.cs,HexViewEventArgs.cs,Label.cs,Line.cs,Link.cs,MessageBox.cs,NumericUpDown.cs,ProgressBar.cs,Prompt.cs,PromptExtensions.cs,ReadOnlyCollectionExtensions.cs,Shortcut.cs,StatusBar.cs,Tabs.cs,Window.cs} +|Views:{Bar.cs,Button.cs,CheckBox.cs,CheckState.cs,Code.cs,CodeRoleLegendHighlighter.cs,DatePicker.cs,Dialog.cs,DialogTResult.cs,DropDownList.cs,DropDownListTEnum.cs,FrameView.cs,HexView.cs,HexViewEventArgs.cs,Label.cs,Line.cs,Link.cs,MessageBox.cs,NumericUpDown.cs,ProgressBar.cs,Prompt.cs,PromptExtensions.cs,ReadOnlyCollectionExtensions.cs,Shortcut.cs,StatusBar.cs,Tabs.cs,Window.cs} |Views/Autocomplete:{AppendAutocomplete.cs,AutocompleteBase.cs,AutocompleteContext.cs,AutocompleteFilepathContext.cs,IAutocomplete.cs,ISuggestionGenerator.cs,PopupAutocomplete.cs,PopupAutocomplete.PopUp.cs,SingleWordSuggestionGenerator.cs,Suggestion.cs} |Views/CharMap:{CharMap.cs,UcdApiClient.cs,UnicodeRange.cs} |Views/CollectionNavigation:{CollectionNavigator.cs,CollectionNavigatorBase.cs,DefaultCollectionNavigatorMatcher.cs,ICollectionNavigator.cs,ICollectionNavigatorMatcher.cs,IListCollectionNavigator.cs,TableCollectionNavigator.cs} |Views/Color:{AttributePicker.cs,BBar.cs,ColorBar.cs,ColorModelStrategy.cs,ColorPicker.16.cs,ColorPicker.cs,ColorPicker.Style.cs,GBar.cs,HueBar.cs,IColorBar.cs,LightnessBar.cs,RBar.cs,SaturationBar.cs,ValueBar.cs} -|Views/FileDialogs:{AllowedType.cs,DefaultFileOperations.cs,FileDialog.Commands.cs,FileDialog.cs,FileDialog.Navigation.cs,FileDialog.TableView.cs,FileDialogCollectionNavigator.cs,FileDialogHistory.cs,FileDialogState.cs,FileDialogStyle.cs,FileDialogTableSource.cs,FilesSelectedEventArgs.cs,FileSystemCollectionNavigationMatcher.cs,OpenDialog.cs,OpenMode.cs,SaveDialog.cs} +|Views/FileDialogs:{AllowedType.cs,AllowedTypeAny.cs,DefaultFileOperations.cs,FileDialog.Commands.cs,FileDialog.cs,FileDialog.Navigation.cs,FileDialog.TableView.cs,FileDialogCollectionNavigator.cs,FileDialogHistory.cs,FileDialogState.cs,FileDialogStyle.cs,FileDialogTableSource.cs,FilesSelectedEventArgs.cs,FileSystemCollectionNavigationMatcher.cs,IAllowedType.cs,OpenDialog.cs,OpenMode.cs,SaveDialog.cs} |Views/GraphView:{Axis.cs,AxisIncrementToRender.cs,BarSeriesBar.cs,GraphCellToRender.cs,GraphView.cs,HorizontalAxis.cs,IAnnotation.cs,ISeries.cs,LegendAnnotation.cs,LineF.cs,MultiBarSeries.cs,PathAnnotation.cs,ScatterSeries.cs,Series.cs,TextAnnotation.cs,VerticalAxis.cs} -|Views/LinearRange:{LinearRange.cs,LinearRangeAttributes.cs,LinearRangeConfiguration.cs,LinearRangeEventArgs.cs,LinearRangeOption.cs,LinearRangeOptionEventArgs.cs,LinearRangeStyle.cs,LinearRangeType.cs} +|Views/ImageView:{ImageView.cs,ImageView.Drawing.cs,ImageView.Geometry.cs,ImageView.Input.cs,ImageView.Lifecycle.cs,ImageView.Render.cs,ImageView.Scaling.cs} +|Views/LinearRange:{LinearMultiSelector.cs,LinearMultiSelectorT.cs,LinearRange.cs,LinearRangeAttributes.cs,LinearRangeConfiguration.cs,LinearRangeDefaults.cs,LinearRangeEventArgs.cs,LinearRangeOption.cs,LinearRangeOptionEventArgs.cs,LinearRangeRenderMode.cs,LinearRangeSpan.cs,LinearRangeSpanKind.cs,LinearRangeStyle.cs,LinearRangeT.cs,LinearRangeViewBase.cs,LinearSelector.cs,LinearSelectorT.cs} |Views/ListView:{IListDataSource.cs,ListView.Commands.cs,ListView.cs,ListView.Drawing.cs,ListView.Movement.cs,ListView.Selection.cs,ListViewEventArgs.cs,ListViewT.cs,ListWrapper.cs} -|Views/Markdown:{InlineRun.cs,IntermediateBlock.cs,Markdown.cs,MarkdownCodeBlock.cs,MarkdownImageResolver.cs,MarkdownInlineParser.cs,MarkdownLinkEventArgs.cs,MarkdownTable.cs,MarkdownView.Drawing.cs,MarkdownView.Layout.cs,MarkdownView.Mouse.cs,MarkdownView.Parsing.cs,RenderedLine.cs,TableData.cs} +|Views/Markdown:{InlineRun.cs,IntermediateBlock.cs,Markdown.cs,MarkdownCodeBlock.cs,MarkdownImageResolver.cs,MarkdownInlineParser.cs,MarkdownLinkEventArgs.cs,MarkdownTable.cs,MarkdownView.Ansi.cs,MarkdownView.Drawing.cs,MarkdownView.Layout.cs,MarkdownView.Mouse.cs,MarkdownView.Parsing.cs,MarkdownView.Selection.cs,RenderedLine.cs,TableData.cs} |Views/Menu:{IMenuBarEntry.cs,Menu.cs,MenuBar.cs,MenuBarItem.cs,MenuItem.cs,PopoverMenu.cs} |Views/Runnable:{Runnable.cs,RunnableTResult.cs,RunnableWrapper.cs} |Views/ScrollBar:{ScrollBar.cs,ScrollBarVisibilityMode.cs,ScrollButton.cs,ScrollSlider.cs} |Views/Selectors:{FlagSelector.cs,FlagSelectorTEnum.cs,OptionSelector.cs,OptionSelectorTEnum.cs,SelectorBase.cs,SelectorStyles.cs} |Views/SpinnerView:{SpinnerStyle.cs,SpinnerView.cs} -|Views/TableView:{CellActivatedEventArgs.cs,CellColorGetterArgs.cs,CellToggledEventArgs.cs,CheckBoxTableSourceWrapper.cs,CheckBoxTableSourceWrapperByIndex.cs,CheckBoxTableSourceWrapperByObject.cs,ColumnStyle.cs,DataTableSource.cs,EnumerableTableSource.cs,IEnumerableTableSource.cs,ITableSource.cs,ListColumnStyle.cs,ListTableSource.cs,RowColorGetterArgs.cs,SelectedCellChangedEventArgs.cs,TableSelection.cs,TableStyle.cs,TableView.CellMapping.cs,TableView.cs,TableView.Drawing.cs,TableView.Mouse.cs,TableView.Navigation.cs,TableView.Selection.cs,TreeTableSource.cs} +|Views/TableView:{CellColorGetterArgs.cs,CheckBoxTableSourceWrapper.cs,CheckBoxTableSourceWrapperByIndex.cs,CheckBoxTableSourceWrapperByObject.cs,ColumnStyle.cs,DataTableSource.cs,EnumerableTableSource.cs,HeaderColorGetterArgs.cs,IEnumerableTableSource.cs,ITableSource.cs,ListColumnStyle.cs,ListTableSource.cs,RowColorGetterArgs.cs,TableSelection.cs,TableSelectionRegion.cs,TableStyle.cs,TableView.CellMapping.cs,TableView.Content.cs,TableView.cs,TableView.Drawing.cs,TableView.Navigation.cs,TableView.Selection.cs,TreeTableSource.cs} |Views/TextInput:{ContentsChangedEventArgs.cs,DateEditor.cs,DateTextProvider.cs,HistoryText.cs,HistoryTextItemEventArgs.cs,ITextValidateProvider.cs,NetMaskedTextProvider.cs,TextEditingLineStatus.cs,TextModel.cs,TextRegexProvider.cs,TextValidateField.cs,TimeEditor.cs,TimeTextProvider.cs} |Views/TextInput/TextField:{TextField.Commands.cs,TextField.cs,TextField.Drawing.cs,TextField.History.cs,TextField.Keyboard.cs,TextField.Mouse.cs,TextField.Selection.cs,TextField.Text.cs,TextFieldAutocomplete.cs} |Views/TextInput/TextView:{TextView.Commands.cs,TextView.cs,TextView.Drawing.cs,TextView.Files.cs,TextView.Find.cs,TextView.History.cs,TextView.Keyboard.cs,TextView.Mouse.cs,TextView.Movement.cs,TextView.Scrolling.cs,TextView.Selection.cs,TextView.Text.cs,TextView.WordWrap.cs,TextViewAutocomplete.cs,WordWrapManager.cs} -|Views/TreeView:{AspectGetterDelegate.cs,Branch.cs,DelegateTreeBuilder.cs,DrawTreeViewLineEventArgs.cs,ITreeBuilder.cs,ITreeNode.cs,ITreeView.cs,ITreeViewFilter.cs,ObjectActivatedEventArgs.cs,SelectionChangedEventArgs.cs,TreeBuilder.cs,TreeNode.cs,TreeNodeBuilder.cs,TreeSelection.cs,TreeStyle.cs,TreeView.cs,TreeView.Drawing.cs,TreeView.Mouse.cs,TreeView.Navigation.cs,TreeViewCollectionNavigatorMatcher.cs,TreeViewT.cs,TreeViewTextFilter.cs} +|Views/TreeView:{AspectGetterDelegate.cs,Branch.cs,CheckedChangedEventArgs.cs,DelegateTreeBuilder.cs,DrawTreeViewLineEventArgs.cs,ITreeBuilder.cs,ITreeNode.cs,ITreeView.cs,ITreeViewFilter.cs,ObjectActivatedEventArgs.cs,SelectionChangedEventArgs.cs,TreeBuilder.cs,TreeNode.cs,TreeNodeBuilder.cs,TreeSelection.cs,TreeStyle.cs,TreeView.cs,TreeView.Drawing.cs,TreeView.Mouse.cs,TreeView.Navigation.cs,TreeViewCollectionNavigatorMatcher.cs,TreeViewT.cs,TreeViewTextFilter.cs} |Views/Wizard:{Wizard.cs,WizardStep.cs} @@ -96,7 +99,7 @@ Instead of embedding descriptions, it points to actual source files that agents [Terminal.Gui Docs Index]|root: ./docfx/docs |IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning. Read files when needed. -|.:{ansihandling.md,application.md,arrangement.md,borders.md,cancellable-work-pattern.md,command-diagrams.md,command.md,config.md,cursor.md,dimauto.md,drawing.md,drivers.md,events.md,getting-started.md,index.md,input-injection.md,keyboard.md,layout.md,lexicon.md,logging.md,menus.md,migratingfromv1.md,mouse.md,multitasking.md,navigation.md,newinv2.md,Popovers.md,prompt.md,scheme.md,scrolling.md,shortcut.md,showcase.md,tableview.md,treeview.md,View.md,views.md} +|.:{ansihandling.md,application.md,arrangement.md,borders.md,bracketed-paste.md,cancellable-work-pattern.md,command-diagrams.md,command.md,config.md,cursor.md,dimauto.md,drawing.md,drivers.md,events.md,getting-started.md,index.md,input-injection.md,keyboard.md,layout.md,lexicon.md,logging.md,menus.md,migrate-cm-to-mec.md,migratingfromv1.md,mouse.md,multitasking.md,navigation.md,newinv2.md,Popovers.md,prompt.md,scheme.md,scrolling.md,shortcut.md,showcase.md,tableview.md,treeview.md,View.md,views.md} --- diff --git a/AGENTS.md b/AGENTS.md index af69c6f72f..47697b80a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -210,7 +210,7 @@ See `.claude/cookbook/` for common UI patterns: |prompt.md|MessageBox,input dialogs [Config & Advanced] -|config.md|ConfigurationManager,themes,JSON config +|config.md|TuiConfigurationBuilder,themes,JSON config |multitasking.md|Background ops,Invoke,threading |logging.md|ILogger,debug output |ansihandling.md|ANSI escape parsing @@ -230,7 +230,7 @@ See `.claude/cookbook/` for common UI patterns: |namespace-input.md|Key,Mouse,Command,ICommandContext |namespace-drawing.md|Attribute,Color,LineCanvas,Cell |namespace-drivers.md|IDriver,DriverRegistry -|namespace-configuration.md|ConfigurationManager,themes +|namespace-configuration.md|TuiConfigurationBuilder,themes |namespace-text.md|Text processing,autocomplete |namespace-fileservices.md|File dialogs ``` @@ -245,7 +245,7 @@ See `.claude/cookbook/` for common UI patterns: |IMPORTANT: Prefer retrieval-led reasoning over pre-training-led reasoning. Read files when needed. |.:{ModuleInitializers.cs} |App:{Application.cs,ApplicationImpl.cs,ApplicationImpl.Driver.cs,ApplicationImpl.Lifecycle.cs,ApplicationImpl.Run.cs,ApplicationImpl.Screen.cs,ApplicationModelUsage.cs,ApplicationNavigation.cs,ApplicationPopover.cs,ApplicationToolTip.cs,AppModel.cs,IApplication.cs,Logging.cs,NotInitializedException.cs} -|App/Clipboard:{Clipboard.cs,ClipboardBase.cs,ClipboardProcessRunner.cs,IClipboard.cs} +|App/Clipboard:{Clipboard.cs,ClipboardBase.cs,ClipboardProcessRunner.cs,ClipboardProcessRunnerImpl.cs,IClipboard.cs,IClipboardProcessRunner.cs} |App/CWP:{CancelEventArgs.cs,CWPEventHelper.cs,CWPPropertyHelper.cs,CWPWorkflowHelper.cs,EventArgs.cs,ResultEventArgs.cs,ValueChangedEventArgs.cs,ValueChangingEventArgs.cs} |App/Keyboard:{ApplicationKeyboard.cs,IKeyboard.cs} |App/Legacy:{Application.Clipboard.cs,Application.Driver.cs,Application.Keyboard.cs,Application.Lifecycle.cs,Application.Mouse.cs,Application.Navigation.cs,Application.Popovers.cs,Application.Run.cs,Application.Screen.cs,Application.TopRunnable.cs} @@ -255,14 +255,16 @@ See `.claude/cookbook/` for common UI patterns: |App/Runnable:{IRunnable.cs,SessionToken.cs,SessionTokenEventArgs.cs} |App/Timeout:{ITimedEvents.cs,LogarithmicTimeout.cs,SmoothAcceleratingTimeout.cs,TimedEvents.cs,Timeout.cs,TimeoutEventArgs.cs} |App/Tracing:{ITraceBackend.cs,ListBackend.cs,LoggingBackend.cs,NullBackend.cs,Trace.cs,TraceCategory.cs,TraceEntry.cs,TraceScope.cs} -|Configuration:{AppSettingsScope.cs,AttributeJsonConverter.cs,ColorJsonConverter.cs,ConcurrentDictionaryJsonConverter.cs,ConfigLocations.cs,ConfigProperty.cs,ConfigurationManager.cs,ConfigurationManagerEventArgs.cs,ConfigurationManagerNotEnabledException.cs,ConfigurationPropertyAttribute.cs,DeepCloner.cs,DictionaryJsonConverter.cs,KeyArrayJsonConverter.cs,KeyCodeJsonConverter.cs,KeyJsonConverter.cs,RuneJsonConverter.cs,SchemeJsonConverter.cs,SchemeManager.cs,Scope.cs,ScopeJsonConverter.cs,SettingsScope.cs,SourceGenerationContext.cs,SourcesManager.cs,ThemeManager.cs,ThemeScope.cs,TraceCategoryJsonConverter.cs} +|Configuration:{AttributeJsonConverter.cs,ColorJsonConverter.cs,ConcurrentDictionaryJsonConverter.cs,DictionaryJsonConverter.cs,KeyArrayJsonConverter.cs,KeyCodeJsonConverter.cs,KeyJsonConverter.cs,MouseFlagsArrayJsonConverter.cs,RuneJsonConverter.cs,SchemeJsonConverter.cs,SchemeManager.cs,SourceGenerationContext.cs,ThemeManager.cs,TraceCategoryJsonConverter.cs,TuiJsonErrors.cs,TuiSerializerContext.cs} +|Configuration/Settings:{ApplicationSettings.cs,ButtonSettings.cs,CharMapSettings.cs,CheckBoxSettings.cs,DialogSettings.cs,DriverSettings.cs,FileDialogSettings.cs,FileDialogStyleSettings.cs,FrameViewSettings.cs,GlyphSettings.cs,HexViewSettings.cs,ISchemeManager.cs,IThemeManager.cs,KeySettings.cs,LinearRangeSettings.cs,MecThemeManager.cs,MenuBarSettings.cs,MenuSettings.cs,MessageBoxSettings.cs,NerdFontsSettings.cs,PopoverMenuSettings.cs,SelectorBaseSettings.cs,StatusBarSettings.cs,TextFieldSettings.cs,TextViewSettings.cs,ThemeCatalog.cs,ThemeChanges.cs,ThemeSettings.cs,TraceSettings.cs,TuiConfigurationBuilder.cs,TuiConfigurationExtensions.cs,WindowSettings.cs} |Drawing:{Attribute.cs,Cell.cs,CellEventArgs.cs,FillPair.cs,Glyphs.cs,Gradient.cs,GradientFill.cs,GraphemeHelper.cs,IFill.cs,Region.cs,RegionOp.cs,Ruler.cs,Scheme.cs,Schemes.cs,SolidFill.cs,TextStyle.cs,Thickness.cs,VisualRole.cs,VisualRoleEventArgs.cs} |Drawing/Color:{AnsiColorCode.cs,Color.ColorExtensions.cs,Color.ColorName.cs,Color.ColorParseException.cs,Color.cs,Color.Formatting.cs,Color.Operators.cs,ColorModel.cs,ColorQuantizer.cs,ColorStrings.cs,IColorDistance.cs,IColorNameResolver.cs,ICustomColorFormatter.cs,StandardColor.cs,StandardColors.cs,StandardColorsNameResolver.cs} +|Drawing/Kitty:{KittyGraphicsEncoder.cs,KittyGraphicsSupportDetector.cs,KittyGraphicsSupportResult.cs} |Drawing/LineCanvas:{IntersectionDefinition.cs,IntersectionRuneType.cs,IntersectionType.cs,LineCanvas.cs,LineDirections.cs,LineStyle.cs,StraightLine.cs,StraightLineExtensions.cs} |Drawing/Markdown:{ISyntaxHighlighter.cs,MarkdownAttributeHelper.cs,MarkdownStyleRole.cs,StyledSegment.cs,TextMateSyntaxHighlighter.cs} -|Drawing/Quant:{EuclideanColorDistance.cs,IPaletteBuilder.cs,PopularityPaletteWithThreshold.cs} +|Drawing/Quant:{EuclideanColorDistance.cs,IPaletteBuilder.cs,IStaticPaletteBuilder.cs,PopularityPaletteWithThreshold.cs} |Drawing/Sixel:{SixelEncoder.cs,SixelSupportDetector.cs,SixelSupportResult.cs,SixelToRender.cs} -|Drivers:{ComponentFactoryImpl.cs,Cursor.cs,CursorStyle.cs,Driver.cs,DriverImpl.cs,DriverRegistry.cs,IComponentFactory.cs,IDriver.cs,ISizeMonitor.cs,PlatformDetection.cs,SizeDetectionMode.cs,SizeMonitorImpl.cs,TuiPlatform.cs} +|Drivers:{ComponentFactoryImpl.cs,Cursor.cs,CursorStyle.cs,Driver.cs,DriverImpl.cs,DriverRegistry.cs,IComponentFactory.cs,IDriver.cs,ISizeMonitor.cs,PlatformDetection.cs,SizeDetectionMode.cs,SizeMonitorImpl.cs,TerminalDevice.cs,TuiPlatform.cs} |Drivers/AnsiDriver:{AnsiComponentFactory.cs,AnsiInput.cs,AnsiInputProcessor.cs,AnsiOutput.cs,AnsiPlatform.cs,AnsiSizeMonitor.cs,AnsiTerminalHelper.cs,FakeClipboard.cs} |Drivers/AnsiHandling:{AnsiEscapeSequence.cs,AnsiEscapeSequenceRequest.cs,AnsiKeyboardEncoder.cs,AnsiKeyboardParser.cs,AnsiKeyboardParserPattern.cs,AnsiKeyConverter.cs,AnsiMouseEncoder.cs,AnsiMouseParser.cs,AnsiRequestScheduler.cs,AnsiResponseExpectation.cs,AnsiResponseParser.cs,AnsiResponseParserBase.cs,AnsiResponseParserState.cs,AnsiResponseParserTInputRecord.cs,AnsiStartupGate.cs,AnsiStartupQuery.cs,CsiCursorPattern.cs,CsiKeyPattern.cs,EscAsAltPattern.cs,GenericHeld.cs,IAnsiResponseParser.cs,IAnsiStartupGate.cs,IHeld.cs,KittyKeyboardCapabilities.cs,KittyKeyboardFlags.cs,KittyKeyboardPattern.cs,KittyKeyboardProtocolDetector.cs,Osc8UrlLinker.cs,ProgressIndicator.cs,ReasonCannotSend.cs,Ss3Pattern.cs,StringHeld.cs,TerminalColorDetector.cs} |Drivers/AnsiHandling/EscSeqUtils:{EscSeqReqStatus.cs,EscSeqRequests.cs,EscSeqUtils.cs} @@ -270,46 +272,47 @@ See `.claude/cookbook/` for common UI patterns: |Drivers/Input:{ConsoleInputSource.cs,IInput.cs,IInputProcessor.cs,IInputSource.cs,InputImpl.cs,InputProcessorImpl.cs,InputRecord.cs,ITestableInput.cs,TestInputSource.cs} |Drivers/Keyboard:{ConsoleKeyInfoExtensions.cs,ConsoleKeyMapping.cs,IKeyConverter.cs,KeyCode.cs,VK.cs} |Drivers/Mouse:{MouseButtonClickTracker.cs,MouseInterpreter.cs} -|Drivers/Output:{IOutput.cs,IOutputBuffer.cs,OutputBase.cs,OutputBufferImpl.cs} +|Drivers/Output:{IOutput.cs,IOutputBuffer.cs,OutputBase.cs,OutputBufferImpl.cs,RasterImageCommand.cs} |Drivers/TerminalEnvironment:{ColorCapabilityLevel.cs,TerminalColorCapabilities.cs,TerminalEnvironmentDetector.cs} |Drivers/UnixHelpers:{SuspendHelper.cs,UnixClipboard.cs,UnixIOHelper.cs,UnixRawModeHelper.cs,UnixTerminalHelper.cs} |Drivers/WindowsDriver:{ClipboardImpl.cs,CursorVisibility.cs,IWindowsInput.cs,WindowsComponentFactory.cs,WindowsConsole.cs,WindowsInput.cs,WindowsInputProcessor.cs,WindowsKeyboardLayout.cs,WindowsKeyConverter.cs,WindowsKeyHelper.cs,WindowsOutput.cs} |Drivers/WindowsHelpers:{NetWinVTConsole.cs,WindowsConsoleHelper.cs,WindowsVTInputHelper.cs,WindowsVTOutputHelper.cs} |FileServices:{DefaultSearchMatcher.cs,FileSystemColorProvider.cs,FileSystemIconProvider.cs,FileSystemInfoStats.cs,FileSystemTreeBuilder.cs,IFileOperations.cs,ISearchMatcher.cs} -|Input:{Command.cs,CommandBinding.cs,CommandBindingsBase.cs,CommandBridge.cs,CommandContext.cs,CommandContextExtensions.cs,CommandEventArgs.cs,CommandOutcome.cs,CommandRouting.cs,IAcceptTarget.cs,ICommandBinding.cs,ICommandContext.cs} +|Input:{Command.cs,CommandBinding.cs,CommandBindingsBase.cs,CommandBridge.cs,CommandContext.cs,CommandContextExtensions.cs,CommandEventArgs.cs,CommandOutcome.cs,CommandRouting.cs,IAcceptTarget.cs,ICommandBinding.cs,ICommandContext.cs,PastedEventArgs.cs,PasteEventArgs.cs,PastePayload.cs,PastingEventArgs.cs} |Input/Keyboard:{Bind.cs,Key.cs,KeyBinding.cs,KeyBindings.cs,KeyChangedEventArgs.cs,KeyEqualityComparer.cs,KeyEventType.cs,KeystrokeNavigatorEventArgs.cs,ModifierKey.cs,PlatformKeyBinding.cs} -|Input/Mouse:{GrabMouseEventArgs.cs,Mouse.cs,MouseBinding.cs,MouseBindings.cs,MouseFlags.cs,MouseFlagsChangedEventArgs.cs} +|Input/Mouse:{BindMouse.cs,GrabMouseEventArgs.cs,Mouse.cs,MouseBinding.cs,MouseBindings.cs,MouseFlags.cs,MouseFlagsChangedEventArgs.cs,PlatformMouseBinding.cs} |Resources:{GlobalResources.cs,ResourceManagerWrapper.cs,Strings.Designer.cs} |Testing:{IInputInjector.cs,InputInjectionEvent.cs,InputInjectionExtensions.cs,InputInjectionMode.cs,InputInjectionOptions.cs,InputInjector.cs} |Text:{NerdFonts.cs,RuneExtensions.cs,StringExtensions.cs,TextDirection.cs,TextFormatter.cs} |Time:{FuncTimeProvider.cs,ITimeProvider.cs,ITimer.cs,SystemTimeProvider.cs,VirtualTimeProvider.cs} -|ViewBase:{DrawAdornmentsEventArgs.cs,DrawContext.cs,DrawEventArgs.cs,IDesignable.cs,IValue.cs,View.Adornments.cs,View.Arrangement.cs,View.Command.cs,View.Content.cs,View.cs,View.Cursor.cs,View.Diagnostics.cs,View.Drawing.Adornments.cs,View.Drawing.Attribute.cs,View.Drawing.Clipping.cs,View.Drawing.cs,View.Drawing.LineCanvas.cs,View.Drawing.Primitives.cs,View.Drawing.Scheme.cs,View.Hierarchy.cs,View.Keyboard.cs,View.Layout.cs,View.Navigation.cs,View.NeedsDraw.cs,View.ScrollBars.cs,View.Text.cs,ViewCollectionHelpers.cs,ViewDiagnosticFlags.cs,ViewEventArgs.cs,ViewExtensions.cs,ViewportSettingsFlags.cs,WeakReferenceExtensions.cs} +|ViewBase:{DrawAdornmentsEventArgs.cs,DrawContext.cs,DrawEventArgs.cs,IDesignable.cs,IValue.cs,IValueParser.cs,View.Adornments.cs,View.Arrangement.cs,View.Command.cs,View.Content.cs,View.cs,View.Cursor.cs,View.Diagnostics.cs,View.Drawing.Adornments.cs,View.Drawing.Attribute.cs,View.Drawing.Clipping.cs,View.Drawing.cs,View.Drawing.LineCanvas.cs,View.Drawing.Primitives.cs,View.Drawing.Scheme.cs,View.Hierarchy.cs,View.Keyboard.cs,View.Layout.cs,View.Navigation.cs,View.NeedsDraw.cs,View.Paste.cs,View.ScrollBars.cs,View.Text.cs,ViewCollectionHelpers.cs,ViewDiagnosticFlags.cs,ViewEventArgs.cs,ViewExtensions.cs,ViewportSettingsFlags.cs,WeakReferenceExtensions.cs} |ViewBase/Adornment:{AdornmentImpl.cs,AdornmentView.cs,ArrangeButtons.cs,Arranger.cs,ArrangerButton.cs,Border.cs,BorderSettings.cs,BorderView.Arrangement.cs,BorderView.cs,IAdornment.cs,IAdornmentView.cs,ITitleView.cs,Margin.cs,MarginView.cs,Padding.cs,PaddingView.cs,ShadowStyles.cs,ShadowView.cs,TabLayoutContext.cs,TitleView.cs} |ViewBase/Helpers:{StackExtensions.cs} |ViewBase/Layout:{AddOrSubtract.cs,Aligner.cs,Alignment.cs,AlignmentModes.cs,Dim.cs,DimAbsolute.cs,DimAuto.cs,DimAutoStyle.cs,DimCombine.cs,Dimension.cs,DimFill.cs,DimFunc.cs,DimPercent.cs,DimPercentMode.cs,DimView.cs,LayoutEventArgs.cs,LayoutException.cs,Pos.cs,PosAbsolute.cs,PosAlign.cs,PosAnchorEnd.cs,PosCenter.cs,PosCombine.cs,PosFunc.cs,PosPercent.cs,PosView.cs,Side.cs,SizeChangedEventArgs.cs,SuperViewChangedEventArgs.cs,ViewArrangement.cs,ViewManipulator.cs} |ViewBase/Mouse:{IMouseHoldRepeater.cs,MouseHoldRepeaterImpl.cs,MouseState.cs,View.Mouse.cs} |ViewBase/Navigation:{AdvanceFocusEventArgs.cs,FocusEventArgs.cs,NavigationDirection.cs,TabBehavior.cs} |ViewBase/Orientation:{IOrientation.cs,Orientation.cs,OrientationHelper.cs} -|Views:{Bar.cs,Button.cs,CheckBox.cs,CheckState.cs,DatePicker.cs,Dialog.cs,DialogTResult.cs,DropDownList.cs,DropDownListTEnum.cs,FrameView.cs,HexView.cs,HexViewEventArgs.cs,Label.cs,Line.cs,Link.cs,MessageBox.cs,NumericUpDown.cs,ProgressBar.cs,Prompt.cs,PromptExtensions.cs,ReadOnlyCollectionExtensions.cs,Shortcut.cs,StatusBar.cs,Tabs.cs,Window.cs} +|Views:{Bar.cs,Button.cs,CheckBox.cs,CheckState.cs,Code.cs,CodeRoleLegendHighlighter.cs,DatePicker.cs,Dialog.cs,DialogTResult.cs,DropDownList.cs,DropDownListTEnum.cs,FrameView.cs,HexView.cs,HexViewEventArgs.cs,Label.cs,Line.cs,Link.cs,MessageBox.cs,NumericUpDown.cs,ProgressBar.cs,Prompt.cs,PromptExtensions.cs,ReadOnlyCollectionExtensions.cs,Shortcut.cs,StatusBar.cs,Tabs.cs,Window.cs} |Views/Autocomplete:{AppendAutocomplete.cs,AutocompleteBase.cs,AutocompleteContext.cs,AutocompleteFilepathContext.cs,IAutocomplete.cs,ISuggestionGenerator.cs,PopupAutocomplete.cs,PopupAutocomplete.PopUp.cs,SingleWordSuggestionGenerator.cs,Suggestion.cs} |Views/CharMap:{CharMap.cs,UcdApiClient.cs,UnicodeRange.cs} |Views/CollectionNavigation:{CollectionNavigator.cs,CollectionNavigatorBase.cs,DefaultCollectionNavigatorMatcher.cs,ICollectionNavigator.cs,ICollectionNavigatorMatcher.cs,IListCollectionNavigator.cs,TableCollectionNavigator.cs} |Views/Color:{AttributePicker.cs,BBar.cs,ColorBar.cs,ColorModelStrategy.cs,ColorPicker.16.cs,ColorPicker.cs,ColorPicker.Style.cs,GBar.cs,HueBar.cs,IColorBar.cs,LightnessBar.cs,RBar.cs,SaturationBar.cs,ValueBar.cs} -|Views/FileDialogs:{AllowedType.cs,DefaultFileOperations.cs,FileDialog.Commands.cs,FileDialog.cs,FileDialog.Navigation.cs,FileDialog.TableView.cs,FileDialogCollectionNavigator.cs,FileDialogHistory.cs,FileDialogState.cs,FileDialogStyle.cs,FileDialogTableSource.cs,FilesSelectedEventArgs.cs,FileSystemCollectionNavigationMatcher.cs,OpenDialog.cs,OpenMode.cs,SaveDialog.cs} +|Views/FileDialogs:{AllowedType.cs,AllowedTypeAny.cs,DefaultFileOperations.cs,FileDialog.Commands.cs,FileDialog.cs,FileDialog.Navigation.cs,FileDialog.TableView.cs,FileDialogCollectionNavigator.cs,FileDialogHistory.cs,FileDialogState.cs,FileDialogStyle.cs,FileDialogTableSource.cs,FilesSelectedEventArgs.cs,FileSystemCollectionNavigationMatcher.cs,IAllowedType.cs,OpenDialog.cs,OpenMode.cs,SaveDialog.cs} |Views/GraphView:{Axis.cs,AxisIncrementToRender.cs,BarSeriesBar.cs,GraphCellToRender.cs,GraphView.cs,HorizontalAxis.cs,IAnnotation.cs,ISeries.cs,LegendAnnotation.cs,LineF.cs,MultiBarSeries.cs,PathAnnotation.cs,ScatterSeries.cs,Series.cs,TextAnnotation.cs,VerticalAxis.cs} -|Views/LinearRange:{LinearRange.cs,LinearRangeAttributes.cs,LinearRangeConfiguration.cs,LinearRangeEventArgs.cs,LinearRangeOption.cs,LinearRangeOptionEventArgs.cs,LinearRangeStyle.cs,LinearRangeType.cs} +|Views/ImageView:{ImageView.cs,ImageView.Drawing.cs,ImageView.Geometry.cs,ImageView.Input.cs,ImageView.Lifecycle.cs,ImageView.Render.cs,ImageView.Scaling.cs} +|Views/LinearRange:{LinearMultiSelector.cs,LinearMultiSelectorT.cs,LinearRange.cs,LinearRangeAttributes.cs,LinearRangeConfiguration.cs,LinearRangeDefaults.cs,LinearRangeEventArgs.cs,LinearRangeOption.cs,LinearRangeOptionEventArgs.cs,LinearRangeRenderMode.cs,LinearRangeSpan.cs,LinearRangeSpanKind.cs,LinearRangeStyle.cs,LinearRangeT.cs,LinearRangeViewBase.cs,LinearSelector.cs,LinearSelectorT.cs} |Views/ListView:{IListDataSource.cs,ListView.Commands.cs,ListView.cs,ListView.Drawing.cs,ListView.Movement.cs,ListView.Selection.cs,ListViewEventArgs.cs,ListViewT.cs,ListWrapper.cs} -|Views/Markdown:{InlineRun.cs,IntermediateBlock.cs,Markdown.cs,MarkdownCodeBlock.cs,MarkdownImageResolver.cs,MarkdownInlineParser.cs,MarkdownLinkEventArgs.cs,MarkdownTable.cs,MarkdownView.Drawing.cs,MarkdownView.Layout.cs,MarkdownView.Mouse.cs,MarkdownView.Parsing.cs,RenderedLine.cs,TableData.cs} +|Views/Markdown:{InlineRun.cs,IntermediateBlock.cs,Markdown.cs,MarkdownCodeBlock.cs,MarkdownImageResolver.cs,MarkdownInlineParser.cs,MarkdownLinkEventArgs.cs,MarkdownTable.cs,MarkdownView.Ansi.cs,MarkdownView.Drawing.cs,MarkdownView.Layout.cs,MarkdownView.Mouse.cs,MarkdownView.Parsing.cs,MarkdownView.Selection.cs,RenderedLine.cs,TableData.cs} |Views/Menu:{IMenuBarEntry.cs,Menu.cs,MenuBar.cs,MenuBarItem.cs,MenuItem.cs,PopoverMenu.cs} |Views/Runnable:{Runnable.cs,RunnableTResult.cs,RunnableWrapper.cs} |Views/ScrollBar:{ScrollBar.cs,ScrollBarVisibilityMode.cs,ScrollButton.cs,ScrollSlider.cs} |Views/Selectors:{FlagSelector.cs,FlagSelectorTEnum.cs,OptionSelector.cs,OptionSelectorTEnum.cs,SelectorBase.cs,SelectorStyles.cs} |Views/SpinnerView:{SpinnerStyle.cs,SpinnerView.cs} -|Views/TableView:{CellActivatedEventArgs.cs,CellColorGetterArgs.cs,CellToggledEventArgs.cs,CheckBoxTableSourceWrapper.cs,CheckBoxTableSourceWrapperByIndex.cs,CheckBoxTableSourceWrapperByObject.cs,ColumnStyle.cs,DataTableSource.cs,EnumerableTableSource.cs,IEnumerableTableSource.cs,ITableSource.cs,ListColumnStyle.cs,ListTableSource.cs,RowColorGetterArgs.cs,SelectedCellChangedEventArgs.cs,TableSelection.cs,TableStyle.cs,TableView.CellMapping.cs,TableView.cs,TableView.Drawing.cs,TableView.Mouse.cs,TableView.Navigation.cs,TableView.Selection.cs,TreeTableSource.cs} +|Views/TableView:{CellColorGetterArgs.cs,CheckBoxTableSourceWrapper.cs,CheckBoxTableSourceWrapperByIndex.cs,CheckBoxTableSourceWrapperByObject.cs,ColumnStyle.cs,DataTableSource.cs,EnumerableTableSource.cs,HeaderColorGetterArgs.cs,IEnumerableTableSource.cs,ITableSource.cs,ListColumnStyle.cs,ListTableSource.cs,RowColorGetterArgs.cs,TableSelection.cs,TableSelectionRegion.cs,TableStyle.cs,TableView.CellMapping.cs,TableView.Content.cs,TableView.cs,TableView.Drawing.cs,TableView.Navigation.cs,TableView.Selection.cs,TreeTableSource.cs} |Views/TextInput:{ContentsChangedEventArgs.cs,DateEditor.cs,DateTextProvider.cs,HistoryText.cs,HistoryTextItemEventArgs.cs,ITextValidateProvider.cs,NetMaskedTextProvider.cs,TextEditingLineStatus.cs,TextModel.cs,TextRegexProvider.cs,TextValidateField.cs,TimeEditor.cs,TimeTextProvider.cs} |Views/TextInput/TextField:{TextField.Commands.cs,TextField.cs,TextField.Drawing.cs,TextField.History.cs,TextField.Keyboard.cs,TextField.Mouse.cs,TextField.Selection.cs,TextField.Text.cs,TextFieldAutocomplete.cs} |Views/TextInput/TextView:{TextView.Commands.cs,TextView.cs,TextView.Drawing.cs,TextView.Files.cs,TextView.Find.cs,TextView.History.cs,TextView.Keyboard.cs,TextView.Mouse.cs,TextView.Movement.cs,TextView.Scrolling.cs,TextView.Selection.cs,TextView.Text.cs,TextView.WordWrap.cs,TextViewAutocomplete.cs,WordWrapManager.cs} -|Views/TreeView:{AspectGetterDelegate.cs,Branch.cs,DelegateTreeBuilder.cs,DrawTreeViewLineEventArgs.cs,ITreeBuilder.cs,ITreeNode.cs,ITreeView.cs,ITreeViewFilter.cs,ObjectActivatedEventArgs.cs,SelectionChangedEventArgs.cs,TreeBuilder.cs,TreeNode.cs,TreeNodeBuilder.cs,TreeSelection.cs,TreeStyle.cs,TreeView.cs,TreeView.Drawing.cs,TreeView.Mouse.cs,TreeView.Navigation.cs,TreeViewCollectionNavigatorMatcher.cs,TreeViewT.cs,TreeViewTextFilter.cs} +|Views/TreeView:{AspectGetterDelegate.cs,Branch.cs,CheckedChangedEventArgs.cs,DelegateTreeBuilder.cs,DrawTreeViewLineEventArgs.cs,ITreeBuilder.cs,ITreeNode.cs,ITreeView.cs,ITreeViewFilter.cs,ObjectActivatedEventArgs.cs,SelectionChangedEventArgs.cs,TreeBuilder.cs,TreeNode.cs,TreeNodeBuilder.cs,TreeSelection.cs,TreeStyle.cs,TreeView.cs,TreeView.Drawing.cs,TreeView.Mouse.cs,TreeView.Navigation.cs,TreeViewCollectionNavigatorMatcher.cs,TreeViewT.cs,TreeViewTextFilter.cs} |Views/Wizard:{Wizard.cs,WizardStep.cs} @@ -476,12 +479,15 @@ Implementing `IValue` requires `ValueChanging`, `ValueChanged`, and `ValueCha |IInputProcessor|Interface|Input processing ``` -### Terminal.Gui.Configuration (15 types) +### Terminal.Gui.Configuration ``` -|ConfigurationManager|Class|Settings,Themes,Apply,Reset -|SchemeManager|Class|GetScheme,Schemes dictionary -|ThemeManager|Class|Theme,Themes,SelectedTheme -|ConfigLocations|Enum|Default,Global,App,Runtime +|TuiConfigurationBuilder|Class|Build,ApplyToStaticFacades,RuntimeConfig +|ThemeManager|Class|Static facade,Theme,ThemeChanged +|SchemeManager|Class|Static facade,GetScheme,Schemes +|MecThemeManager|Class|IThemeManager implementation +|SchemeManager|Class|Static facade + ISchemeManager +|ThemeChanges|Class|ThemeChanged observer +|TuiJsonErrors|Class|Deferred JSON load errors ``` ### Terminal.Gui.Testing (8 types) @@ -552,6 +558,8 @@ Implementing `IValue` requires `ValueChanging`, `ValueChanged`, and `ValueCha + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6449f2f275..d8cbb560c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -228,7 +228,7 @@ foreach (Rune rune in text.EnumerateRunes ()) - **Make tests granular** - Each test should cover smallest area possible - Follow existing test patterns in respective test projects - **Avoid adding new tests to the `UnitTests` Project** - Make them parallelizable and add them to `UnitTests.Parallelizable` -- **Avoid static dependencies** - DO NOT use the legacy/static `Application` API or `ConfigurationManager` in tests unless the tests explicitly test related functionality. +- **Avoid static dependencies** - DO NOT use the legacy/static `Application` API or mutate process-wide `*Settings` facades in tests unless the tests explicitly test related functionality. Use `SettingsFacadeSnapshot` when you must. - **Don't use `[AutoInitShutdown]` or `[SetupFakeApplication]`** - Legacy pattern, being phased out ### Test Configuration @@ -292,7 +292,7 @@ foreach (Rune rune in text.EnumerateRunes ()) **`/Terminal.Gui/`** - Core library (496 C# files): - `App/` - Application lifecycle (`Application.cs` static class, `SessionToken`, `MainLoop`) -- `Configuration/` - `ConfigurationManager` for settings +- `Configuration/` - `TuiConfigurationBuilder`, Settings POCOs, `ThemeManager` / `SchemeManager` - `Drivers/` - Console driver implementations (`dotnet`, `Windows`, `Unix`, `ansi`) - `Drawing/` - Rendering system (attributes, colors, glyphs) - `Input/` - Keyboard and mouse input handling diff --git a/Directory.Packages.props b/Directory.Packages.props index 87cdab6cad..b329997c77 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,31 +4,31 @@ - + - + - - - - - + + + + + - + - - - - - + + + + + @@ -38,7 +38,7 @@ - + @@ -52,7 +52,7 @@ - + diff --git a/Examples/ScenarioRunner/Program.cs b/Examples/ScenarioRunner/Program.cs index b2d353c46d..aa3dcf7e7a 100644 --- a/Examples/ScenarioRunner/Program.cs +++ b/Examples/ScenarioRunner/Program.cs @@ -36,7 +36,7 @@ public static int Main (string [] args) driverOption.AcceptOnlyFromAmong (allowedDrivers!); driverOption.Aliases.Add ("-d"); - Option disableConfigManagement = new ("--disable-cm") { Description = "Indicates Configuration Management should not be enabled." }; + Option disableConfigManagement = new ("--disable-cm") { Description = "Skip applying CLI driver / 16-color overlays via TuiConfigurationBuilder.RuntimeConfig." }; disableConfigManagement.Aliases.Add ("-dcm"); Option force16Colors = new ("--force-16-colors") { Description = "Forces the driver to use 16-color mode instead of TrueColor." }; @@ -104,7 +104,6 @@ public static int Main (string [] args) if (!disableCm) { runner.SetRuntimeConfig (driver, force16 ? true : null); - ConfigurationManager.Enable (ConfigLocations.All); } Scenario? scenario = FindScenario (scenarioName); @@ -155,7 +154,6 @@ public static int Main (string [] args) { // Pass force16 only if explicitly set runner.SetRuntimeConfig (driver, force16 ? true : null); - ConfigurationManager.Enable (ConfigLocations.All); } List results; diff --git a/Examples/UICatalog/README.md b/Examples/UICatalog/README.md index c3ee18f4ea..5312f47aed 100644 --- a/Examples/UICatalog/README.md +++ b/Examples/UICatalog/README.md @@ -55,7 +55,7 @@ Usage: `UICatalog [] [options]` | `-t`, `--timeout ` | Max time in milliseconds per benchmark. Default: `2500`. | | `-f`, `--file ` | File to save benchmark results to. If omitted, results are displayed in a `TableView`. | | `-d`, `--driver ` | The `IDriver` to use (`ansi`, `dotnet`, `windows`). | -| `-dcm`, `--disable-cm` | Disables Configuration Management. Only `ConfigLocations.HardCoded` settings will be loaded. | +| `-dcm`, `--disable-cm` | Skip applying CLI driver / 16-color overlays via `TuiConfigurationBuilder.RuntimeConfig`. Library defaults still load when `Terminal.Gui.dll` loads. | | `-16`, `--force-16-colors` | Forces 16-color mode instead of TrueColor. | | `--help` | Show help (renders this README as formatted markdown). | | `--version` | Show version information. | diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 8c6b84425c..42ddef3773 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -1,321 +1,421 @@ { "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", - "Themes": [ - { - "Hot Dog Stand": { - "Glyphs.WideGlyphReplacement": "①", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "Black", - "Background": "#FFFF00" - }, - "Focus": { - "Foreground": "White", - "Background": "Black" - }, - "HotNormal": { - "Foreground": "#FF0000", - "Background": "#FFFF00" - }, - "HotFocus": { - "Foreground": "#FFFF00", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "Black", - "Background": "#FFFF00" - }, - "Focus": { - "Foreground": "White", - "Background": "Black" - }, - "HotNormal": { - "Foreground": "#FF0000", - "Background": "#FFFF00" - }, - "HotFocus": { - "Foreground": "#FFFF00", - "Background": "Black" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - }, - "Code": { "Foreground": "Black", "Background": "#FFFF00", "Style": "None" }, - "CodeComment": { "Foreground": "#008000", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#FF0000", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#0000CC", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#800080", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "Black", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#C00000", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#008000", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "Black", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#800080", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "Black", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#0000CC", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#008000", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Gray", - "Background": "#FF0000" - }, - "Focus": { - "Foreground": "#FF0000", - "Background": "White" - }, - "HotNormal": { - "Foreground": "#FF0000", - "Background": "#FFF000" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightYellow", - "Background": "Gray" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "White" - }, - "Focus": { - "Foreground": "White", - "Background": "Black" - }, - "HotNormal": { - "Foreground": "#FF00", - "Background": "White" - }, - "HotFocus": { - "Foreground": "#FF00", - "Background": "Black" - }, - "Disabled": { - "Foreground": "Gray", - "Background": "White" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "#FF00", - "Background": "#FFF000" - }, - "Focus": { - "Foreground": "Black", - "Background": "#FFF000" - }, - "HotNormal": { - "Foreground": "DarkGray", - "Background": "BrightYellow" - }, - "HotFocus": { - "Foreground": "Red", - "Background": "BrightYellow" - }, - "Disabled": { - "Foreground": "Gray", - "Background": "BrightYellow" - } - } + "Themes": { + "Hot Dog Stand": { + "Glyphs": { + "WideGlyphReplacement": "①" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "Black", + "Background": "#FFFF00" + }, + "Focus": { + "Foreground": "White", + "Background": "Black" + }, + "HotNormal": { + "Foreground": "#FF0000", + "Background": "#FFFF00" + }, + "HotFocus": { + "Foreground": "#FFFF00", + "Background": "White" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" + } + }, + "Base": { + "Normal": { + "Foreground": "Black", + "Background": "#FFFF00" + }, + "Focus": { + "Foreground": "White", + "Background": "Black" + }, + "HotNormal": { + "Foreground": "#FF0000", + "Background": "#FFFF00" + }, + "HotFocus": { + "Foreground": "#FFFF00", + "Background": "Black" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" + }, + "Code": { + "Foreground": "Black", + "Background": "#FFFF00", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#008000", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#FF0000", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#0000CC", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#800080", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "Black", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#C00000", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#008000", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "Black", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#800080", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "Black", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#0000CC", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#008000", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Gray", + "Background": "#FF0000" + }, + "Focus": { + "Foreground": "#FF0000", + "Background": "White" + }, + "HotNormal": { + "Foreground": "#FF0000", + "Background": "#FFF000" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "White" + }, + "Disabled": { + "Foreground": "BrightYellow", + "Background": "Gray" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "White" + }, + "Focus": { + "Foreground": "White", + "Background": "Black" + }, + "HotNormal": { + "Foreground": "#FF00", + "Background": "White" + }, + "HotFocus": { + "Foreground": "#FF00", + "Background": "Black" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "White" + } + }, + "Error": { + "Normal": { + "Foreground": "#FF00", + "Background": "#FFF000" + }, + "Focus": { + "Foreground": "Black", + "Background": "#FFF000" + }, + "HotNormal": { + "Foreground": "DarkGray", + "Background": "BrightYellow" + }, + "HotFocus": { + "Foreground": "Red", + "Background": "BrightYellow" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "BrightYellow" } - ] + } } }, - { - "UI Catalog": { - "Window.DefaultShadow": "Transparent", - "Button.DefaultShadow": "None", - "CheckBox.DefaultMouseHighlightStates": "In, Pressed, PressedOutside", - "MessageBox.DefaultButtonAlignment": "Start", - "StatusBar.DefaultSeparatorLineStyle": "Single", - "NerdFonts.Enable": false, - "Window.DefaultBorderStyle": "Double", - "Dialog.DefaultShadow": "Opaque", - "Dialog.DefaultButtonAlignment": "Start", - "FrameView.DefaultBorderStyle": "Double", - "Button.DefaultMouseHighlightStates": "In, Pressed", - "Menu.DefaultBorderStyle": "Heavy", - "MenuBar.DefaultBorderStyle": "Heavy", - "Schemes": [ - { - "UI Catalog Scheme": { - "Normal": { - "Foreground": "White", - "Background": "Green" - }, - "Focus": { - "Foreground": "Green", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Blue", - "Background": "Green" - }, - "HotFocus": { - "Foreground": "BrightRed", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - } - } - }, - { - "Accent": { - "Normal": { - "Foreground": "DarkGray", - "Background": "White" - }, - "Focus": { - "Foreground": "Black", - "Background": "White" - }, - "HotNormal": { - "Foreground": "BrightGreen", - "Background": "White" - }, - "HotFocus": { - "Foreground": "Cyan", - "Background": "White" - }, - "Disabled": { - "Foreground": "Gray", - "Background": "White" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "White", - "Background": "Green" - }, - "Focus": { - "Foreground": "Green", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Blue", - "Background": "Green" - }, - "HotFocus": { - "Foreground": "BrightRed", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - }, - "Code": { "Foreground": "White", "Background": "Green", "Style": "None" }, - "CodeComment": { "Foreground": "LightGray", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "Yellow", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "LightCyan", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "LightYellow", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "Cyan", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "BrightRed", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "Yellow", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "LightCyan", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "LightGray", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Gray", - "Background": "Green" - }, - "Focus": { - "Foreground": "Green", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Blue", - "Background": "Green" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "White" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "Gray" - }, - "Focus": { - "Foreground": "Green", - "Background": "DarkGray" - }, - "HotNormal": { - "Foreground": "Green", - "Background": "Gray" - }, - "HotFocus": { - "Foreground": "DarkGray", - "Background": "DarkGray" - }, - "Disabled": { - "Foreground": "Gray", - "Background": "White" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "BrightRed", - "Background": "BrightYellow" - }, - "Focus": { - "Foreground": "Black", - "Background": "BrightYellow" - }, - "HotNormal": { - "Foreground": "DarkGray", - "Background": "BrightYellow" - }, - "HotFocus": { - "Foreground": "Red", - "Background": "BrightYellow" - }, - "Disabled": { - "Foreground": "BrightGreen", - "Background": "Gray" - } - } + "UI Catalog": { + "Window": { + "DefaultShadow": "Transparent", + "DefaultBorderStyle": "Double" + }, + "Button": { + "DefaultShadow": "None", + "DefaultMouseHighlightStates": "In, Pressed" + }, + "CheckBox": { + "DefaultMouseHighlightStates": "In, Pressed, PressedOutside" + }, + "MessageBox": { + "DefaultButtonAlignment": "Start" + }, + "StatusBar": { + "DefaultSeparatorLineStyle": "Single" + }, + "NerdFonts": { + "Enable": false + }, + "Dialog": { + "DefaultShadow": "Opaque", + "DefaultButtonAlignment": "Start" + }, + "FrameView": { + "DefaultBorderStyle": "Double" + }, + "Menu": { + "DefaultBorderStyle": "Heavy" + }, + "MenuBar": { + "DefaultBorderStyle": "Heavy" + }, + "Schemes": { + "UI Catalog Scheme": { + "Normal": { + "Foreground": "White", + "Background": "Green" + }, + "Focus": { + "Foreground": "Green", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Blue", + "Background": "Green" + }, + "HotFocus": { + "Foreground": "BrightRed", + "Background": "White" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" + } + }, + "Accent": { + "Normal": { + "Foreground": "DarkGray", + "Background": "White" + }, + "Focus": { + "Foreground": "Black", + "Background": "White" + }, + "HotNormal": { + "Foreground": "BrightGreen", + "Background": "White" + }, + "HotFocus": { + "Foreground": "Cyan", + "Background": "White" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "White" + } + }, + "Base": { + "Normal": { + "Foreground": "White", + "Background": "Green" + }, + "Focus": { + "Foreground": "Green", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Blue", + "Background": "Green" + }, + "HotFocus": { + "Foreground": "BrightRed", + "Background": "White" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" + }, + "Code": { + "Foreground": "White", + "Background": "Green", + "Style": "None" + }, + "CodeComment": { + "Foreground": "LightGray", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "Yellow", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "LightCyan", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "LightYellow", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "Cyan", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "BrightRed", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "Yellow", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "LightCyan", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "LightGray", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Gray", + "Background": "Green" + }, + "Focus": { + "Foreground": "Green", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Blue", + "Background": "Green" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "White" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "Gray" + }, + "Focus": { + "Foreground": "Green", + "Background": "DarkGray" + }, + "HotNormal": { + "Foreground": "Green", + "Background": "Gray" + }, + "HotFocus": { + "Foreground": "DarkGray", + "Background": "DarkGray" + }, + "Disabled": { + "Foreground": "Gray", + "Background": "White" + } + }, + "Error": { + "Normal": { + "Foreground": "BrightRed", + "Background": "BrightYellow" + }, + "Focus": { + "Foreground": "Black", + "Background": "BrightYellow" + }, + "HotNormal": { + "Foreground": "DarkGray", + "Background": "BrightYellow" + }, + "HotFocus": { + "Foreground": "Red", + "Background": "BrightYellow" + }, + "Disabled": { + "Foreground": "BrightGreen", + "Background": "Gray" } - ] + } } } - ] -} + } +} \ No newline at end of file diff --git a/Examples/UICatalog/Runner.cs b/Examples/UICatalog/Runner.cs index cf7d20ef39..1e11306666 100644 --- a/Examples/UICatalog/Runner.cs +++ b/Examples/UICatalog/Runner.cs @@ -11,7 +11,7 @@ namespace UICatalog; public class Runner { /// - /// Sets with "Application.ForceDriver" and ["Driver.Force16Colors" based on the params. + /// Sets with Application.ForceDriver and Driver.Force16Colors. /// /// The driver to use, or null to use the default. /// @@ -24,19 +24,21 @@ public void SetRuntimeConfig (string? forceDriver = null, bool? force16Colors = if (!string.IsNullOrEmpty (forceDriver)) { - runtimeConfig ["Application.ForceDriver"] = forceDriver; + runtimeConfig ["Application"] = new Dictionary { ["ForceDriver"] = forceDriver }; } if (force16Colors.HasValue) { - runtimeConfig ["Driver.Force16Colors"] = force16Colors.Value; + runtimeConfig ["Driver"] = new Dictionary { ["Force16Colors"] = force16Colors.Value }; } if (runtimeConfig.Count == 0) { return; } - ConfigurationManager.RuntimeConfig = JsonSerializer.Serialize (runtimeConfig); + + TuiConfigurationBuilder.Shared.RuntimeConfig = JsonSerializer.Serialize (runtimeConfig); + TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); } /// @@ -367,16 +369,46 @@ public void StopConfigWatcher () _homeDirWatcher.EnableRaisingEvents = false; _homeDirWatcher.Changed -= ConfigFileChanged; + lock (_configReloadLock) + { + _configReloadTimer?.Dispose (); + _configReloadTimer = null; + } + _configWatcherStarted = false; } - private static void ThemeManagerOnThemeChanged (object? sender, EventArgs e) => ConfigurationManager.Apply (); + private static void ThemeManagerOnThemeChanged (object? sender, EventArgs e) => + TuiConfigurationBuilder.Shared.ApplyActiveThemeOverlays (); + + private static readonly Lock _configReloadLock = new (); + private static Timer? _configReloadTimer; private static void ConfigFileChanged (object sender, FileSystemEventArgs e) { - Logging.Debug ($"{e.FullPath} {e.ChangeType} - Loading and Applying"); - ConfigurationManager.Load (ConfigLocations.All); - ConfigurationManager.Apply (); + Logging.Debug ($"{e.FullPath} {e.ChangeType} - scheduling reload"); + + // Editors fire several Changed events per save (and may still hold the file mid-write); + // debounce so one settled reload runs instead of a rebuild per event. + lock (_configReloadLock) + { + _configReloadTimer ??= new (ReloadConfig); + _configReloadTimer.Change (250, System.Threading.Timeout.Infinite); + } + } + + private static void ReloadConfig (object? state) + { + // Never let a bad config value kill the process from the watcher thread — collect and log instead. + try + { + TuiConfigurationBuilder.Shared.Reload (); + TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); + } + catch (Exception ex) + { + Logging.Warning ($"Configuration reload failed: {ex.Message}"); + } } #endregion Interactive Mode diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 6cdb64492a..412f52b097 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -14,7 +14,6 @@ public class Adornments : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index c7e5779a01..c70125c2da 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -27,7 +27,6 @@ public class AllViewsTester : Scenario public override void Main () { // Don't create a sub-win (Scenario.Win); just use Application.TopRunnable - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index 25d44cf122..e181edcff8 100644 --- a/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs +++ b/Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Diagnostics; using System.Reflection; using System.Text; @@ -17,7 +17,6 @@ public class AnimationScenario : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs index 2875cce76e..60ca475744 100644 --- a/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs +++ b/Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs @@ -26,7 +26,6 @@ public sealed class AnsiEscapeSequenceRequests : Scenario public override void Main () { // Init - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/Arrangement.cs b/Examples/UICatalog/Scenarios/Arrangement.cs index a24a501169..1c611198da 100644 --- a/Examples/UICatalog/Scenarios/Arrangement.cs +++ b/Examples/UICatalog/Scenarios/Arrangement.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Timer = System.Timers.Timer; namespace UICatalog.Scenarios; @@ -13,7 +13,6 @@ public class Arrangement : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index ea490d42c8..0a2ad14c43 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -8,7 +8,6 @@ public class Bars : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/BracketedPasteDemo.cs b/Examples/UICatalog/Scenarios/BracketedPasteDemo.cs index 36f0274247..401903cc33 100644 --- a/Examples/UICatalog/Scenarios/BracketedPasteDemo.cs +++ b/Examples/UICatalog/Scenarios/BracketedPasteDemo.cs @@ -8,7 +8,6 @@ public sealed class BracketedPasteDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index 1496d01e70..0c62a433d7 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -10,7 +10,6 @@ public class Buttons : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 82a8422215..6963e2791b 100644 --- a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -54,7 +54,6 @@ public override List GetDemoKeyStrokes (IApplication? app) // Don't create a Window, just return the top-level view public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ChineseUI.cs b/Examples/UICatalog/Scenarios/ChineseUI.cs index e36c48ae53..a89af4d112 100644 --- a/Examples/UICatalog/Scenarios/ChineseUI.cs +++ b/Examples/UICatalog/Scenarios/ChineseUI.cs @@ -1,4 +1,4 @@ - + namespace UICatalog.Scenarios; [ScenarioMetadata ("ChineseUI", "Chinese UI")] @@ -8,7 +8,6 @@ public class ChineseUI : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ClassExplorer.cs b/Examples/UICatalog/Scenarios/ClassExplorer.cs index 86a98f4104..5ebf6ba988 100644 --- a/Examples/UICatalog/Scenarios/ClassExplorer.cs +++ b/Examples/UICatalog/Scenarios/ClassExplorer.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Reflection; using System.Text; @@ -19,7 +19,6 @@ public class ClassExplorer : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Clipping.cs b/Examples/UICatalog/Scenarios/Clipping.cs index d6804e3200..649be6498f 100644 --- a/Examples/UICatalog/Scenarios/Clipping.cs +++ b/Examples/UICatalog/Scenarios/Clipping.cs @@ -1,4 +1,4 @@ -using Timer = System.Timers.Timer; +using Timer = System.Timers.Timer; namespace UICatalog.Scenarios; @@ -14,7 +14,6 @@ public class Clipping : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/CodeViewDemo.cs b/Examples/UICatalog/Scenarios/CodeViewDemo.cs index dc33af5f8c..3f439f7ac7 100644 --- a/Examples/UICatalog/Scenarios/CodeViewDemo.cs +++ b/Examples/UICatalog/Scenarios/CodeViewDemo.cs @@ -82,7 +82,6 @@ CodeFunctionName CodeAttribute /// public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); @@ -150,7 +149,6 @@ public override void Main () string theme = themes [(int)args.NewValue] [1..]; ThemeManager.Theme = theme; - ConfigurationManager.Apply (); }; window.Add (languageSelector, themeSelector, code); diff --git a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs index 2e075cba21..5b7d58f915 100644 --- a/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/Examples/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.ObjectModel; @@ -76,7 +76,6 @@ public class CollectionNavigatorTester : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ColorPicker.cs b/Examples/UICatalog/Scenarios/ColorPicker.cs index f0a5ec40f1..a0a9edea0b 100644 --- a/Examples/UICatalog/Scenarios/ColorPicker.cs +++ b/Examples/UICatalog/Scenarios/ColorPicker.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; [ScenarioMetadata ("ColorPicker", "Color Picker and TrueColor demonstration.")] @@ -30,7 +30,6 @@ public class ColorPickers : Scenario /// Set up the scenario. public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 6678cbdaee..a3b5deaf55 100644 --- a/Examples/UICatalog/Scenarios/CombiningMarks.cs +++ b/Examples/UICatalog/Scenarios/CombiningMarks.cs @@ -8,7 +8,6 @@ public class CombiningMarks : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ComputedLayout.cs b/Examples/UICatalog/Scenarios/ComputedLayout.cs index 64aeb6ebd9..80f3597bd1 100644 --- a/Examples/UICatalog/Scenarios/ComputedLayout.cs +++ b/Examples/UICatalog/Scenarios/ComputedLayout.cs @@ -11,7 +11,6 @@ public class ComputedLayout : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index e1b35674b3..fcdfd9095e 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -20,7 +20,6 @@ public class ConfigurationEditor : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; @@ -48,7 +47,7 @@ public override void Main () win.Add (_tabs, statusBar); - ConfigurationManager.Applied += ConfigurationManagerOnApplied; + ThemeChanges.ThemeChanged += (_, _) => _app?.TopRunnableView?.SetNeedsDraw (); Open (); _tabs.Disposing += (_, _) => @@ -58,8 +57,6 @@ public override void Main () app.Run (win); return; - - void ConfigurationManagerOnApplied (object? sender, ConfigurationManagerEventArgs e) => _app?.TopRunnableView?.SetNeedsDraw (); } public void Save () @@ -72,37 +69,32 @@ public void Save () private void Open () { - foreach (KeyValuePair config in ConfigurationManager.SourcesManager!.Sources) - { - var homeDir = $"{Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)}"; - FileInfo fileInfo = new (config.Value.Replace ("~", homeDir)); + string homeDir = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); + string userPath = Path.Combine (homeDir, TuiConfigurationExtensions.TUI_CONFIG_FOLDER, TuiConfigurationExtensions.CONFIG_FILENAME); + (string Title, FileInfo File) [] sources = + [ + ("Library", new ("[Terminal.Gui]config.json")), + ("User", new (userPath)), + ("Runtime", new ("RuntimeConfig")) + ]; + + foreach ((string title, FileInfo fileInfo) in sources) + { ConfigEditorView editor = new () { - Title = config.Value.StartsWith ("resource://", StringComparison.Ordinal) ? fileInfo.Name : config.Value, + Title = title, Width = Dim.Fill (), Height = Dim.Fill (), FileInfo = fileInfo }; - if (config.Value == "HardCoded") - { - editor.Title = "HardCoded"; - } - - View tab = new () { Title = config.Key.ToString () }; + View tab = new () { Title = title }; tab.Add (editor); - _tabs?.Add (tab); - editor.Read (); - - editor.Disposing += (_, _) => - { - editor.Document!.Changed -= OnEditorDocumentChanged; - }; + editor.Disposing += (_, _) => { editor.Document!.Changed -= OnEditorDocumentChanged; }; editor.Document!.Changed += OnEditorDocumentChanged; - _lenShortcut?.Title = $"{editor.Title}"; } @@ -191,7 +183,7 @@ internal void Read () if (FileInfo!.FullName.Contains ("[Terminal.Gui]")) { // Library resources - assembly = typeof (ConfigurationManager).Assembly; + assembly = typeof (TuiConfigurationBuilder).Assembly; } else if (FileInfo.FullName.Contains ("[UICatalog]")) { @@ -217,19 +209,13 @@ internal void Read () return; } - if (FileInfo!.FullName.Contains ("HardCoded")) - { - Text = ConfigurationManager.GetHardCodedConfig (); - ReadOnly = true; - } - else if (FileInfo!.FullName.Contains ("RuntimeConfig")) + if (FileInfo!.FullName.Contains ("RuntimeConfig")) { - Text = ConfigurationManager.RuntimeConfig!; + Text = TuiConfigurationBuilder.Shared.RuntimeConfig ?? "{}"; } else if (!FileInfo.Exists) { - // Create empty config file - Text = ConfigurationManager.GetEmptyConfig (); + Text = "{}"; } else { @@ -244,7 +230,8 @@ internal void Save () { if (FileInfo!.FullName.Contains ("RuntimeConfig")) { - ConfigurationManager.RuntimeConfig = Text; + TuiConfigurationBuilder.Shared.RuntimeConfig = Text; + TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); Document!.UndoStack.ClearAll (); Document!.UndoStack.MarkAsOriginalFile (); diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index b97f772e84..e32863b9a1 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -28,7 +28,6 @@ public class CsvEditor : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/DatePickers.cs b/Examples/UICatalog/Scenarios/DatePickers.cs index 4025050a0e..d98651775b 100644 --- a/Examples/UICatalog/Scenarios/DatePickers.cs +++ b/Examples/UICatalog/Scenarios/DatePickers.cs @@ -1,4 +1,4 @@ - + #nullable enable namespace UICatalog.Scenarios; @@ -9,7 +9,6 @@ public class DatePickers : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Dialogs.cs b/Examples/UICatalog/Scenarios/Dialogs.cs index 0d2f57afd9..77dc5f6bf2 100644 --- a/Examples/UICatalog/Scenarios/Dialogs.cs +++ b/Examples/UICatalog/Scenarios/Dialogs.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -12,7 +12,6 @@ public class Dialogs : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/DimAutoDemo.cs b/Examples/UICatalog/Scenarios/DimAutoDemo.cs index a314a80afc..1ccf17b95b 100644 --- a/Examples/UICatalog/Scenarios/DimAutoDemo.cs +++ b/Examples/UICatalog/Scenarios/DimAutoDemo.cs @@ -9,7 +9,6 @@ public class DimAutoDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/DropDownListExample.cs b/Examples/UICatalog/Scenarios/DropDownListExample.cs index 594288eeee..2d4782d91f 100644 --- a/Examples/UICatalog/Scenarios/DropDownListExample.cs +++ b/Examples/UICatalog/Scenarios/DropDownListExample.cs @@ -23,7 +23,6 @@ private enum DayOfWeekShort public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); _app = Application.Create (); _app.Init (); diff --git a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs index 11b17b7045..68ba616cc8 100644 --- a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs @@ -16,7 +16,6 @@ public class DynamicStatusBar : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); _app = app; _app.Init (); diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 03674bfa20..43e6e938ab 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -26,7 +26,6 @@ public class FileDialogExamples : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); var y = 0; diff --git a/Examples/UICatalog/Scenarios/Generic.cs b/Examples/UICatalog/Scenarios/Generic.cs index e3a337b05f..dd81423e93 100644 --- a/Examples/UICatalog/Scenarios/Generic.cs +++ b/Examples/UICatalog/Scenarios/Generic.cs @@ -8,7 +8,6 @@ public sealed class Generic : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 4a394e8c4d..e15ea6c244 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -21,7 +21,6 @@ public class GenericListView : Scenario /// public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/GraphViewExample.cs b/Examples/UICatalog/Scenarios/GraphViewExample.cs index 1df4442a2e..6e19a86598 100644 --- a/Examples/UICatalog/Scenarios/GraphViewExample.cs +++ b/Examples/UICatalog/Scenarios/GraphViewExample.cs @@ -25,7 +25,6 @@ public class GraphViewExample : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/HexEditor.cs b/Examples/UICatalog/Scenarios/HexEditor.cs index d19bdc05e8..47939c60b0 100644 --- a/Examples/UICatalog/Scenarios/HexEditor.cs +++ b/Examples/UICatalog/Scenarios/HexEditor.cs @@ -25,7 +25,6 @@ public class HexEditor : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/HotKeys.cs b/Examples/UICatalog/Scenarios/HotKeys.cs index fa4bbc2384..4438608697 100644 --- a/Examples/UICatalog/Scenarios/HotKeys.cs +++ b/Examples/UICatalog/Scenarios/HotKeys.cs @@ -9,7 +9,6 @@ public class HotKeys : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Images/Images.cs b/Examples/UICatalog/Scenarios/Images/Images.cs index 18428d1450..c4969ce495 100644 --- a/Examples/UICatalog/Scenarios/Images/Images.cs +++ b/Examples/UICatalog/Scenarios/Images/Images.cs @@ -46,7 +46,6 @@ public class Images : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); IDriver driver = app.Driver ?? throw new InvalidOperationException ("The application driver was not initialized."); diff --git a/Examples/UICatalog/Scenarios/InteractiveTree.cs b/Examples/UICatalog/Scenarios/InteractiveTree.cs index f7d47ee867..5320c8c696 100644 --- a/Examples/UICatalog/Scenarios/InteractiveTree.cs +++ b/Examples/UICatalog/Scenarios/InteractiveTree.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; @@ -13,7 +13,6 @@ public class InteractiveTree : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/KeyBindings.cs b/Examples/UICatalog/Scenarios/KeyBindings.cs index ab7bd86e79..c4748bf066 100644 --- a/Examples/UICatalog/Scenarios/KeyBindings.cs +++ b/Examples/UICatalog/Scenarios/KeyBindings.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Reflection; using Terminal.Gui; @@ -15,7 +15,6 @@ public sealed class KeyBindings : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Keys.cs b/Examples/UICatalog/Scenarios/Keys.cs index 6412c90d69..3213f2bc27 100644 --- a/Examples/UICatalog/Scenarios/Keys.cs +++ b/Examples/UICatalog/Scenarios/Keys.cs @@ -8,7 +8,6 @@ public class Keys : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); ObservableCollection keyDownList = []; diff --git a/Examples/UICatalog/Scenarios/LineDrawing.cs b/Examples/UICatalog/Scenarios/LineDrawing.cs index 54d0986d43..f5760ca5ed 100644 --- a/Examples/UICatalog/Scenarios/LineDrawing.cs +++ b/Examples/UICatalog/Scenarios/LineDrawing.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; namespace UICatalog.Scenarios; @@ -92,7 +92,6 @@ public class LineDrawing : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/LineExample.cs b/Examples/UICatalog/Scenarios/LineExample.cs index 634c4d909f..a4bdf7e196 100644 --- a/Examples/UICatalog/Scenarios/LineExample.cs +++ b/Examples/UICatalog/Scenarios/LineExample.cs @@ -8,7 +8,6 @@ public class LineExample : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/LinearRanges.cs b/Examples/UICatalog/Scenarios/LinearRanges.cs index eef953c5bc..bbb874ab30 100644 --- a/Examples/UICatalog/Scenarios/LinearRanges.cs +++ b/Examples/UICatalog/Scenarios/LinearRanges.cs @@ -8,7 +8,6 @@ public class LinearRanges : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Links.cs b/Examples/UICatalog/Scenarios/Links.cs index 2822210f01..1904522937 100644 --- a/Examples/UICatalog/Scenarios/Links.cs +++ b/Examples/UICatalog/Scenarios/Links.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; @@ -9,7 +9,6 @@ public class Links : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ListColumns.cs b/Examples/UICatalog/Scenarios/ListColumns.cs index 220dc1f959..1b3206dfef 100644 --- a/Examples/UICatalog/Scenarios/ListColumns.cs +++ b/Examples/UICatalog/Scenarios/ListColumns.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections; using System.Data; @@ -48,7 +48,6 @@ public static IList BuildSimpleList (int items) public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs index e7b83bac5b..9df4c79618 100644 --- a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -26,7 +26,6 @@ public class ListViewWithSelection : Scenario /// public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Localization.cs b/Examples/UICatalog/Scenarios/Localization.cs index dec51024e1..bb58e420db 100644 --- a/Examples/UICatalog/Scenarios/Localization.cs +++ b/Examples/UICatalog/Scenarios/Localization.cs @@ -54,7 +54,6 @@ public void SetCulture (CultureInfo culture) public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); _app = Application.Create (); _app.Init (); diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index 0a04fea6cc..3a83c8a28d 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/Examples/UICatalog/Scenarios/Mazing.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -19,7 +19,6 @@ public class Mazing : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/MenuBars.cs b/Examples/UICatalog/Scenarios/MenuBars.cs index 536f875e4c..8ea1f697cb 100644 --- a/Examples/UICatalog/Scenarios/MenuBars.cs +++ b/Examples/UICatalog/Scenarios/MenuBars.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Diagnostics; @@ -14,7 +14,6 @@ public class MenuBars : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/MenuBarsWithoutPopovers.cs b/Examples/UICatalog/Scenarios/MenuBarsWithoutPopovers.cs index f682ace3ad..3310b74ed0 100644 --- a/Examples/UICatalog/Scenarios/MenuBarsWithoutPopovers.cs +++ b/Examples/UICatalog/Scenarios/MenuBarsWithoutPopovers.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; @@ -11,7 +11,6 @@ public class MenuBarsWithoutPopovers : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index 89218235d9..e5418aed53 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/Examples/UICatalog/Scenarios/Menus.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; @@ -11,7 +11,6 @@ public class Menus : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/MessageBoxes.cs b/Examples/UICatalog/Scenarios/MessageBoxes.cs index 0d90e6adc3..75800ddac2 100644 --- a/Examples/UICatalog/Scenarios/MessageBoxes.cs +++ b/Examples/UICatalog/Scenarios/MessageBoxes.cs @@ -54,7 +54,6 @@ public override List GetDemoKeyStrokes (IApplication app) public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/MouseTester.cs b/Examples/UICatalog/Scenarios/MouseTester.cs index f1ac6094f4..e29bd5d911 100644 --- a/Examples/UICatalog/Scenarios/MouseTester.cs +++ b/Examples/UICatalog/Scenarios/MouseTester.cs @@ -9,7 +9,6 @@ public class MouseTester : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/MultiColouredTable.cs b/Examples/UICatalog/Scenarios/MultiColouredTable.cs index b9661aeb68..445e03ce4a 100644 --- a/Examples/UICatalog/Scenarios/MultiColouredTable.cs +++ b/Examples/UICatalog/Scenarios/MultiColouredTable.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Data; using System.Text; @@ -17,7 +17,6 @@ public class MultiColouredTable : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index f4d8aec38c..d6c6571d8f 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -1,4 +1,4 @@ - + // ReSharper disable AccessToModifiedClosure namespace UICatalog.Scenarios; @@ -13,7 +13,6 @@ public class Navigation : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index fa9130f9fe..9cf58a0760 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -24,7 +24,6 @@ public class Notepad : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); _app = app; diff --git a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs index 4d44b7b1db..33d0f9c3f0 100644 --- a/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs +++ b/Examples/UICatalog/Scenarios/NumericUpDownDemo.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; [ScenarioMetadata ("NumericUpDown", "Demonstrates the NumericUpDown View")] @@ -7,7 +7,6 @@ public class NumericUpDownDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Osc94ProgressIndicator.cs b/Examples/UICatalog/Scenarios/Osc94ProgressIndicator.cs index 0cb01e6d3c..53c0f189a1 100644 --- a/Examples/UICatalog/Scenarios/Osc94ProgressIndicator.cs +++ b/Examples/UICatalog/Scenarios/Osc94ProgressIndicator.cs @@ -14,7 +14,6 @@ public sealed class Osc94ProgressIndicator : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/PopoverMenus.cs b/Examples/UICatalog/Scenarios/PopoverMenus.cs index 69bbd5d17c..3978d9b712 100644 --- a/Examples/UICatalog/Scenarios/PopoverMenus.cs +++ b/Examples/UICatalog/Scenarios/PopoverMenus.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Globalization; // ReSharper disable AccessToDisposedClosure @@ -21,7 +21,6 @@ public class PopoverMenus : Scenario public override void Main () { // Init - ConfigurationManager.Enable (ConfigLocations.All); // Prepping for modern app model using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/Popovers.cs b/Examples/UICatalog/Scenarios/Popovers.cs index 5ba8db1170..4ecb20672c 100644 --- a/Examples/UICatalog/Scenarios/Popovers.cs +++ b/Examples/UICatalog/Scenarios/Popovers.cs @@ -22,7 +22,6 @@ public class Popovers : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); _app = Application.Create (); _app.Init (); diff --git a/Examples/UICatalog/Scenarios/PosAlignDemo.cs b/Examples/UICatalog/Scenarios/PosAlignDemo.cs index df65e6330d..788277a6f4 100644 --- a/Examples/UICatalog/Scenarios/PosAlignDemo.cs +++ b/Examples/UICatalog/Scenarios/PosAlignDemo.cs @@ -11,7 +11,6 @@ public sealed class PosAlignDemo : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/ProcessTable.cs b/Examples/UICatalog/Scenarios/ProcessTable.cs index 87837f056d..01a66d4c74 100644 --- a/Examples/UICatalog/Scenarios/ProcessTable.cs +++ b/Examples/UICatalog/Scenarios/ProcessTable.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -12,7 +12,6 @@ public class ProcessTable : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Progress.cs b/Examples/UICatalog/Scenarios/Progress.cs index 0b84b360ea..cf27b714a1 100644 --- a/Examples/UICatalog/Scenarios/Progress.cs +++ b/Examples/UICatalog/Scenarios/Progress.cs @@ -17,7 +17,6 @@ public class Progress : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs index d6d916a041..be5f563f5f 100644 --- a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; // ReSharper disable AccessToDisposedClosure namespace UICatalog.Scenarios; @@ -20,7 +20,6 @@ public class ProgressBarStyles : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/RegionScenario.cs b/Examples/UICatalog/Scenarios/RegionScenario.cs index d5b5c16f0a..7565e08ce2 100644 --- a/Examples/UICatalog/Scenarios/RegionScenario.cs +++ b/Examples/UICatalog/Scenarios/RegionScenario.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -23,7 +23,6 @@ public class RegionScenario : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/RunTExample.cs b/Examples/UICatalog/Scenarios/RunTExample.cs index 25d3c020d2..f61b94a190 100644 --- a/Examples/UICatalog/Scenarios/RunTExample.cs +++ b/Examples/UICatalog/Scenarios/RunTExample.cs @@ -1,4 +1,4 @@ - + namespace UICatalog.Scenarios; [ScenarioMetadata ("Run Example", "Illustrates using Application.Run to run a custom class")] @@ -7,7 +7,6 @@ public class RunTExample : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs index 08d88f959f..fdcae1806c 100644 --- a/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs +++ b/Examples/UICatalog/Scenarios/RuneWidthGreaterThanOne.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; @@ -21,7 +21,6 @@ public class RuneWidthGreaterThanOne : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ScrollBarDemo.cs b/Examples/UICatalog/Scenarios/ScrollBarDemo.cs index 3ee7d1683b..e9d8e32a91 100644 --- a/Examples/UICatalog/Scenarios/ScrollBarDemo.cs +++ b/Examples/UICatalog/Scenarios/ScrollBarDemo.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.ObjectModel; namespace UICatalog.Scenarios; @@ -9,7 +9,6 @@ public class ScrollBarDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Scrolling.cs b/Examples/UICatalog/Scenarios/Scrolling.cs index 85fb7f8e53..ac9242b87f 100644 --- a/Examples/UICatalog/Scenarios/Scrolling.cs +++ b/Examples/UICatalog/Scenarios/Scrolling.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable namespace UICatalog.Scenarios; @@ -12,7 +12,6 @@ public class Scrolling : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Selectors.cs b/Examples/UICatalog/Scenarios/Selectors.cs index c90dc4e8ff..d6ba0da9e1 100644 --- a/Examples/UICatalog/Scenarios/Selectors.cs +++ b/Examples/UICatalog/Scenarios/Selectors.cs @@ -1,4 +1,4 @@ -// ReSharper disable AccessToDisposedClosure +// ReSharper disable AccessToDisposedClosure #nullable enable namespace UICatalog.Scenarios; @@ -9,7 +9,6 @@ public sealed class Selectors : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/ShadowStyleDemo.cs b/Examples/UICatalog/Scenarios/ShadowStyleDemo.cs index 8392bb66af..900fefd240 100644 --- a/Examples/UICatalog/Scenarios/ShadowStyleDemo.cs +++ b/Examples/UICatalog/Scenarios/ShadowStyleDemo.cs @@ -1,4 +1,4 @@ -namespace UICatalog.Scenarios; +namespace UICatalog.Scenarios; [ScenarioMetadata ("ShadowStyle Demo", "Demonstrates ShadowStyle Effects.")] [ScenarioCategory ("Layout")] @@ -7,7 +7,6 @@ public class ShadowStyleDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index f0320a678e..861ad2ac75 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -13,7 +13,6 @@ public class Shortcuts : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); _app = Application.Create (); _app.Init (); _window = new Window (); @@ -342,7 +341,6 @@ private void HandleOnIsRunningChanged (object? sender, EventArgs e) framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Accent); _window.Add (framedShortcut); - if (ConfigurationManager.IsEnabled) { OptionSelector themesSelector = new (); themesSelector.Id = "themesSelector"; diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index 3468f4c964..684a7cdcf1 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.ObjectModel; using System.ComponentModel; @@ -13,7 +13,6 @@ public class SingleBackgroundWorker : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Snake.cs b/Examples/UICatalog/Scenarios/Snake.cs index cef85e6faa..e53803c0d2 100644 --- a/Examples/UICatalog/Scenarios/Snake.cs +++ b/Examples/UICatalog/Scenarios/Snake.cs @@ -13,7 +13,6 @@ public class Snake : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/SpectreViewScenario.cs b/Examples/UICatalog/Scenarios/SpectreViewScenario.cs index 2cc6526d1d..0bcc053433 100644 --- a/Examples/UICatalog/Scenarios/SpectreViewScenario.cs +++ b/Examples/UICatalog/Scenarios/SpectreViewScenario.cs @@ -14,7 +14,6 @@ public sealed class SpectreViewScenario : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/SpinnerStyles.cs b/Examples/UICatalog/Scenarios/SpinnerStyles.cs index 9e0ed5537c..dd4e53ffb8 100644 --- a/Examples/UICatalog/Scenarios/SpinnerStyles.cs +++ b/Examples/UICatalog/Scenarios/SpinnerStyles.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -12,7 +12,6 @@ public class SpinnerViewStyles : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 2f1647c690..fb813649f7 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Data; using System.Globalization; using System.Text; @@ -204,7 +204,6 @@ public static DataTable BuildSimpleDataTable (int cols, int rows) public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/TableViewTest.cs b/Examples/UICatalog/Scenarios/TableViewTest.cs index ca78731b9e..3b91a7e7fd 100644 --- a/Examples/UICatalog/Scenarios/TableViewTest.cs +++ b/Examples/UICatalog/Scenarios/TableViewTest.cs @@ -1,4 +1,4 @@ -namespace UICatalog.Scenarios; +namespace UICatalog.Scenarios; [ScenarioMetadata ("TableViewTest", "Demonstrates and tests TableView.")] [ScenarioCategory ("TableView")] @@ -11,7 +11,6 @@ public class TableViewTest : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TabsExample.cs b/Examples/UICatalog/Scenarios/TabsExample.cs index c96c5d7818..04fe1fea29 100644 --- a/Examples/UICatalog/Scenarios/TabsExample.cs +++ b/Examples/UICatalog/Scenarios/TabsExample.cs @@ -11,7 +11,6 @@ public sealed class TabsExample : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs b/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs index 9b41f04a26..099e5927e1 100644 --- a/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs +++ b/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs @@ -9,7 +9,6 @@ public class TextAlignmentAndDirection : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs index 767ad0a6d9..fecf2dc2dd 100644 --- a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs +++ b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs @@ -12,7 +12,6 @@ public class TextEffectsScenario : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs index 01fff4c28a..6cdc7a2647 100644 --- a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs +++ b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; namespace UICatalog.Scenarios; @@ -9,7 +9,6 @@ public class TextFormatterDemo : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TextInputControls.cs b/Examples/UICatalog/Scenarios/TextInputControls.cs index b6fd04d708..66a4910e27 100644 --- a/Examples/UICatalog/Scenarios/TextInputControls.cs +++ b/Examples/UICatalog/Scenarios/TextInputControls.cs @@ -20,7 +20,6 @@ public class TextInputControls : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); @@ -381,7 +380,7 @@ public override void Main () win.Accepting += WinOnAccept; - ConfigurationManager.Applied += ConfigurationManagerOnApplied; + ThemeChanges.ThemeChanged += (_, _) => win.SetNeedsDraw (); app.Run (win); @@ -407,7 +406,6 @@ void WinOnAccept (object? sender, CommandEventArgs e) }); } - void ConfigurationManagerOnApplied (object? sender, ConfigurationManagerEventArgs e) => win.SetNeedsDraw (); } private void TimeChanged (object? sender, ValueChangedEventArgs e) diff --git a/Examples/UICatalog/Scenarios/TextStyles.cs b/Examples/UICatalog/Scenarios/TextStyles.cs index d30de29651..3e113bd425 100644 --- a/Examples/UICatalog/Scenarios/TextStyles.cs +++ b/Examples/UICatalog/Scenarios/TextStyles.cs @@ -11,7 +11,6 @@ public sealed class TextStyles : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/ThemeFallback.cs b/Examples/UICatalog/Scenarios/ThemeFallback.cs index b93a5706c7..d3ada93800 100644 --- a/Examples/UICatalog/Scenarios/ThemeFallback.cs +++ b/Examples/UICatalog/Scenarios/ThemeFallback.cs @@ -31,7 +31,6 @@ public sealed class ThemeFallback : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); @@ -70,7 +69,6 @@ public override void Main () // Strip the leading underscore added for keyboard shortcut. ThemeManager.Theme = rawLabel [1..]; - ConfigurationManager.Apply (); // Re-add the custom scheme to the newly-active theme so the // "Default" theme always demonstrates the found case. diff --git a/Examples/UICatalog/Scenarios/Themes.cs b/Examples/UICatalog/Scenarios/Themes.cs index 8da5582bda..5e01d6f222 100644 --- a/Examples/UICatalog/Scenarios/Themes.cs +++ b/Examples/UICatalog/Scenarios/Themes.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.ObjectModel; @@ -17,7 +17,6 @@ public sealed class Themes : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); @@ -86,7 +85,6 @@ public sealed class Demo // strip off the leading underscore ThemeManager.Theme = newTheme [1..]; - ConfigurationManager.Apply (); }; ThemeViewer themeViewer = new () { X = Pos.Right (themeOptionSelector) }; diff --git a/Examples/UICatalog/Scenarios/Threading.cs b/Examples/UICatalog/Scenarios/Threading.cs index 4bdc954b28..455dffa8d4 100644 --- a/Examples/UICatalog/Scenarios/Threading.cs +++ b/Examples/UICatalog/Scenarios/Threading.cs @@ -32,7 +32,6 @@ public class Threading : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TimeAndDate.cs b/Examples/UICatalog/Scenarios/TimeAndDate.cs index 514dd542ee..7f18120db4 100644 --- a/Examples/UICatalog/Scenarios/TimeAndDate.cs +++ b/Examples/UICatalog/Scenarios/TimeAndDate.cs @@ -14,7 +14,6 @@ public class TimeAndDate : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Transparent.cs b/Examples/UICatalog/Scenarios/Transparent.cs index 2c6ca4df08..c6f06c1fb8 100644 --- a/Examples/UICatalog/Scenarios/Transparent.cs +++ b/Examples/UICatalog/Scenarios/Transparent.cs @@ -9,7 +9,6 @@ public sealed class Transparent : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/TreeUseCases.cs b/Examples/UICatalog/Scenarios/TreeUseCases.cs index 1823a03548..b6e829fdbb 100644 --- a/Examples/UICatalog/Scenarios/TreeUseCases.cs +++ b/Examples/UICatalog/Scenarios/TreeUseCases.cs @@ -16,7 +16,6 @@ public partial class TreeUseCases : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs index 61e77b12b7..d7cb64a4d5 100644 --- a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.IO.Abstractions; using System.Text; @@ -33,7 +33,6 @@ public class TreeViewFileSystem : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Unicode.cs b/Examples/UICatalog/Scenarios/Unicode.cs index fe062061c0..69343f1bae 100644 --- a/Examples/UICatalog/Scenarios/Unicode.cs +++ b/Examples/UICatalog/Scenarios/Unicode.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Text; using Terminal.Gui.Editor; // ReSharper disable AccessToDisposedClosure @@ -17,8 +17,6 @@ public override void Main () var gitString = $"gui.cs 糊 (hú) {Glyphs.IdenticalTo} {Glyphs.DownArrow}18 {Glyphs.UpArrow}10 {Glyphs.VerticalFourDots}1 {Glyphs.HorizontalEllipsis}"; - ConfigurationManager.Enable (ConfigLocations.All); - using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index 8a50f67562..58fde1e627 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -10,7 +10,6 @@ public class ViewportSettings : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/WideGlyphs.cs b/Examples/UICatalog/Scenarios/WideGlyphs.cs index de1ecbdbbb..b52a3dc372 100644 --- a/Examples/UICatalog/Scenarios/WideGlyphs.cs +++ b/Examples/UICatalog/Scenarios/WideGlyphs.cs @@ -13,7 +13,6 @@ public sealed class WideGlyphs : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); // Init using IApplication app = Application.Create (); diff --git a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs index 081b6611ae..f77b0805ef 100644 --- a/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs +++ b/Examples/UICatalog/Scenarios/WindowsAndFrameViews.cs @@ -9,7 +9,6 @@ public class WindowsAndFrameViews : Scenario { public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/Scenarios/Wizards.cs b/Examples/UICatalog/Scenarios/Wizards.cs index f2d07edd1a..bb3c1061d0 100644 --- a/Examples/UICatalog/Scenarios/Wizards.cs +++ b/Examples/UICatalog/Scenarios/Wizards.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable // ReSharper disable AccessToDisposedClosure namespace UICatalog.Scenarios; @@ -15,7 +15,6 @@ public class Wizards : Scenario public override void Main () { - ConfigurationManager.Enable (ConfigLocations.All); using IApplication app = Application.Create (); app.Init (); diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index b7ea221c03..66cb5cc662 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -1,4 +1,4 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; global using Terminal.Gui.App; global using Terminal.Gui.ViewBase; @@ -105,7 +105,7 @@ private static int Main (string [] args) // Configuration Management Option disableConfigManagement = new ("--disable-cm") { - Description = "Indicates Configuration Management should not be enabled. Only `ConfigLocations.HardCoded` settings will be loaded." + Description = "Skip applying CLI driver / 16-color overlays via TuiConfigurationBuilder.RuntimeConfig. Library defaults still load at assembly load." }; disableConfigManagement.Aliases.Add ("-dcm"); disableConfigManagement.Aliases.Add ("--dcm"); @@ -293,7 +293,6 @@ private static void UICatalogMain (UICatalogCommandLineOptions options) if (!Options.DontEnableConfigurationManagement) { runner.SetRuntimeConfig (options.Driver, options.Force16Colors); - ConfigurationManager.Enable (ConfigLocations.All); } else { diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index 8ad0501caa..b9295d22b3 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -27,7 +27,7 @@ public UICatalogRunnable () { Title = "UICatalog"; SchemeName = CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); - ConfigurationManager.Applied += ConfigAppliedHandler; + ThemeChanges.ThemeChanged += ConfigAppliedHandler; } /// @@ -128,7 +128,7 @@ protected override void OnIsRunningChanged (bool newIsRunning) return; } - ConfigurationManager.Applied -= ConfigAppliedHandler; + ThemeChanges.ThemeChanged -= ConfigAppliedHandler; } // Track if this is the first time running the main UI Catalog screen @@ -200,7 +200,6 @@ View [] CreateThemeMenuItems () menuItems.Add (new Line ()); - if (ConfigurationManager.IsEnabled) { _themesSelector = new OptionSelector { CanFocus = true }; _themesSelector.ValueChanged += OnThemesSelectorOnValueChanged; @@ -224,10 +223,6 @@ View [] CreateThemeMenuItems () UpdateThemesMenu (); } - else - { - menuItems.Add (new MenuItem { Title = "Configuration Manager is not Enabled", Enabled = false }); - } return menuItems.ToArray (); @@ -711,8 +706,6 @@ private void CategoryView_SelectedChanged (object? sender, ValueChangedEventArgs #region StatusBar private StatusBar? _statusBar; - - [ConfigurationProperty (Scope = typeof (AppSettingsScope), OmitClassName = true)] [JsonPropertyName ("UICatalog.StatusBar")] public static bool ShowStatusBar { @@ -890,7 +883,7 @@ private void ConfigApplied () App?.TopRunnableView?.SetNeedsDraw (); } - private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) => ConfigApplied (); + private void ConfigAppliedHandler (object? sender, EventArgs? a) => ConfigApplied (); #endregion Configuration Manager diff --git a/GitVersion.yml b/GitVersion.yml index 18f1a8b6f5..5f00bdd66f 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -21,6 +21,7 @@ # - from develop: 2.0.0-develop.1 (patch version increments) # - from main (pre-release): 2.0.0-prealpha.1 or 2.0.0-beta.1 # - from main (release): 2.0.0 (patch version increments) +# - To bump minor on develop, include `+semver: minor` in a commit message. Do not pin next-version. # mode: ContinuousDelivery # GitVersion 6.x is configured here to use ContinuousDelivery mode for this GitFlow-style setup @@ -34,7 +35,7 @@ branches: # Matches the main branch regex: ^main$ # Uses an empty label for stable releases - label: '' + label: beta # Increments patch version (x.y.z+1) on commits increment: Patch # Specifies develop as the source branch diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index 00071b3312..d5f794adc2 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -15,17 +15,12 @@ global using Terminal.Gui.ViewBase; global using Terminal.Gui.Views; global using Attribute = Terminal.Gui.Drawing.Attribute; -#pragma warning disable CS0618 // Obsolete - ConfigurationManager alias still used internally during transition -global using CM = Terminal.Gui.Configuration.ConfigurationManager; -#pragma warning restore CS0618 global using Color = Terminal.Gui.Drawing.Color; using System.Globalization; using System.Reflection; using System.Resources; using Terminal.Gui.Tracing; -#pragma warning disable CS0618 // Obsolete - Application still uses ConfigurationPropertyAttribute/SettingsScope during transition - namespace Terminal.Gui.App; /// @@ -239,7 +234,6 @@ internal static List GetSupportedCultures () /// application interacts with the terminal buffer. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static AppModel AppModel { get => ApplicationSettings.Defaults.AppModel; @@ -269,7 +263,6 @@ public static Point? ForceInlinePosition } /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static string ForceDriver { get => ApplicationSettings.Defaults.ForceDriver; @@ -286,6 +279,10 @@ public static string ForceDriver /// Each entry maps a to a /// that specifies the key strings for all platforms or specific ones. /// + /// Nested JSON Application.DefaultKeyBindings overlays this dictionary by command when + /// runs. Unmentioned commands keep these defaults. + /// + /// /// To change a single binding and have the change take effect immediately, use /// or /// instead of mutating the dictionary directly. Direct dictionary mutation @@ -297,7 +294,6 @@ public static string ForceDriver /// Do not set in parallelizable unit tests. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Dictionary? DefaultKeyBindings { get; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 9ee447ad89..194598274a 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -1,9 +1,8 @@ using System.Diagnostics; +using Terminal.Gui.Configuration; using Wcwidth; using Trace = Terminal.Gui.Tracing.Trace; -#pragma warning disable CS0618 // Obsolete - ConfigurationManager still used internally during transition - namespace Terminal.Gui.App; internal partial class ApplicationImpl @@ -14,6 +13,8 @@ internal partial class ApplicationImpl /// public bool Initialized { get; set; } + internal SynchronizationContext? SynchronizationContext { get; private set; } + /// public event EventHandler>? InitializedChanged; @@ -79,7 +80,11 @@ public IApplication Init (string? driverName = null) RaiseInitializedChanged (this, new EventArgs (true)); SubscribeDriverEvents (); - SynchronizationContext.SetSynchronizationContext (new SynchronizationContext ()); + // Created here; installed as the thread's ambient context only while a session is running + // (see Begin/Run). Installing it at Init time would make an await between Init and Run + // capture a context whose continuations only execute once the main loop is pumping — + // stranding the continuation that was going to start the loop (#5636). + SynchronizationContext = new MainLoopSyncContext (this); _result = null; @@ -182,8 +187,7 @@ private void DisposeCore () // ResetState handles the case where Initialized is false ResetState (); - // Configuration manager diagnostics - ConfigurationManager.PrintJsonErrors (); + TuiJsonErrors.Print (); // Raise the initialized changed event to notify shutdown if (wasInitialized) @@ -302,15 +306,16 @@ public void ResetState (bool ignoreDisposed = false) // === 8. Reset initialization state === Initialized = false; MainThreadId = null; + ResetHasEndedSession (); // === 9. Reset synchronization context === - // IMPORTANT: Always reset sync context, even if not initialized - // This ensures cleanup works correctly even if Shutdown is called without Init - // Reset synchronization context to allow the user to run async/await, - // as the main loop has been ended, the synchronization context from - // gui.cs does no longer process any callbacks. See #1084 for more details: - // (https://github.com/tui-cs/Terminal.Gui/issues/1084). - SynchronizationContext.SetSynchronizationContext (null); + // If this app's context is still the thread's ambient context, clear it so later + // async/await does not capture a context that no longer processes callbacks (#1084). + // A foreign ambient context (the caller's own) is left untouched. + if (SynchronizationContext is { } ownContext && System.Threading.SynchronizationContext.Current == ownContext) + { + System.Threading.SynchronizationContext.SetSynchronizationContext (null); + } // === 10. Unsubscribe from Application static property change events === UnsubscribeApplicationEvents (); diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index 3b4cbfbd1f..8657e40153 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -136,6 +136,11 @@ public void Invoke (Action action) // Set the application reference in the runnable runnable.SetApp (this); + // Set the synchronization context to MainLoopSyncContext for this application, saving the + // caller's context on the token so End can restore it (#5636). + token.PreviousSynchronizationContext = System.Threading.SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext (SynchronizationContext); + // Ensure the mouse is ungrabbed Mouse.UngrabMouse (); @@ -234,47 +239,59 @@ public void Invoke (Action action) throw new NotInitializedException (@"Init must be called before Run."); } - // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) - - SessionToken? token = + // Begin installs this app's MainLoopSyncContext as the thread's ambient context for the + // duration of the session; restore the caller's context on exit so an await after Run + // does not capture a context that is no longer pumping (#5636). + SynchronizationContext? previousContext = System.Threading.SynchronizationContext.Current; - // Find it on the stack - runnable.IsRunning ? SessionStack?.FirstOrDefault (st => st.Runnable == runnable) : Begin (runnable); - - if (token is null) + try { - Logging.Warning (@"Run - Begin session failed or was cancelled."); + // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) - return null; - } + SessionToken? token = - // Loop to handle the case where End is cancelled by an IsRunningChanging handler. - // When End is cancelled, IsRunning remains true; we reset StopRequested and re-run the loop. - while (true) - { - try - { - // All runnables block until RequestStop() is called - RunLoop (runnable, errorHandler); - } - finally + // Find it on the stack + runnable.IsRunning ? SessionStack?.FirstOrDefault (st => st.Runnable == runnable) : Begin (runnable); + + if (token is null) { - // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) - End (token); + Logging.Warning (@"Run - Begin session failed or was cancelled."); + + return null; } - // If End succeeded IsRunning is now false — we are done - if (!runnable.IsRunning) + // Loop to handle the case where End is cancelled by an IsRunningChanging handler. + // When End is cancelled, IsRunning remains true; we reset StopRequested and re-run the loop. + while (true) { - break; + try + { + // All runnables block until RequestStop() is called + RunLoop (runnable, errorHandler); + } + finally + { + // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) + End (token); + } + + // If End succeeded IsRunning is now false — we are done + if (!runnable.IsRunning) + { + break; + } + + // End was cancelled by an IsRunningChanging handler (e.g., "Are you sure?" veto). + // Reset StopRequested so RunLoop can re-enter its while condition correctly. + runnable.StopRequested = false; } - // End was cancelled by an IsRunningChanging handler (e.g., "Are you sure?" veto). - // Reset StopRequested so RunLoop can re-enter its while condition correctly. - runnable.StopRequested = false; + return token.Result; + } + finally + { + System.Threading.SynchronizationContext.SetSynchronizationContext (previousContext); } - - return token.Result; } /// @@ -496,9 +513,34 @@ public void End (SessionToken token) // Clear the Runnable from the token token.Runnable = null; + HasEndedSession = true; SessionEnded?.Invoke (this, new SessionTokenEventArgs (token)); + + // Restore the ambient context the caller had at Begin, so an await after a directly-begun + // session cannot capture a context that is no longer pumping (#5636). For nested sessions + // the previous context is the same app context, so the outermost End restores the caller's. + if (System.Threading.SynchronizationContext.Current == SynchronizationContext) + { + System.Threading.SynchronizationContext.SetSynchronizationContext (token.PreviousSynchronizationContext); + } } + /// + /// INTERNAL: Whether any session has ended. Once true (and no session is running), posts to + /// fall back to the thread pool instead of queueing onto a + /// loop that may never pump again. + /// + internal bool HasEndedSession { get; private set; } + + /// + /// INTERNAL: Whether work posted to can rely on the main + /// loop to pump it: the app is initialized, and either a session is running or none has run + /// to completion yet (posts made between Init and the first Run are pumped by that Run). + /// + internal bool CanPumpPostedWork => Initialized && (TopRunnable is { IsRunning: true } || !HasEndedSession); + + internal void ResetHasEndedSession () => HasEndedSession = false; + #endregion Session Lifecycle - End #region Session Lifecycle - RequestStop diff --git a/Terminal.Gui/App/ApplicationPopover.cs b/Terminal.Gui/App/ApplicationPopover.cs index 204043f05e..7cff9a3e5f 100644 --- a/Terminal.Gui/App/ApplicationPopover.cs +++ b/Terminal.Gui/App/ApplicationPopover.cs @@ -57,6 +57,9 @@ public void Dispose () /// When a popover is registered, the View instance lifetime is managed by the application. Call /// /// to manage the lifetime of the popover directly. + /// If is the active popover, it is removed from the registry first, then hidden so the + /// region it covered is invalidated. Unregistering before prevents a + /// handler from calling during hide. /// /// /// @@ -67,6 +70,20 @@ public bool DeRegister (IPopoverView? popover) return false; } + bool wasActive = GetActivePopover () == popover; + + // Unregister before Hide. Hide sets Visible=false and raises VisibleChanged. + // A handler can call Show only while the popover is still registered. + _popovers.Remove (popover); + + if (wasActive) + { + // Hide after unregister so the covered region is still invalidated (#5635). + Hide (popover); + } + + // A VisibleChanged handler may have re-registered via MakeVisible and restored + // _activePopover. The caller asked to deregister. if (GetActivePopover () == popover) { _activePopover = null; diff --git a/Terminal.Gui/App/Legacy/Application.Mouse.cs b/Terminal.Gui/App/Legacy/Application.Mouse.cs index e09e3249de..80791642d8 100644 --- a/Terminal.Gui/App/Legacy/Application.Mouse.cs +++ b/Terminal.Gui/App/Legacy/Application.Mouse.cs @@ -5,7 +5,6 @@ namespace Terminal.Gui.App; public static partial class Application // Mouse handling { /// Disable or enable the mouse. The mouse is enabled by default. - [ConfigurationProperty (Scope = typeof (SettingsScope))] [Obsolete ("The legacy static Application object is going away.")] public static bool IsMouseDisabled { diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index 42819ecbd9..3705b4d043 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -71,8 +71,7 @@ public async Task StartInputTaskAsync (IApplication? app) Task waitForSemaphore = _startupSemaphore.WaitAsync (); // Wait for either the semaphore to be released or the input task to crash. - // ReSharper disable once UseConfigureAwaitFalse - Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask); + Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false); // Check if the task was the input task and if it has failed. if (completedTask == _inputTask) @@ -223,11 +222,6 @@ private void BuildDriverIfPossible (IApplication? app) { if (!result.IsSupported) { - if (_inputProcessor is AnsiInputProcessor unsupportedAnsiInputProcessor) - { - unsupportedAnsiInputProcessor.SetKittyKeyboardEnabled (false); - } - Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", "Kitty keyboard mode not enabled"); return; @@ -235,12 +229,6 @@ private void BuildDriverIfPossible (IApplication? app) // Kitty is supported. Store the capabilities and set the flags we care about. _driver?.SetKittyKeyboardCapabilities (result); - - if (_inputProcessor is AnsiInputProcessor supportedAnsiInputProcessor) - { - supportedAnsiInputProcessor.SetKittyKeyboardEnabled (true); - } - kittyKeyboardDetector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); Trace.Lifecycle (app?.MainThreadId?.ToString (), diff --git a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs index b9375ccca4..e5ad8b6521 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopSyncContext.cs @@ -1,43 +1,97 @@ -#nullable disable namespace Terminal.Gui.App; -///// -///// provides the sync context set while executing code in Terminal.Gui, to let -///// users use async/await on their code -///// -//internal sealed class MainLoopSyncContext : SynchronizationContext -//{ -// public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } - -// public override void Post (SendOrPostCallback d, object state) -// { -// // Queue the task using the modern architecture -// ApplicationImpl.Instance.Invoke (() => { d (state); }); -// } - -// //_mainLoop.Driver.Wakeup (); -// public override void Send (SendOrPostCallback d, object state) -// { -// if (Thread.CurrentThread.ManagedThreadId == Application.MainThreadId) -// { -// d (state); -// } -// else -// { -// var wasExecuted = false; - -// ApplicationImpl.Instance.Invoke ( -// () => -// { -// d (state); -// wasExecuted = true; -// } -// ); - -// while (!wasExecuted) -// { -// Thread.Sleep (15); -// } -// } -// } -//} +/// +/// Provides the sync context set while executing code in Terminal.Gui, to let +/// users use async/await on their code +/// +internal sealed class MainLoopSyncContext : SynchronizationContext +{ + private readonly IApplication _app; + + /// + /// Initializes a new instance of the class. + /// + /// The application instance that owns the main loop. + public MainLoopSyncContext (IApplication app) => _app = app; + + /// + public override SynchronizationContext CreateCopy () => new MainLoopSyncContext (_app); + + private bool CanPump => _app is ApplicationImpl { CanPumpPostedWork: true }; + + /// + public override void Post (SendOrPostCallback d, object? state) + { + ArgumentNullException.ThrowIfNull (d); + + // With no main loop pumping (after Shutdown/Dispose, or after a session ended with none + // running), run the callback on the thread pool instead of stranding it — and any awaiter — + // forever (#5636). Posts made between Init and the first Run stay queued for that Run. + if (!CanPump) + { + ThreadPool.QueueUserWorkItem ( + static s => + { + (SendOrPostCallback callback, object? callbackState) = ((SendOrPostCallback, object?))s!; + callback (callbackState); + }, + (d, state)); + + return; + } + + // Queue the task using the modern architecture + _app.Invoke (() => d (state)); + } + + /// + public override void Send (SendOrPostCallback d, object? state) + { + ArgumentNullException.ThrowIfNull (d); + + // With no main loop pumping, execute inline rather than waiting on a queue nothing drains. + if (!CanPump || _app.MainThreadId == Thread.CurrentThread.ManagedThreadId) + { + d (state); + + return; + } + + object gate = new (); + bool wasExecuted = false; + Exception? error = null; + + _app.Invoke (() => + { + try + { + d (state); + } + catch (Exception ex) + { + error = ex; + } + finally + { + lock (gate) + { + wasExecuted = true; + Monitor.Pulse (gate); + } + } + }); + + lock (gate) + { + while (!wasExecuted) + { + Monitor.Wait (gate); + } + } + + if (error is { }) + { + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture (error).Throw (); + } + } +} diff --git a/Terminal.Gui/App/Runnable/SessionToken.cs b/Terminal.Gui/App/Runnable/SessionToken.cs index 1df43a1fcb..c13d1587f4 100644 --- a/Terminal.Gui/App/Runnable/SessionToken.cs +++ b/Terminal.Gui/App/Runnable/SessionToken.cs @@ -18,4 +18,11 @@ public class SessionToken /// The result of the session. Typically set by the runnable in /// public object? Result { get; set; } + + /// + /// The ambient the caller had when + /// installed the app's context; + /// restored by . + /// + internal SynchronizationContext? PreviousSynchronizationContext { get; set; } } diff --git a/Terminal.Gui/App/Tracing/Trace.cs b/Terminal.Gui/App/Tracing/Trace.cs index 2b67908aeb..512677f7a5 100644 --- a/Terminal.Gui/App/Tracing/Trace.cs +++ b/Terminal.Gui/App/Tracing/Trace.cs @@ -35,7 +35,6 @@ public static class Trace /// Gets or sets the enabled trace categories for the current async context. /// This property is thread-safe and isolated per async flow. /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (TraceCategoryJsonConverter))] public static TraceCategory EnabledCategories { diff --git a/Terminal.Gui/Configuration/AppSettingsScope.cs b/Terminal.Gui/Configuration/AppSettingsScope.cs deleted file mode 100644 index ed5e364030..0000000000 --- a/Terminal.Gui/Configuration/AppSettingsScope.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; - -#pragma warning disable CS0618 // Obsolete - AppSettingsScope references ConfigurationPropertyAttribute in docs - -namespace Terminal.Gui.Configuration; - -/// The class for application-defined configuration settings. -/// -/// -/// -/// Use the attribute to mark properties that should be -/// serialized as part of application-defined configuration settings. -/// -/// -/// public class MyAppSettings { -/// [ConfigurationProperty] -/// public static bool? MyProperty { get; set; } = true; -/// } -/// -/// The resultant Json will look like this: -/// -/// "AppSettings": { -/// "MyAppSettings.MyProperty": true, -/// "UICatalog.ShowStatusBar": true -/// }, -/// -/// -[Obsolete ("Use Microsoft.Extensions.Configuration with TuiConfigurationBuilder instead. See TuiConfigurationBuilder docs for migration guide.")] -[JsonConverter (typeof (ScopeJsonConverter))] -public class AppSettingsScope : Scope -{ } diff --git a/Terminal.Gui/Configuration/ConfigLocations.cs b/Terminal.Gui/Configuration/ConfigLocations.cs deleted file mode 100644 index d14823f912..0000000000 --- a/Terminal.Gui/Configuration/ConfigLocations.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace Terminal.Gui.Configuration; - -/// -/// Describes the location of the configuration settings. The constants can be combined (bitwise) to specify multiple -/// locations. The more significant the bit, the higher the priority the location, meaning that the last location will -/// override the -/// earlier ones. -/// -[Flags] -public enum ConfigLocations -{ - /// - /// No locations are specified. This is the default value. - /// - None = 0, - - /// - /// Settings of the static properties when the module is - /// initially loaded. - /// - /// When the module is initialized, the will retrieve the values of the - /// configuration - /// properties - /// from their corresponding static properties. These are default settings available even if - /// - /// is . - /// - /// - HardCoded = 0b_0000_0000_0001, - - /// - /// Settings defined in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json). - /// - LibraryResources = 0b_0000_0000_0010, - - /// - /// App resources (e.g. MyApp.Resources.config.json). See . - /// - AppResources = 0b_0000_0000_0100, - - /// - /// Global settings in the home directory (e.g. ~/.tui/config.json). - /// - GlobalHome = 0b_0000_0000_1000, - - /// - /// Global settings in the current directory (e.g. ./.tui/config.json). - /// - GlobalCurrent = 0b_0000_0001_0000, - - /// - /// App settings in the home directory (e.g. ~/.tui/MyApp.config.json). - /// - AppHome = 0b_0000_0010_0000, - - /// - /// App settings in the current directory (e.g. ./.tui/MyApp.config.json). - /// - AppCurrent = 0b_0000_0100_0000, - - /// - /// Settings from the TUI_CONFIG environment variable. - /// - Env = 0b_0000_1000_0000, - - /// - /// Settings in the static property. - /// - Runtime = 0b_0001_0000_0000, - - /// This constant is a combination of all locations - All = HardCoded | LibraryResources | AppResources | GlobalHome | GlobalCurrent | AppHome | AppCurrent | Env | Runtime -} diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs deleted file mode 100644 index 9a1804b722..0000000000 --- a/Terminal.Gui/Configuration/ConfigProperty.cs +++ /dev/null @@ -1,654 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using Terminal.Gui.Tracing; - -#pragma warning disable CS0618 // Obsolete - ConfigProperty references ConfigurationManager/ConfigurationPropertyAttribute internally - -namespace Terminal.Gui.Configuration; - -/// -/// Holds a property's value and the that allows to -/// retrieve and apply the property's value. -/// -/// -/// Configuration properties must be / and and have the -/// attribute. If the type of the property requires specialized JSON -/// serialization, a must be provided using the -/// attribute. -/// -[Obsolete ("Being replaced by Microsoft.Extensions.Configuration. Will be removed in a future version.")] -public class ConfigProperty -{ - /// Describes the property. - public PropertyInfo? PropertyInfo { get; set; } - - /// INTERNAL: Cached value of ConfigurationPropertyAttribute.OmitClassName; makes more AOT friendly. - internal bool OmitClassName { get; set; } - - /// INTERNAL: Cached value of ConfigurationPropertyAttribute.Scope; makes more AOT friendly. - internal string? ScopeType { get; set; } - - /// INTERNAL: Cached value of JsonConverterAttribute.ConverterType; makes more AOT friendly. - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - internal Type? ConverterType { get; set; } - - private object? _propertyValue; - - /// - /// Holds the property's value as it was either read from the class's implementation or from a config file. If the - /// property has not been set (e.g. because no configuration file specified a value), will be . - /// - public object? PropertyValue - { - get => _propertyValue; - set - { - if (Immutable) - { - throw new InvalidOperationException ($"Property {PropertyInfo?.Name} is immutable and cannot be set."); - } - - // TODO: Verify value is correct type? - - _propertyValue = value; - HasValue = true; - } - } - - /// - /// Gets or sets whether this config property has a value. This is set to when is set. - /// - public bool HasValue { get; set; } - - /// - /// INTERNAL: Gets or sets whether this property is immutable. If , the property cannot be changed. - /// - internal bool Immutable { get; set; } - - /// Applies the to the static property described by . - /// - public bool Apply () - { - try - { - if (PropertyInfo?.GetValue (null) is { }) - { - // Use DeepCloner to create a deep copy of PropertyValue - object? val = DeepCloner.DeepClone (PropertyValue); - - Debug.Assert (!Immutable); - PropertyInfo.SetValue (null, val); - - } - } - catch (TargetInvocationException tie) - { - if (tie.InnerException is { }) - { - throw new JsonException ( - $"Error Applying Configuration Change: {tie.InnerException.Message}", - tie.InnerException - ); - } - - throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie); - } - catch (ArgumentException ae) - { - throw new JsonException ( - $"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", - ae - ); - } - - return PropertyValue != null; - } - - /// - /// INTERNAL: Creates a copy of a ConfigProperty with the same metadata but no value. - /// - /// The source ConfigProperty. - /// A new ConfigProperty instance. - internal static ConfigProperty CreateCopy (ConfigProperty source) - { - return new ConfigProperty - { - Immutable = false, - PropertyInfo = source.PropertyInfo, - OmitClassName = source.OmitClassName, - ScopeType = source.ScopeType, - ConverterType = source.ConverterType, - HasValue = false - }; - } - - /// - /// INTERNAL: Create an immutable ConfigProperty with cached attribute information - /// - /// The PropertyInfo to create from - /// A new ConfigProperty with attribute data cached - internal static ConfigProperty CreateImmutableWithAttributeInfo (PropertyInfo propertyInfo) - { - var attr = propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute; - var jsonConverterAttribute = propertyInfo.GetCustomAttribute (typeof (JsonConverterAttribute)) as JsonConverterAttribute; - - return new ConfigProperty - { - PropertyInfo = propertyInfo, - OmitClassName = attr?.OmitClassName ?? false, - ScopeType = attr?.Scope!.Name, - ConverterType = jsonConverterAttribute?.ConverterType ?? InferConverterType (propertyInfo), - // By default, properties are immutable - Immutable = true - }; - } - - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - private static Type? InferConverterType (PropertyInfo propertyInfo) - { - Type propertyType = propertyInfo.PropertyType; - - if (propertyType == typeof (ConcurrentDictionary)) - { - return typeof (ConcurrentDictionaryJsonConverter); - } - - if (propertyType == typeof (Dictionary)) - { - return typeof (DictionaryJsonConverter); - } - - if (propertyInfo.DeclaringType == typeof (Terminal.Gui.Tracing.Trace) && propertyType == typeof (TraceCategory)) - { - return typeof (TraceCategoryJsonConverter); - } - - return null; - } - - /// - /// INTERNAL: Helper method to get the ConfigurationPropertyAttribute for a PropertyInfo - /// - /// The PropertyInfo to get the attribute from - /// The ConfigurationPropertyAttribute if found; otherwise, null - internal static ConfigurationPropertyAttribute? GetConfigurationPropertyAttribute (PropertyInfo propertyInfo) - { - return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) as ConfigurationPropertyAttribute; - } - - /// - /// INTERNAL: Helper method to check if a PropertyInfo has a ConfigurationPropertyAttribute - /// - /// The PropertyInfo to check - /// True if the PropertyInfo has a ConfigurationPropertyAttribute; otherwise, false - internal static bool HasConfigurationPropertyAttribute (PropertyInfo propertyInfo) - { - try - { - return propertyInfo.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) != null; - } - catch (TypeLoadException) - { - // Foreign assemblies loaded into the process can carry broken custom-attribute metadata. - // Those properties are not Terminal.Gui configuration hosts and should not terminate the scan. - return false; - } - } - - /// - /// INTERNAL: Helper to get either the Json property named (specified by [JsonPropertyName(name)] or the actual property - /// name. - /// - /// - /// - internal static string GetJsonPropertyName (PropertyInfo pi) - { - var attr = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute; - - return attr?.Name ?? pi.Name; - } - - /// - /// Updates (using reflection) the from the static - /// property described in . - /// - /// - public object? UpdateToCurrentValue () - { - return PropertyValue = PropertyInfo!.GetValue (null); - } - - /// - /// INTERNAL: Updates with the value in using a deep memberwise copy that - /// copies only the values that . - /// - /// The source object to copy values from. - /// The updated property value. - /// Thrown when the source type doesn't match the property type. - internal object? UpdateFrom (object? source) - { - // If the source (higher-priority layer) doesn't provide a value, keep the existing value - // In the context of layering, a null source means the higher-priority layer doesn't specify a value, - // so we should retain the value from the lower-priority layer. - if (source is null) - { - return PropertyValue; - } - - // Process the source based on its type - if (source is ConcurrentDictionary themeDictSource && - PropertyValue is ConcurrentDictionary themeDictDest) - { - UpdateThemeScopeDictionary (themeDictSource, themeDictDest); - } - else if (source is ConcurrentDictionary concurrentDictSource && - PropertyValue is ConcurrentDictionary concurrentDictDest) - { - UpdateConfigPropertyConcurrentDictionary (concurrentDictSource, concurrentDictDest); - } - else if (source is Dictionary dictSource && - PropertyValue is Dictionary dictDest) - { - UpdateConfigPropertyDictionary (dictSource, dictDest); - } - else if (source is ConfigProperty configProperty) - { - if (configProperty.HasValue) - { - PropertyValue = DeepCloner.DeepClone (configProperty.PropertyValue); - } - } - else if (source is Dictionary dictSchemeSource && - PropertyValue is Dictionary dictSchemesDest) - { - UpdateSchemeDictionary (dictSchemeSource, dictSchemesDest); - } - else if (source is Scheme scheme) - { - PropertyValue = new Scheme (scheme); // use copy constructor - } - - else - { - // Validate type compatibility for non-dictionary types - ValidateTypeCompatibility (source); - - // For non-scope types, perform a deep copy of the source value to ensure immutability - PropertyValue = DeepCloner.DeepClone (source); - } - - return PropertyValue; - } - - /// - /// Validates that the source type is compatible with the property type. - /// - /// The source object to validate. - /// Thrown when the source type doesn't match the property type. - private void ValidateTypeCompatibility (object source) - { - Type? underlyingType = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType); - - bool isCompatibleType = source.GetType () == PropertyInfo.PropertyType || - (underlyingType is { } && source.GetType () == underlyingType); - - if (!isCompatibleType) - { - throw new ArgumentException ( - $"The source object ({PropertyInfo.DeclaringType}.{PropertyInfo.Name}) is not of type {PropertyInfo.PropertyType}." - ); - } - } - - - /// - /// Updates a Scheme object by selectively applying explicitly set attributes from the source. - /// - /// The source Scheme. - /// The destination Scheme to update. - private void UpdateScheme (Scheme sourceScheme, Scheme destScheme) - { - // We can't modify properties of a record directly, so we need to create a new one - // First, create a clone of the destination to preserve any values - var updatedScheme = new Scheme (destScheme); - - //// Use with expressions to update only explicitly set attributes - //// For each role, check if the source has an explicitly set attribute - //if (sourceScheme.Normal.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Normal = sourceScheme.Normal }; - //} - - //if (sourceScheme.HotNormal.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { HotNormal = sourceScheme.HotNormal }; - //} - - //if (sourceScheme.Focus.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Focus = sourceScheme.Focus }; - //} - - //if (sourceScheme.HotFocus.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { HotFocus = sourceScheme.HotFocus }; - //} - - //if (sourceScheme.Active.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Active = sourceScheme.Active }; - //} - - //if (sourceScheme.HotActive.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { HotActive = sourceScheme.HotActive }; - //} - - //if (sourceScheme.Highlight.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Highlight = sourceScheme.Highlight }; - //} - - //if (sourceScheme.Editable.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Editable = sourceScheme.Editable }; - //} - - //if (sourceScheme.ReadOnly.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { ReadOnly = sourceScheme.ReadOnly }; - //} - - //if (sourceScheme.Disabled.IsExplicitlySet) - //{ - // updatedScheme = updatedScheme with { Disabled = sourceScheme.Disabled }; - //} - - // Update the PropertyValue with the merged scheme - PropertyValue = updatedScheme; - } - - /// - /// Updates a ThemeScope dictionary with values from a source dictionary. - /// - /// The source ThemeScope dictionary. - /// The destination ThemeScope dictionary. - private static void UpdateThemeScopeDictionary ( - ConcurrentDictionary source, - ConcurrentDictionary destination) - { - foreach (KeyValuePair scope in source) - { - if (!destination.ContainsKey (scope.Key)) - { - destination.TryAdd (scope.Key, scope.Value); - continue; - } - - destination [scope.Key].UpdateFrom (scope.Value); - } - } - - /// - /// Updates a ConfigProperty dictionary with values from a source dictionary. - /// - /// The source ConfigProperty dictionary. - /// The destination ConfigProperty dictionary. - private static void UpdateConfigPropertyConcurrentDictionary ( - ConcurrentDictionary source, - ConcurrentDictionary destination) - { - foreach (KeyValuePair sourceProp in source) - { - // Skip properties without values - if (!sourceProp.Value.HasValue) - { - continue; - } - - if (!destination.ContainsKey (sourceProp.Key)) - { - // Add the property to the destination - var copy = CreateCopy (sourceProp.Value); - destination.TryAdd (sourceProp.Key, copy); - } - - // Update the value in the destination - destination [sourceProp.Key].UpdateFrom (sourceProp.Value); - } - } - - /// - /// Updates a ConfigProperty dictionary with values from a source dictionary. - /// - /// The source ConfigProperty dictionary. - /// The destination ConfigProperty dictionary. - private static void UpdateConfigPropertyDictionary ( - Dictionary source, - Dictionary destination) - { - foreach (KeyValuePair sourceProp in source) - { - // Skip properties without values - if (!sourceProp.Value.HasValue) - { - continue; - } - - if (!destination.ContainsKey (sourceProp.Key)) - { - // Add the property to the destination - var copy = CreateCopy (sourceProp.Value); - destination.Add (sourceProp.Key, copy); - } - - // Update the value in the destination - destination [sourceProp.Key].UpdateFrom (sourceProp.Value); - } - } - - /// - /// Updates a ConfigProperty dictionary with values from a source dictionary. - /// - /// The source ConfigProperty dictionary. - /// The destination ConfigProperty dictionary. - private static void UpdateSchemeDictionary ( - Dictionary source, - Dictionary destination) - { - foreach (KeyValuePair sourceProp in source) - { - if (!destination.ContainsKey (sourceProp.Key)) - { - // Add the property to the destination - // Schemes are structs are passed by val - destination.Add (sourceProp.Key, sourceProp.Value); - } - - // Update the value in the destination - // Schemes are structs are passed by val - destination [sourceProp.Key] = sourceProp.Value; - } - } - - #region Initialization - - /// - /// INTERNAL: A cache of all classes that have properties decorated with the . - /// - /// Is until is called. - private static ImmutableSortedDictionary? _classesWithConfigProps; - - /// - /// INTERNAL: Called from the method to initialize the - /// dictionary. - /// - /// - /// - /// The set of host types that ship in Terminal.Gui is declared statically in - /// and registered unconditionally. Declaring the set statically keeps initialization - /// trim- and AOT-safe without requiring consumer apps to carry <TrimmerRootAssembly Include="Terminal.Gui" />. - /// See . - /// - /// - /// Additional host types defined outside Terminal.Gui (test suites, plugins, embedding apps) are picked up via a - /// runtime assembly scan. Under NativeAOT this still works for consumer-rooted metadata, so initialization must not - /// treat "no dynamic code" as "no reflection". Consumers that define their own - /// hosts are responsible for preserving those types for trimming/AOT. - /// - /// - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "The runtime scan is required to discover consumer-defined configuration hosts outside Terminal.Gui. Reflection works under NativeAOT when the consumer roots those types.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "The runtime scan is required to discover consumer-defined configuration hosts outside Terminal.Gui. Consumers must preserve their config host types for trimming/AOT.")] - internal static void Initialize () - { - if (_classesWithConfigProps is { }) - { - return; - } - - Dictionary dict = new (StringComparer.InvariantCultureIgnoreCase); - - // Trim/AOT-safe: statically-rooted host types that ship with Terminal.Gui. - foreach (Type type in ConfigPropertyHostTypes.GetTypes ()) - { - dict [type.Name] = type; - } - - // Supplement the built-in list by discovering host types defined outside Terminal.Gui - // (test suites, plugins, embedding apps). This remains important under NativeAOT for - // consumer-rooted host types such as app-defined AppSettingsScope entries. - ScanLoadedAssembliesForConfigPropertyHosts (dict, AppDomain.CurrentDomain.GetAssemblies ()); - - _classesWithConfigProps = dict.ToImmutableSortedDictionary (); - } - - [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties.")] - [RequiresUnreferencedCode ("Scanning for types via reflection is not trim-safe.")] - internal static ImmutableSortedDictionary ScanAssembliesForConfigPropertyHosts (IEnumerable assemblies) - { - Dictionary dict = new (StringComparer.InvariantCultureIgnoreCase); - - ScanLoadedAssembliesForConfigPropertyHosts (dict, assemblies); - - return dict.ToImmutableSortedDictionary (); - } - - [RequiresDynamicCode ("Uses reflection to scan assemblies for configuration properties.")] - [RequiresUnreferencedCode ("Scanning for types via reflection is not trim-safe.")] - private static void ScanLoadedAssembliesForConfigPropertyHosts (Dictionary dict, IEnumerable assemblies) - { - foreach (Assembly assembly in assemblies) - { - try - { - if (assembly.IsDynamic) - { - continue; - } - - foreach (Type type in assembly.GetTypes ()) - { - PropertyInfo [] properties = type.GetProperties (); - var hasConfigProp = false; - - foreach (PropertyInfo prop in properties) - { - if (HasConfigurationPropertyAttribute (prop)) - { - hasConfigProp = true; - - break; - } - } - - if (!hasConfigProp) - { - continue; - } - - // TryAdd — never overwrite a statically-registered Terminal.Gui host. An external assembly - // with a same-named type (e.g. a consumer's own `Window`) must not displace the built-in entry. - dict.TryAdd (type.Name, type); - } - } - - // Skip problematic assemblies that can't be loaded or analyzed - catch (ReflectionTypeLoadException) - { - continue; - } - catch (BadImageFormatException) - { - continue; - } - } - } - - /// - /// INTERNAL: Retrieves a dictionary of all properties annotated with from the classes in the module. - /// The dictionary case-insensitive and sorted. - /// The items have set, but not . - /// is set to . - /// - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Called only on types preserved by ConfigPropertyHostTypes. HasConfigurationPropertyAttribute and CreateImmutableWithAttributeInfo operate only on statically-rooted types.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "GetProperties is called only on types preserved by ConfigPropertyHostTypes via DynamicDependency(PublicProperties).")] - [UnconditionalSuppressMessage ("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to 'Type' member. The return value of the source method does not have matching annotations.", Justification = "GetProperties is called only on types whose PublicProperties are preserved by ConfigPropertyHostTypes via DynamicDependency.")] - internal static ImmutableSortedDictionary GetAllConfigProperties () - { - if (_classesWithConfigProps is null) - { - throw new InvalidOperationException ("Initialize has not been called."); - } - - // Estimate capacity to reduce resizing operations - int estimatedCapacity = _classesWithConfigProps.Count * 5; // Assume ~5 properties per class - Dictionary allConfigProperties = new (estimatedCapacity, StringComparer.InvariantCultureIgnoreCase); - - // Process each class with direct iteration instead of LINQ - foreach (KeyValuePair classEntry in _classesWithConfigProps) - { - Type type = classEntry.Value; - - // Get all public static/instance properties - BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; - PropertyInfo [] properties = type.GetProperties (bindingFlags); - - foreach (PropertyInfo propertyInfo in properties) - { - // Skip properties without our attribute - if (!HasConfigurationPropertyAttribute (propertyInfo)) - { - continue; - } - - // Verify the property is static - if (!propertyInfo.GetGetMethod (true)!.IsStatic) - { - throw new InvalidOperationException ( - $"Property {propertyInfo.Name} in class {propertyInfo.DeclaringType?.Name} is not static. " + - "[ConfigurationProperty] properties must be static."); - } - - // Create config property with cached attribute data - ConfigProperty configProperty = CreateImmutableWithAttributeInfo (propertyInfo); - - // Use cached attribute data to determine the key - string key = configProperty.OmitClassName - ? GetJsonPropertyName (propertyInfo) - : $"{propertyInfo.DeclaringType?.Name}.{propertyInfo.Name}"; - - allConfigProperties.Add (key, configProperty); - } - } - - return allConfigProperties.ToImmutableSortedDictionary (StringComparer.InvariantCultureIgnoreCase); - } - - - #endregion Initialization -} diff --git a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs b/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs deleted file mode 100644 index 6fd66cd061..0000000000 --- a/Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Terminal.Gui.Tracing; - -#pragma warning disable CS0618 // Obsolete - ConfigPropertyHostTypes references ConfigurationPropertyAttribute during transition - -namespace Terminal.Gui.Configuration; - -/// -/// INTERNAL: The statically-known set of types that own -/// -decorated properties. -/// -/// -/// -/// used to discover these types by reflecting over every loaded -/// assembly at module-init time. That scan was not trim-safe: with PublishTrimmed=true, types -/// not otherwise referenced by the consuming application were stripped and -/// Scope<T>.GetUninitializedProperty would throw at startup. -/// See . -/// -/// -/// The per-type entries on root -/// each host and preserve its so the -/// trimmer does not remove the properties the scan depends on. -/// -/// -/// A unit test verifies this list stays in sync with the types that actually carry -/// so drift is caught at build time, not in AOT -/// consumer apps. -/// -/// -[Obsolete ("Being replaced by Microsoft.Extensions.Configuration. Will be removed in a future version.")] -internal static class ConfigPropertyHostTypes -{ - private const DynamicallyAccessedMemberTypes PRESERVED_MEMBERS = DynamicallyAccessedMemberTypes.PublicProperties; - - private static readonly Type [] _types = - [ - typeof (Application), - typeof (ConfigurationManager), - typeof (SchemeManager), - typeof (ThemeManager), - typeof (Color), - typeof (Glyphs), - typeof (Driver), - typeof (Key), - typeof (NerdFonts), - typeof (Trace), - typeof (View), - typeof (BorderView), - typeof (Button), - typeof (CharMap), - typeof (CheckBox), - typeof (Dialog), - typeof (FileDialog), - typeof (FileDialogStyle), - typeof (FrameView), - typeof (HexView), - typeof (LinearRangeDefaults), - typeof (Menu), - typeof (MenuBar), - typeof (MessageBox), - typeof (PopoverMenu), - typeof (SelectorBase), - typeof (StatusBar), - typeof (TextField), - typeof (TextView), - typeof (Window) - ]; - - [DynamicDependency (PRESERVED_MEMBERS, typeof (Application))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (ConfigurationManager))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (SchemeManager))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (ThemeManager))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Color))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Glyphs))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Driver))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Key))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (NerdFonts))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Trace))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (View))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (BorderView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Button))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (CharMap))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (CheckBox))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Dialog))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (FileDialog))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (FileDialogStyle))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (FrameView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (HexView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (LinearRangeDefaults))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Menu))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (MenuBar))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (MessageBox))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (PopoverMenu))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (SelectorBase))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (StatusBar))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (TextField))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (TextView))] - [DynamicDependency (PRESERVED_MEMBERS, typeof (Window))] - internal static Type [] GetTypes () => _types; -} diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs deleted file mode 100644 index 2d43d27b4d..0000000000 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ /dev/null @@ -1,844 +0,0 @@ -using System.Collections.Frozen; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Encodings.Web; -using System.Text.Json; -using System.Text.Json.Serialization; -using Trace = Terminal.Gui.Tracing.Trace; - -#pragma warning disable CS0618 // Obsolete - ConfigurationManager uses ConfigProperty/SettingsScope/ThemeScope internally - -namespace Terminal.Gui.Configuration; - -/// -/// Provides settings and configuration management for Terminal.Gui applications. See the Configuration Deep Dive for -/// more information: . -/// -/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted -/// configuration files. The configuration files can be placed in at .tui folder in the user's home -/// directory (e.g. C:/Users/username/.tui, or /usr/username/.tui), the folder where the Terminal.Gui -/// application was launched from (e.g. ./.tui ), or as a resource within the Terminal.Gui application's -/// main assembly. -/// -/// -/// Settings are defined in JSON format, according to this schema: -/// https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json -/// -/// -/// Settings that will apply to all applications (global settings) reside in files named config.json. -/// Settings that will apply to a specific Terminal.Gui application reside in files named -/// appname.config.json, where appname is the assembly name of the application (e.g. -/// UICatalog.config.json). -/// -/// -/// Settings are applied using the precedence defined in . -/// -/// -/// Configuration Management is based on static properties decorated with the -/// . Since these properties are static, changes to -/// configuration settings are applied process-wide. -/// -/// -/// Configuration Management is disabled by default and can be enabled by setting calling -/// . -/// -/// -/// See the UICatalog example for a complete example of how to use ConfigurationManager. -/// -/// -[Obsolete ("ConfigurationManager is being replaced by Microsoft.Extensions.Configuration. Use TuiConfigurationBuilder instead. Will be removed in a future version.")] -public static class ConfigurationManager -{ - private static readonly Lock _appNameLock = new (); - - /// The backing property for (config settings of ). - /// - /// Is until is called. Gets set to a new instance by - /// deserialization - /// (see ). - /// - private static SettingsScope? _settings; - -#pragma warning disable IDE1006 // Naming Styles - private static readonly ReaderWriterLockSlim _settingsLockSlim = new (); -#pragma warning restore IDE1006 // Naming Styles - - /// Name of the running application. By default, this property is set to the application's assembly name. - public static string AppName - { - get - { - lock (_appNameLock) - { - return field; - } - } - set - { - lock (_appNameLock) - { - field = value; - } - } - } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0].Trim ()!; - - #region AppSettings - - /// - /// Gets or sets the application-specific configuration settings (config properties with the - /// scope. - /// - [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] - [JsonPropertyName ("AppSettings")] - public static AppSettingsScope? AppSettings - { - [UnconditionalSuppressMessage ("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = - "UpdateToCurrentValues uses reflection only to read static property values from types preserved by ConfigPropertyHostTypes.")] - get - { - if (!IsInitialized ()) - { - // We're being called from the module initializer. - // Hard coded default value is an empty AppSettingsScope - var appSettings = new AppSettingsScope (); - appSettings.LoadHardCodedDefaults (); - - return appSettings; - } - - if (Settings is null || !Settings.TryGetValue ("AppSettings", out ConfigProperty? appSettingsConfigProperty)) - { - throw new InvalidOperationException ("Settings is null."); - } - - { - if (!appSettingsConfigProperty.HasValue) - { - var appSettings = new AppSettingsScope (); - appSettings.UpdateToCurrentValues (); - - return appSettings; - } - - return (appSettingsConfigProperty.PropertyValue as AppSettingsScope)!; - } - } - set - { - if (!IsInitialized ()) - { - throw new InvalidOperationException ("AppSettings cannot be set before ConfigurationManager is initialized."); - } - - // Check if the AppSettings is the same as the previous one - if (value != Settings! ["AppSettings"].PropertyValue) - { - // Update the backing store - Settings ["AppSettings"].PropertyValue = value; - - // Instance.OnThemeChanged (previousThemeValue); - } - } - } - - #endregion AppSettings - - /// Returns an empty Json document with just the $schema tag. - /// - public static string GetEmptyConfig () - { - var emptyScope = new SettingsScope (); - emptyScope.Clear (); - - return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext); - } - - /// Returns a Json document containing the hard-coded config. - /// - public static string GetHardCodedConfig () - { - var emptyScope = new SettingsScope (); - emptyScope.LoadHardCodedDefaults (); - IEnumerable>? settings = GetHardCodedConfigPropertiesByScope ("SettingsScope"); - - if (settings is null) - { - throw new InvalidOperationException ("GetHardCodedConfigPropertiesByScope returned null."); - } - - Dictionary settingsDict = settings.ToDictionary (); - - foreach (KeyValuePair p in Settings!.Where (cp => cp.Value.PropertyInfo is { })) - { - emptyScope [p.Key].PropertyValue = settingsDict [p.Key].PropertyValue; - } - - return JsonSerializer.Serialize (emptyScope, typeof (SettingsScope), SerializerContext); - } - - /// - /// The root object of Terminal.Gui configuration settings / JSON schema. - /// - public static SettingsScope? Settings - { - get - { - _settingsLockSlim.EnterReadLock (); - - try - { - return _settings; - } - finally - { - _settingsLockSlim.ExitReadLock (); - } - } - set - { - _settingsLockSlim.EnterWriteLock (); - - try - { - _settings = value; - } - finally - { - _settingsLockSlim.ExitWriteLock (); - } - } - } - - /// - /// INTERNAL: Retrieves all configuration properties that belong to a specific scope from the hard coded value cache. - /// The items in the collection are references to the original objects in the - /// cache. They contain the hard coded values and have set. - /// - internal static IEnumerable> GetHardCodedConfigPropertiesByScope (string scopeType) - { - // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection - // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer - // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext. - - // Filter properties by scope - IEnumerable>? cache = GetHardCodedConfigPropertyCache (); - - if (cache is null) - { - throw new InvalidOperationException ("GetHardCodedConfigPropertyCache returned null"); - } - - if (string.IsNullOrEmpty (scopeType)) - { - return cache; - } - - // Use the cached ScopeType property instead of reflection - IEnumerable> scopedCache = cache.Where (cp => cp.Value.ScopeType == scopeType); - - return scopedCache!; - } - - /// - /// INTERNAL: Retrieves all uninitialized configuration properties that belong to a specific scope from the cache. - /// The items in the collection are references to the original objects in the - /// cache. They do not have values and have set. - /// - internal static IEnumerable> GetUninitializedConfigPropertiesByScope (string scopeType) - { - // AOT Note: This method does NOT need the RequiresUnreferencedCode attribute as it is not using reflection - // and is not using any dynamic code. _allConfigProperties is a static property that is set in the module initializer - // and is not using any dynamic code. In addition, ScopeType are registered in SourceGenerationContext. - - if (_uninitializedConfigPropertiesCache is null) - { - throw new InvalidOperationException ("_allConfigPropertiesCache has not been set."); - } - - if (string.IsNullOrEmpty (scopeType)) - { - return _uninitializedConfigPropertiesCache; - } - - lock (_uninitializedConfigPropertiesCacheCacheLock) - { - // Filter properties by scope using the cached ScopeType property instead of reflection - IEnumerable>? filtered = _uninitializedConfigPropertiesCache.Where (cp => cp.Value.ScopeType == scopeType); - - Debug.Assert (filtered is { }); - - IEnumerable> configPropertiesByScope = - filtered as KeyValuePair [] ?? filtered.ToArray (); - Debug.Assert (configPropertiesByScope.All (v => !v.Value.HasValue)); - - return configPropertiesByScope; - } - } - - #region Initialization - - // ConfigurationManager is initialized when the module is loaded, via ModuleInitializers.InitializeConfigurationManager - // Once initialized, the ConfigurationManager is never un-initialized. - // The _initialized field is set to true when the module is loaded and the ConfigurationManager is initialized. - private static bool _initialized; -#pragma warning disable IDE1006 // Naming Styles - private static readonly Lock _initializedLock = new (); -#pragma warning restore IDE1006 // Naming Styles - - /// - /// INTERNAL: For Testing - Indicates whether the has been initialized. - /// - internal static bool IsInitialized () - { - lock (_initializedLock) - { - { - return _initialized; - } - } - } - - // TODO: Find a way to make this cache truly read-only at the leaf node level. - // TODO: Right now, the dictionary is frozen, but the ConfigProperty instances can still be modified - // TODO: if the PropertyValue is a reference type. - // TODO: See https://github.com/tui-cs/Terminal.Gui/issues/4288 - /// - /// A cache of all properties and their hard coded values. - /// - /// Is until is called. -#pragma warning disable IDE1006 // Naming Styles - internal static FrozenDictionary? _hardCodedConfigPropertyCache; - - private static readonly Lock _hardCodedConfigPropertyCacheLock = new (); -#pragma warning restore IDE1006 // Naming Styles - internal static FrozenDictionary GetHardCodedConfigPropertyCache () - { - lock (_hardCodedConfigPropertyCacheLock) - { - if (_hardCodedConfigPropertyCache is null) - { - throw new InvalidOperationException ("_hardCodedConfigPropertyCache has not been set."); - } - - return _hardCodedConfigPropertyCache; - } - } - - /// - /// An immutable cache of all s in module decorated with the - /// attribute. Both the dictionary and the contained - /// s - /// are immutable. - /// - /// Is until is called. - private static ImmutableSortedDictionary? _uninitializedConfigPropertiesCache; - -#pragma warning disable IDE1006 // Naming Styles - private static readonly object _uninitializedConfigPropertiesCacheCacheLock = new (); -#pragma warning restore IDE1006 // Naming Styles - - /// - /// INTERNAL: Initializes the . - /// This method is called when the module is loaded, - /// via . - /// For ConfigurationManager to access config resources, needs to be - /// set to after this method has been called. - /// - [UnconditionalSuppressMessage ("Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = - "All config property host types are statically rooted via ConfigPropertyHostTypes and registered in SourceGenerationContext. DeepCloner and Apply use source-generated serialization for known types.")] - [UnconditionalSuppressMessage ("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = "Reflection-heavy paths are guarded by RuntimeFeature.IsDynamicCodeSupported and are dead code under AOT.")] - internal static void Initialize () - { - lock (_initializedLock) - { - if (_initialized) - { - throw new InvalidOperationException ("ConfigurationManager is already initialized."); - } - } - - // Ensure ConfigProperty has cached the list of all the classes with config properties. - ConfigProperty.Initialize (); - - // Cache all configuration properties - lock (_uninitializedConfigPropertiesCacheCacheLock) - { - // _allConfigProperties: for ordered, iterable access (LINQ-friendly) - // _hardCodedConfigPropertyCache: for high-speed key lookup (frozen) - - // Note GetAllConfigProperties returns a new instance and all the properties !HasValue and Immutable. - _uninitializedConfigPropertiesCache = ConfigProperty.GetAllConfigProperties (); - } - - // Create a COPY of the _allConfigPropertiesCache to ensure that the original is not modified. - lock (_hardCodedConfigPropertyCacheLock) - { - _hardCodedConfigPropertyCache = ConfigProperty.GetAllConfigProperties ().ToFrozenDictionary (); - - foreach (KeyValuePair hardCodedProperty in _hardCodedConfigPropertyCache) - { - hardCodedProperty.Value.Immutable = false; - hardCodedProperty.Value.UpdateToCurrentValue (); - } - - foreach (KeyValuePair hardCodedProperty in _hardCodedConfigPropertyCache) - { - // Deep-clone after every cache entry has been populated from its current static value. - // Some getters (notably ThemeManager.Themes while uninitialized) synthesize composite - // objects from other configuration properties, so a single populate-and-clone pass can - // observe half-initialized cache entries and persist null/default values into the clone. - hardCodedProperty.Value.PropertyValue = DeepCloner.DeepClone (hardCodedProperty.Value.PropertyValue); - hardCodedProperty.Value.Immutable = true; - } - } - - lock (_initializedLock) - { - _initialized = true; - } - - LoadHardCodedDefaults (); - - // BUGBUG: ThemeScope is broken and needs to be fixed to not have the hard coded schemes get overwritten. - // BUGBUG: This a partial workaround. - // BUGBUG: See https://github.com/tui-cs/Terminal.Gui/issues/4288 - ThemeManager.Themes? [ThemeManager.Theme].Apply (); - } - - #endregion Initialization - - #region Enable/Disable - - private static bool _enabled; -#pragma warning disable IDE1006 // Naming Styles - private static readonly object _enabledLock = new (); -#pragma warning restore IDE1006 // Naming Styles - - /// - /// Gets whether is enabled or not. - /// If , only the hard coded defaults will be loaded. See and - /// - /// - public static bool IsEnabled - { - get - { - lock (_enabledLock) - { - return _enabled; - } - } - } - - /// - /// Enables . If is , - /// ConfigurationManager will be enabled as-is; no configuration will be loaded or applied. If - /// is , - /// ConfigurationManager will be enabled and reset to hard-coded defaults. - /// For any other value, - /// ConfigurationManager will be enabled and the configuration will be loaded from the specified locations and applied. - /// - /// - public static void Enable (ConfigLocations locations) - { - if (IsEnabled) - { - return; - } - - lock (_enabledLock) - { - _enabled = true; - } - - ClearJsonErrors (); - - if (locations == ConfigLocations.None) - { - return; - } - - Load (locations); - - // Works even if ConfigurationManager is not enabled. - InternalApply (); - } - - /// - /// Disables . - /// - /// - /// If all static properties will be reset to their - /// initial, hard-coded - /// defaults. - /// - public static void Disable (bool resetToHardCodedDefaults = false) - { - lock (_enabledLock) - { - _enabled = false; - } - - if (resetToHardCodedDefaults) - { - // Calls Apply - ResetToHardCodedDefaults (); - } - } - - #endregion Enable/Disable - - #region Reset - - // `Update` - Updates the configuration from either the current values or the hard-coded defaults. - // Updating does not load the configuration; it only updates the configuration to the values currently - // in the static ConfigProperties. - - /// - /// INTERNAL: Updates to the settings from the current - /// values of the static properties. - /// - internal static void UpdateToCurrentValues () - { - if (!IsInitialized ()) - { - throw new InvalidOperationException ("Initialize must be called first."); - } - - _settingsLockSlim.EnterWriteLock (); - - try - { - _settings = new SettingsScope (); - _settings.LoadHardCodedDefaults (); - } - finally - { - _settingsLockSlim.ExitWriteLock (); - } - - Settings!.UpdateToCurrentValues (); - ThemeManager.UpdateToCurrentValues (); - AppSettings!.UpdateToCurrentValues (); - } - - /// - /// INTERNAL: Loads the hard-coded values of the - /// properties and applies them. - /// - internal static void ResetToHardCodedDefaults () - { - LoadHardCodedDefaults (); - Applied = null; - Updated = null; - - // Works even if ConfigurationManager is not enabled. - InternalApply (); - } - - #endregion Reset - - #region Load - - // `Load` - Load configuration from the given location(s), updating the configuration with any new values. - // Loading does not apply the settings to the application; that happens when the `Apply` method is called. - - /// - /// INTERNAL: Loads all hard-coded configuration properties. Use to cause the loaded settings to be - /// applied to the running application. - /// - /// WARNING: This method clears and all existing . - /// Only call this when you want to completely reset configuration to hard-coded defaults. - /// - /// - internal static void LoadHardCodedDefaults () - { - if (!IsInitialized ()) - { - throw new InvalidOperationException ("Initialize must be called first."); - } - - RuntimeConfig = null; - SourcesManager!.Sources.Clear (); - SourcesManager.AddSource (ConfigLocations.HardCoded, "HardCoded"); - - Settings = new SettingsScope (); - Settings.LoadHardCodedDefaults (); - ThemeManager.LoadHardCodedDefaults (); - AppSettings!.LoadHardCodedDefaults (); - } - - /// - /// Loads all settings found in . Use to cause the loaded settings to - /// be applied to the running application. - /// - /// Configuration manager is not enabled. - public static void Load (ConfigLocations locations) - { - if (!IsEnabled) - { - throw new ConfigurationManagerNotEnabledException (); - } - - // Only load the hard-coded defaults if the user specified HardCoded exclusively. - // LoadHardCodedDefaults clears RuntimeConfig and resets everything, so we only - // want to do this when HardCoded is the only location requested. - if (locations == ConfigLocations.HardCoded) - { - LoadHardCodedDefaults (); - - return; - } - - // Use SourcesManager to load from all locations in the correct order - // Note: SourcesManager.LoadFromLocations skips HardCoded because those are - // already loaded during Enable(). If HardCoded is included in locations with - // other flags, the hard-coded defaults are already in Settings. - SourcesManager?.LoadFromLocations (Settings, locations); - } - - // TODO: Rename to Loaded? - /// - /// Called when the configuration has been updated from a configuration file or reset. Invokes the - /// - /// event. - /// - public static void OnUpdated () - { - // Logging.Trace (@""); - - if (!IsEnabled) - { - return; - } - - // Use a local copy of the event delegate when invoking it to avoid race conditions. - EventHandler? handler = Updated; - handler?.Invoke (null, new ConfigurationManagerEventArgs ()); - } - - /// Event fired when the configuration has been updated from a configuration source or reset. - public static event EventHandler? Updated; - - #endregion Load - - #region Apply - - // `Apply` - Apply the configuration to the application; this means the settings are copied from the - // configuration properties to the corresponding `static` `[ConfigurationProperty]` properties. - - /// - /// Applies the configuration settings to static properties. - /// ConfigurationManager must be Enabled. - /// - /// Configuration Manager is not enabled. - public static void Apply () - { - if (!IsEnabled) - { - throw new ConfigurationManagerNotEnabledException (); - } - - InternalApply (); - } - - private static void InternalApply () - { - Trace.Configuration ("ConfigurationManager", "Apply", "Start"); - - var settings = false; - var themes = false; - var appSettings = false; - - try - { - settings = Settings?.Apply () ?? false; - themes = ThemeManager.Themes? [ThemeManager.Theme].Apply () ?? false; - appSettings = AppSettings?.Apply () ?? false; - } - catch (JsonException e) - { - if (ThrowOnJsonErrors ?? false) - { - throw; - } - else - { - AddJsonError ($"Error applying Configuration Change: {e.Message}"); - } - } - finally - { - Trace.Configuration ("ConfigurationManager", "Apply", $"End - settings={settings}, themes={themes}, appSettings={appSettings}"); - - if (settings || themes || appSettings) - { - OnApplied (); - } - } - } - - /// - /// Called when an updated configuration has been applied to the application. Fires the - /// event. - /// - /// Configuration manager is not enabled. - private static void OnApplied () - { - if (!IsEnabled) - { - return; - } - - // Use a local copy of the event delegate when invoking it to avoid race conditions. - EventHandler? handler = Applied; - handler?.Invoke (null, new ConfigurationManagerEventArgs ()); - - // TODO: Refactor ConfigurationManager to not use an event handler for this. - // Instead, have it call a method on any class appropriately attributed - // to update the cached values. See Issue #2871 - } - - /// Event fired when an updated configuration has been applied to the application. - public static event EventHandler? Applied; - - #endregion Apply - - #region Sources - - // `Sources` - A source is a location where a configuration can be stored. Sources are defined in the `ConfigLocations` enum. - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - internal static readonly SourceGenerationContext SerializerContext = TuiSerializerContext.Instance; - - private static SourcesManager? _sourcesManager = new (); - private static readonly object _sourcesManagerLock = new (); - - /// - /// Gets the Sources Manager - manages the loading of configuration sources from files and resources. - /// - public static SourcesManager? SourcesManager - { - get - { - lock (_sourcesManagerLock) - { - return _sourcesManager; - } - } - internal set - { - lock (_sourcesManagerLock) - { - _sourcesManager = value; - } - } - } - - private static string? _runtimeConfig = """{ }"""; - private static readonly object _runtimeConfigLock = new (); - - /// - /// Gets or sets the in-memory config.json. See . - /// - public static string? RuntimeConfig - { - get - { - lock (_runtimeConfigLock) - { - return _runtimeConfig; - } - } - set - { - lock (_runtimeConfigLock) - { - _runtimeConfig = value; - } - } - } - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - internal static readonly string ConfigFilename = "config.json"; - - #endregion Sources - - #region Error Logging - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - internal static StringBuilder _jsonErrors = new (); - - private static bool? _throwOnJsonErrors = false; - private static readonly object _throwOnJsonErrorsLock = new (); - - /// - /// Gets or sets whether the should throw an exception if it encounters an - /// error on deserialization. If (the default), the error is logged and printed to the console - /// when is called. - /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool? ThrowOnJsonErrors - { - get - { - lock (_throwOnJsonErrorsLock) - { - return _throwOnJsonErrors; - } - } - set - { - lock (_throwOnJsonErrorsLock) - { - _throwOnJsonErrors = value; - } - } - } - -#pragma warning disable IDE1006 // Naming Styles - private static readonly object _jsonErrorsLock = new (); -#pragma warning restore IDE1006 // Naming Styles - - internal static void AddJsonError (string error) - { - Logging.Error ($"{error}"); - - lock (_jsonErrorsLock) - { - _jsonErrors.AppendLine (@$" {error}"); - } - } - - private static void ClearJsonErrors () - { - lock (_jsonErrorsLock) - { - _jsonErrors.Clear (); - } - } - - /// Prints any Json deserialization errors that occurred during deserialization to the console. - public static void PrintJsonErrors () - { - lock (_jsonErrorsLock) - { - if (_jsonErrors.Length > 0) - { - Console.WriteLine (@"Terminal.Gui ConfigurationManager encountered these errors while reading configuration files" - + @"(set ThrowOnJsonErrors to have these caught during execution):"); - Console.WriteLine (_jsonErrors.ToString ()); - } - } - } - - #endregion Error Logging -} diff --git a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs b/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs deleted file mode 100644 index ea03daff18..0000000000 --- a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Terminal.Gui.Configuration; - -/// Event arguments for the events. -public class ConfigurationManagerEventArgs : EventArgs -{ - /// Initializes a new instance of - public ConfigurationManagerEventArgs () { } -} - - -//public class ConfigurationLoadEventArgs : ResultEventArgs -//{ -// public ConfigLocations Location { get; } -// public string? Path { get; } - -// public ConfigurationLoadEventArgs (ConfigLocations location, string? path) -// { -// Location = location; -// Path = path; -// } -//} diff --git a/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs b/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs deleted file mode 100644 index f9fc6b62ee..0000000000 --- a/Terminal.Gui/Configuration/ConfigurationManagerNotEnabledException.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Terminal.Gui.Configuration; - -/// -/// The exception that is thrown when a API is called but the configuration manager is not enabled. -/// -public class ConfigurationManagerNotEnabledException : Exception -{ } diff --git a/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs b/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs deleted file mode 100644 index 334e4b1dc8..0000000000 --- a/Terminal.Gui/Configuration/ConfigurationPropertyAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Terminal.Gui.Configuration; - -/// An attribute indicating a property is managed by . -/// -/// [ConfigurationProperty(Scope = typeof(AppSettingsScope))] public static string? MyProperty { get; set; } = "MyValue"; -/// -[AttributeUsage (AttributeTargets.Property)] -[Obsolete ("Use Settings POCOs with TuiConfigurationBuilder instead. Will be removed in a future version.")] -public class ConfigurationPropertyAttribute : System.Attribute -{ - /// - /// If , the property will be serialized to the configuration file using only the property - /// name as the key. If , the property will be serialized to the configuration file using the - /// property name pre-pended with the classname (e.g. Application.UseSystemConsole). - /// - public bool OmitClassName { get; set; } - - private Type? _scope; - - /// Specifies the scope of the property. If then will be used. - public Type? Scope - { - get - { - if (_scope is { }) - { - return _scope; - } - return typeof (AppSettingsScope); - } - set - { - if (value == typeof (AppSettingsScope) && OmitClassName) - { - throw new ArgumentException ("OmitClassName is not allowed when Scope is AppSettingsScope to ensure property names are globally unique."); - } - _scope = value; - } - } -} diff --git a/Terminal.Gui/Configuration/DeepCloner.cs b/Terminal.Gui/Configuration/DeepCloner.cs deleted file mode 100644 index 2406c78dd0..0000000000 --- a/Terminal.Gui/Configuration/DeepCloner.cs +++ /dev/null @@ -1,499 +0,0 @@ -using System.Collections; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Terminal.Gui.Configuration; - -/// -/// Provides deep cloning functionality for Terminal.Gui configuration objects. -/// Creates a deep copy of an object by recursively cloning public properties, -/// handling collections, arrays, dictionaries, and circular references. -/// -/// -/// This class does not use because it does not guarantee deep cloning, -/// may not handle circular references, and is not widely implemented in modern .NET types. -/// Instead, it uses reflection to ensure consistent deep cloning behavior across all types. -/// -/// Limitations: -/// - Types without a parameterless constructor (and not handled as simple types, arrays, dictionaries, or -/// collections) may be instantiated using uninitialized objects, which could lead to runtime errors if not -/// properly handled. -/// - Immutable collections (e.g., ) are -/// not supported and will throw a . -/// - Only public, writable properties are cloned; private fields, read-only properties, and non-public members are -/// ignored. -/// -/// -public static class DeepCloner -{ - /// - /// Creates a deep copy of the specified object. - /// - /// The type of the object to clone. - /// The object to clone. - /// A deep copy of the source object, or default if source is null. - public static T? DeepClone (T? source) - { - if (source is null) - { - return default (T?); - } - ConcurrentDictionary visited = new (ReferenceEqualityComparer.Instance); - - return (T?)DeepCloneInternal (source, visited); - } - - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "DeepCloner reflects over writable properties of runtime configuration/test types to preserve object graphs, circular references, and non-public setters.")] - private static object? DeepCloneInternal (object? source, ConcurrentDictionary visited) - { - if (source is null) - { - return null; - } - - if (source is Drawing.Attribute attribute) - { - return attribute; - } - - if (source is Drawing.Scheme scheme) - { - return new Drawing.Scheme (scheme); - } - - if (source is ThemeScope themeScope) - { - return CloneScope (themeScope, visited); - } - - if (source is AppSettingsScope appSettingsScope) - { - return CloneScope (appSettingsScope, visited); - } - - if (source is SettingsScope settingsScope) - { - return CloneScope (settingsScope, visited); - } - - // Handle already visited objects to avoid circular references - if (visited.TryGetValue (source, out object? existingClone)) - { - return existingClone; - } - - Type type = source.GetType (); - - // Handle immutable or simple types - if (IsSimpleType (type)) - { - return source; - } - - // Handle arrays - if (type.IsArray) - { - return CloneArray (source, visited); - } - - // Handle dictionaries - if (source is IDictionary) - { - return CloneDictionary (source, visited); - } - - // Handle collections - if (typeof (ICollection).IsAssignableFrom (type)) - { - return CloneCollection (source, visited); - } - - // Create new instance - object clone = CreateInstance (type); - - // Add to visited before cloning properties - visited.TryAdd (source, clone); - - // Clone writable public and internal properties - foreach (PropertyInfo prop in type.GetProperties (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where (p => !p.IsSpecialName) // exclude private backing fields or compiler-generated props - .Where (p => p is { CanRead: true, CanWrite: true } && p.GetIndexParameters ().Length == 0)) - { - object? value = prop.GetValue (source); - object? clonedValue = DeepCloneInternal (value, visited); - prop.SetValue (clone, clonedValue); - } - - return clone; - } - - private static bool IsSimpleType (Type type) - { - if (type.IsPrimitive - || type.IsEnum - || type.IsValueType - || type == typeof (decimal) - || type == typeof (DateTime) - || type == typeof (DateTimeOffset) - || type == typeof (TimeSpan) - || type == typeof (Guid) - || type == typeof (Rune) - || type == typeof (string)) - { - return true; - } - - // Treat Type and PropertyInfo as simple metadata objects that should not be cloned. - if (typeof (Type).IsAssignableFrom (type) || typeof (PropertyInfo).IsAssignableFrom (type)) - { - return true; - } - - return false; - } - - [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "DeepCloner creates only runtime configuration/test types with parameterless constructors, and this path is covered by unit tests and NativeAOT validation.")] - [UnconditionalSuppressMessage ("Trimming", "IL2070", Justification = "DeepCloner probes for a public parameterless constructor on runtime configuration/test types before instantiating them.")] - private static object CreateInstance (Type type) - { - try - { - // Try parameterless constructor - if (type.GetConstructor (Type.EmptyTypes) != null) - { - return Activator.CreateInstance (type)!; - } - - // In AOT, try using the JsonSerializer if available - if (IsAotEnvironment ()) - { - JsonTypeInfo? jsonTypeInfo = TuiSerializerContext.Instance.GetTypeInfo (type); - - if (jsonTypeInfo is not null) - { - return JsonSerializer.Deserialize ("{}", jsonTypeInfo)!; - } - } - - throw new InvalidOperationException ($"Cannot create instance of type {type.FullName}. No parameterless constructor or clone method found."); - } - catch (MissingMethodException) - { - throw new InvalidOperationException ( - $"Cannot create instance of type {type.FullName} in AOT context. Consider adding this type to your SourceGenerationContext."); - } - } - - private static object CloneArray (object source, ConcurrentDictionary visited) - { - Array array = (Array)source; - Array newArray = (Array)array.Clone (); - visited.TryAdd (source, newArray); - - for (var i = 0; i < array.Length; i++) - { - object? value = array.GetValue (i); - object? clonedValue = DeepCloneInternal (value, visited); - newArray.SetValue (clonedValue, i); - } - - return newArray; - } - - [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Collection cloning only instantiates the runtime collection type after filtering to supported IList implementations.")] - private static object CloneCollection (object source, ConcurrentDictionary visited) - { - Type type = source.GetType (); - - // Check for immutable collections and throw if found - if (type.IsGenericType) - { - Type genericTypeDef = type.GetGenericTypeDefinition (); - - if (genericTypeDef.FullName != null && genericTypeDef.FullName.StartsWith ("System.Collections.Immutable")) - { - throw new NotSupportedException ($"Cloning of immutable collections like {type.Name} is not supported."); - } - } - - if (source is not IList) - { - throw new NotSupportedException ($"Cloning of collection type {type.Name} is not supported unless it implements IList."); - } - - if (Activator.CreateInstance (type) is not IList tempList) - { - throw new NotSupportedException ($"Cloning of collection type {type.Name} is not supported without a parameterless constructor."); - } - - // Add to visited before cloning contents to prevent circular reference issues - visited.TryAdd (source, tempList); - - foreach (object? item in (IEnumerable)source) - { - object? clonedItem = DeepCloneInternal (item, visited); - tempList.Add (clonedItem); - } - - return tempList; - } - - #region Dictionary Support - - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Dictionary cloning constructs supported runtime dictionary shapes (Dictionary<,> and ConcurrentDictionary<,>) via MakeGenericType, which is validated by NativeAOT publish tests.")] - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Dictionary cloning reads the runtime dictionary comparer from supported dictionary types to preserve comparer semantics.")] - private static object CloneDictionary (object source, ConcurrentDictionary visited) - { - if (source is ConcurrentDictionary themeSource) - { - ConcurrentDictionary clonedThemes = new (StringComparer.InvariantCultureIgnoreCase); - visited.TryAdd (source, clonedThemes); - - foreach (KeyValuePair kvp in themeSource) - { - object? clonedThemeObject = DeepCloneInternal (kvp.Value, visited); - - if (clonedThemeObject is not ThemeScope clonedTheme) - { - throw new InvalidOperationException ( - $"Expected cloned theme scope to be {typeof (ThemeScope).FullName}, but got {clonedThemeObject?.GetType ().FullName ?? ""}."); - } - - clonedThemes [kvp.Key] = clonedTheme; - } - - return clonedThemes; - } - - if (source is Dictionary schemeSource) - { - Dictionary clonedSchemes = new (schemeSource.Comparer); - visited.TryAdd (source, clonedSchemes); - - foreach (KeyValuePair kvp in schemeSource) - { - clonedSchemes [kvp.Key] = (Scheme?)DeepCloneInternal (kvp.Value, visited); - } - - return clonedSchemes; - } - - IDictionary sourceDict = (IDictionary)source; - Type type = source.GetType (); - - // Check for frozen or immutable dictionaries and throw if found - if (type.IsGenericType) - { - CheckForUnsupportedDictionaryTypes (type); - } - - Type [] genericArgs = type.GetGenericArguments (); - Type dictType; - - if (genericArgs.Length == 2) - { - if (type.GetGenericTypeDefinition () == typeof (Dictionary<,>)) - { - dictType = typeof (Dictionary<,>).MakeGenericType (genericArgs); - } - else if (type.GetGenericTypeDefinition () == typeof (ConcurrentDictionary<,>)) - { - dictType = typeof (ConcurrentDictionary<,>).MakeGenericType (genericArgs); - } - else - { - throw new InvalidOperationException ( - $"Unsupported dictionary type: {type}. Only Dictionary<,> and ConcurrentDictionary<,> are supported."); - } - } - else - { - dictType = typeof (Dictionary); - } - - object? comparer = type.GetProperty ("Comparer")?.GetValue (source); - - IDictionary tempDict = CreateDictionaryInstance (dictType, comparer); - visited.TryAdd (source, tempDict); - - object? lastKey = null; - - try - { - // Clone all key-value pairs - foreach (object? key in sourceDict.Keys) - { - lastKey = key; - object? clonedKey = DeepCloneInternal (key, visited); - object? clonedValue = DeepCloneInternal (sourceDict [key], visited); - - if (tempDict is ConcurrentDictionary themeDict) - { - themeDict [(string)clonedKey!] = (ThemeScope)clonedValue!; - - continue; - } - - if (tempDict is Dictionary schemeDict) - { - schemeDict [(string)clonedKey!] = (Scheme?)clonedValue; - - continue; - } - - if (tempDict.Contains (clonedKey!)) - { - tempDict [clonedKey!] = clonedValue; - } - else - { - tempDict.Add (clonedKey!, clonedValue); - } - } - } - catch (InvalidOperationException ex) - { - // Handle cases where the dictionary is modified during enumeration - throw new InvalidOperationException ( - $"Error cloning dictionary ({source}) (last key was \"{lastKey}\"). Ensure the source dictionary is not modified during cloning.", - ex); - } - - return tempDict; - } - - [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Dictionary cloning only instantiates supported dictionary runtime types and falls back safely when comparer constructors are unavailable.")] - private static IDictionary CreateDictionaryInstance (Type dictType, object? comparer) - { - // Typed paths for dictionary types that require custom comparers. - if (dictType == typeof (ConcurrentDictionary)) - { - if (comparer is IEqualityComparer stringComparer) - { - return new ConcurrentDictionary (stringComparer); - } - - return new ConcurrentDictionary (); - } - - if (dictType == typeof (Dictionary)) - { - if (comparer is IEqualityComparer stringComparer) - { - return new Dictionary (stringComparer); - } - - return new Dictionary (); - } - - if (dictType == typeof (Dictionary)) - { - return new Dictionary (); - } - - if (dictType == typeof (Dictionary>)) - { - if (comparer is IEqualityComparer stringComparer) - { - return new Dictionary> (stringComparer); - } - - return new Dictionary> (); - } - - // AOT-safe: use the source-generated JSON serializer to create empty dictionary instances. - // This avoids Activator.CreateInstance, whose target constructors are trimmed by the AOT linker - // for closed generic dictionary types not otherwise statically reachable. - // Always attempted regardless of comparer — in AOT, a default-comparer dictionary is preferable - // to a crash from a trimmed constructor. - try - { - JsonTypeInfo? jsonTypeInfo = TuiSerializerContext.Instance.GetTypeInfo (dictType); - - if (jsonTypeInfo is not null) - { - IDictionary? result = JsonSerializer.Deserialize ("{}", jsonTypeInfo) as IDictionary; - - if (result is not null) - { - return result; - } - } - } - catch (InvalidOperationException) - { - // JSON serializer context may not be initialized — fall through to reflective construction. - } - - try - { - // Try to create the dictionary with the comparer - return comparer != null - ? (IDictionary)Activator.CreateInstance (dictType, comparer)! - : (IDictionary)Activator.CreateInstance (dictType)!; - } - catch (MissingMethodException) - { - // Fallback to parameterless constructor if comparer constructor is not available - return (IDictionary)Activator.CreateInstance (dictType)!; - } - } - - private static void CheckForUnsupportedDictionaryTypes (Type type) - { - Type? currentType = type; - - while (currentType != null && currentType != typeof (object)) - { - if (currentType.IsGenericType) - { - string? genericTypeName = currentType.GetGenericTypeDefinition ().FullName; - - if (genericTypeName != null - && (genericTypeName.StartsWith ("System.Collections.Frozen") || genericTypeName.StartsWith ("System.Collections.Immutable"))) - { - throw new NotSupportedException ($"Cloning of frozen or immutable dictionaries like {type.Name} is not supported."); - } - } - - currentType = currentType.BaseType; - } - } - - #endregion Dictionary Support - - #region AOT Support - - private static TScopeT CloneScope (TScopeT scope, ConcurrentDictionary visited) - where TScopeT : Scope, new() - { - TScopeT clonedScope = new (); - visited.TryAdd (scope, clonedScope); - - foreach (KeyValuePair kvp in scope) - { - ConfigProperty clonedProperty = ConfigProperty.CreateCopy (kvp.Value); - clonedProperty.Immutable = kvp.Value.Immutable; - - if (kvp.Value.HasValue) - { - clonedProperty.PropertyValue = DeepCloneInternal (kvp.Value.PropertyValue, visited); - } - - clonedScope.TryAdd (kvp.Key, clonedProperty); - } - - return clonedScope; - } - - private static bool IsAotEnvironment () => - - // Check if running in an AOT environment - Type.GetType ("System.Runtime.CompilerServices.RuntimeFeature")?.GetProperty ("IsDynamicCodeSupported")?.GetValue (null) is bool and false; - - #endregion AOT Support -} diff --git a/Terminal.Gui/Configuration/RuneJsonConverter.cs b/Terminal.Gui/Configuration/RuneJsonConverter.cs index 7c9bd989de..718790e1ae 100644 --- a/Terminal.Gui/Configuration/RuneJsonConverter.cs +++ b/Terminal.Gui/Configuration/RuneJsonConverter.cs @@ -20,104 +20,37 @@ namespace Terminal.Gui.Configuration; /// internal class RuneJsonConverter : JsonConverter { - public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + /// + /// Parses a configuration string using the same rules as for a JSON string token. + /// Does not round-trip through (that path is trim-unsafe on NativeAOT). + /// + internal static bool TryParse (string? value, out Rune result) { - switch (reader.TokenType) - { - case JsonTokenType.String: - { - string? value = reader.GetString (); - int first = RuneExtensions.MaxUnicodeCodePoint + 1; - int second = RuneExtensions.MaxUnicodeCodePoint + 1; - - if (value is { } && (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) - || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase))) - { - // Handle encoded single char, surrogate pair, or combining mark + char - uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})") - .Select ( - match => uint.Parse ( - match.Groups [1].Value, - NumberStyles.HexNumber - ) - ) - .ToArray (); - - if (codePoints.Length is 0 or > 2) - { - throw new JsonException ($"{value}: Invalid Rune."); - } - - if (codePoints.Length > 0) - { - first = (int)codePoints [0]; - } - - if (codePoints.Length == 2) - { - second = (int)codePoints [1]; - } - } - else - { - // Handle single character, surrogate pair, or combining mark + char - if (value is { Length: 0 or > 2 }) - { - throw new JsonException ($"{value}: Invalid Rune"); - } - - if (value is { Length: > 0 }) - { - first = value [0]; - } - - if (value is { Length: 2 }) - { - second = value [1]; - } - } - - Rune result; - - if (second == RuneExtensions.MaxUnicodeCodePoint + 1) - { - // Single codepoint - if (!Rune.TryCreate (first, out result)) - { - throw new JsonException ($"{value}: Invalid Rune"); - } - - return result; - } - - // Surrogate pair? - if (Rune.TryCreate ((char)first, (char)second, out result)) - { - return result; - } - - if (!Rune.IsValid (second)) - { - throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not valid: {second}."); - } - - var cm = new Rune (second); + result = default; - if (!cm.IsCombiningMark ()) - { - throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not a combining mark: {cm}."); - } + if (value is null) + { + return false; + } - // not a surrogate pair, so a combining mark + char? - string combined = string.Concat ((char)first, (char)second).Normalize (); + try + { + result = ParseString (value); - if (!Rune.IsValid (combined [0])) - { - throw new JsonException ($"{value}: Invalid combined Rune."); - } + return true; + } + catch (JsonException) + { + return false; + } + } - return new (combined [0]); - } + public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return ParseString (reader.GetString ()); case JsonTokenType.Number: { uint num = reader.GetUInt32 (); @@ -150,5 +83,102 @@ public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOpt writer.WriteStringValue (value.ToString ()); } } + + /// + /// Parses a glyph, U+hex, or \u string using the same rules as a JSON string token in . + /// + private static Rune ParseString (string? value) + { + int first = RuneExtensions.MaxUnicodeCodePoint + 1; + int second = RuneExtensions.MaxUnicodeCodePoint + 1; + + if (value is { } && (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) + || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase))) + { + // Handle encoded single char, surrogate pair, or combining mark + char + uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})") + .Select ( + match => uint.Parse ( + match.Groups [1].Value, + NumberStyles.HexNumber + ) + ) + .ToArray (); + + if (codePoints.Length is 0 or > 2) + { + throw new JsonException ($"{value}: Invalid Rune."); + } + + if (codePoints.Length > 0) + { + first = (int)codePoints [0]; + } + + if (codePoints.Length == 2) + { + second = (int)codePoints [1]; + } + } + else + { + // Handle single character, surrogate pair, or combining mark + char + if (value is { Length: 0 or > 2 }) + { + throw new JsonException ($"{value}: Invalid Rune"); + } + + if (value is { Length: > 0 }) + { + first = value [0]; + } + + if (value is { Length: 2 }) + { + second = value [1]; + } + } + + Rune result; + + if (second == RuneExtensions.MaxUnicodeCodePoint + 1) + { + // Single codepoint + if (!Rune.TryCreate (first, out result)) + { + throw new JsonException ($"{value}: Invalid Rune"); + } + + return result; + } + + // Surrogate pair? + if (Rune.TryCreate ((char)first, (char)second, out result)) + { + return result; + } + + if (!Rune.IsValid (second)) + { + throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not valid: {second}."); + } + + var cm = new Rune (second); + + if (!cm.IsCombiningMark ()) + { + throw new JsonException ($"{value}: Invalid Rune. The second codepoint is not a combining mark: {cm}."); + } + + // not a surrogate pair, so a combining mark + char? + string combined = string.Concat ((char)first, (char)second).Normalize (); + + if (!Rune.IsValid (combined [0])) + { + throw new JsonException ($"{value}: Invalid combined Rune."); + } + + return new (combined [0]); + } } #pragma warning restore 1591 diff --git a/Terminal.Gui/Configuration/SchemeManager.cs b/Terminal.Gui/Configuration/SchemeManager.cs index 7d2e842d4e..fdc06ed7df 100644 --- a/Terminal.Gui/Configuration/SchemeManager.cs +++ b/Terminal.Gui/Configuration/SchemeManager.cs @@ -1,98 +1,71 @@ -using System.Collections.Immutable; -using System.Diagnostics; +// Grok - grok-4.6 +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -#pragma warning disable CS0618 // Obsolete - SchemeManager still uses ConfigurationManager/ConfigProperty/ThemeScope internally during transition +using System.Text.Json.Nodes; namespace Terminal.Gui.Configuration; /// -/// Holds the s that define the s that are used by views to -/// render -/// themselves. A Scheme is a mapping from s (such as -/// ) to s. -/// A Scheme defines how a `View` should look based on its purpose (e.g. Menu or Dialog). +/// Holds the s that define the s views use to render. +/// A Scheme maps s to s. /// -public sealed class SchemeManager // : INotifyCollectionChanged, IDictionary +public sealed class SchemeManager : ISchemeManager { -#pragma warning disable IDE1006 // Naming Styles private static readonly Lock _schemesLock = new (); -#pragma warning restore IDE1006 // Naming Styles + private static Dictionary _schemes = HardCodedDictionary (); - /// - /// INTERNAL: Gets the hard-coded schemes defined by . These are not loaded from the configuration - /// files, - /// but are hard-coded in the source code. Used for unit testing when ConfigurationManager is not initialized. - /// - /// - internal static ImmutableSortedDictionary GetHardCodedSchemes () => Scheme.GetHardCodedSchemes ()!; + /// INTERNAL: Hard-coded schemes used before configuration is applied and as the overlay base. + internal static ImmutableSortedDictionary GetHardCodedSchemes () + { + ImmutableSortedDictionary hardCoded = Scheme.GetHardCodedSchemes ()!; - /// - /// Use , , , - /// , etc... instead. - /// - [ConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)] - [JsonConverter (typeof (DictionaryJsonConverter))] - [UsedImplicitly] - public static Dictionary? Schemes + return hardCoded.ToImmutableSortedDictionary ( + kv => kv.Key, + kv => (Scheme?)kv.Value, + StringComparer.InvariantCultureIgnoreCase); + } + + private static Dictionary HardCodedDictionary () => + GetHardCodedSchemes ().ToDictionary (StringComparer.InvariantCultureIgnoreCase); + + /// Gets the dictionary of defined schemes for the current theme. + public static Dictionary Schemes { get => GetSchemes (); - private set => SetSchemes (value); + private set => ReplaceSchemes (value); } - /// INTERNAL: Gets the dictionary of defined s. The get method for . internal static Dictionary GetSchemes () { - if (!ConfigurationManager.IsInitialized ()) + lock (_schemesLock) { - // We're being called from the module initializer. - // Hard coded default value - return GetHardCodedSchemes ().ToDictionary (StringComparer.InvariantCultureIgnoreCase); + return _schemes; } - - return GetSchemesForCurrentTheme (); } - /// INTERNAL: The set method for . - internal static void SetSchemes (Dictionary? value) + /// Replaces the current theme's scheme dictionary (called from configuration apply). + internal static void ReplaceSchemes (Dictionary schemes) { lock (_schemesLock) { - if (!ConfigurationManager.IsInitialized ()) - { - throw new InvalidOperationException ("Schemes cannot be set before ConfigurationManager is initialized."); - } - - Debug.Assert (value is { }); - - // Update the backing store - ThemeManager.GetCurrentTheme () ["Schemes"].UpdateFrom (value); + _schemes = new (schemes, StringComparer.InvariantCultureIgnoreCase); } - - //Instance.OnThemeChanged (previousValue); } /// - /// Adds a new to . If the Scheme has already been added, - /// it will be updated to . + /// Adds a new . If the name already exists, it is updated. + /// Schemes added or updated here survive theme switches and configuration re-application. /// - /// The name of the Scheme. This must be unique. - /// - /// public static void AddScheme (string schemeName, Scheme scheme) { - if (!GetSchemes ().TryAdd (schemeName, scheme)) + lock (_schemesLock) { - GetSchemes () [schemeName] = scheme; + _schemes [schemeName] = scheme; + _runtimeSchemes [schemeName] = scheme; } } - /// - /// Removes a Scheme from . - /// - /// - /// If the scheme is a built-in Scheme or was not previously added. + /// Removes a previously added (non-built-in) scheme. public static void RemoveScheme (string schemeName) { if (SchemeNameToSchemes (schemeName) is { }) @@ -100,74 +73,36 @@ public static void RemoveScheme (string schemeName) throw new InvalidOperationException ($@"{schemeName}: Cannot remove a built-in Scheme."); } - if (!GetSchemes ().TryGetValue (schemeName, out _)) + lock (_schemesLock) { - throw new InvalidOperationException ($@"{schemeName}: Does not exist in Schemes."); - } + if (!_schemes.Remove (schemeName)) + { + throw new InvalidOperationException ($@"{schemeName}: Does not exist in Schemes."); + } - GetSchemes ().Remove (schemeName); + _runtimeSchemes.Remove (schemeName); + } } - /// - /// Gets the for the specified . - /// - /// - /// - /// + /// Gets the for a built-in value. public static Scheme GetScheme (Schemes schemeName) { - // Convert schemeName to string via Enum api string? schemeNameString = SchemesToSchemeName (schemeName); - return schemeNameString is null ? throw new ArgumentException ($"Invalid scheme name: {schemeName}") : GetSchemesForCurrentTheme () [schemeNameString]!; + return schemeNameString is null + ? throw new ArgumentException ($"Invalid scheme name: {schemeName}") + : GetSchemesForCurrentTheme () [schemeNameString]!; } - /// - /// Gets the for the specified string. - /// - /// - /// - /// If is not found in the current theme. + /// Gets the for the specified name. public static Scheme GetScheme (string schemeName) => GetSchemesForCurrentTheme () [schemeName]!; - /// - /// Attempts to get the for the specified name without throwing. - /// Returns and sets to if the scheme is - /// not found, or if the configuration is not in a state where schemes can be resolved. - /// - /// The name of the scheme to retrieve. - /// - /// When this method returns , contains the resolved ; otherwise - /// . - /// - /// if the scheme was found; otherwise . + /// Attempts to get a scheme without throwing. public static bool TryGetScheme (string schemeName, [NotNullWhen (true)] out Scheme? scheme) { lock (_schemesLock) { - Dictionary schemes; - - if (!ConfigurationManager.IsInitialized ()) - { - // Module initializer / unit-test path — fall back to hard-coded defaults. - ImmutableSortedDictionary hardCoded = GetHardCodedSchemes (); - - schemes = hardCoded.ToDictionary (StringComparer.InvariantCultureIgnoreCase); - } - else - { - // Avoid GetSchemesForCurrentTheme() — it throws if the Schemes property is absent. - if (ThemeManager.GetCurrentTheme () ["Schemes"].PropertyValue is not Dictionary themeSchemes) - { - scheme = null; - - return false; - } - - schemes = themeSchemes; - } - - if (schemes.TryGetValue (schemeName, out Scheme? s) && s is { }) + if (_schemes.TryGetValue (schemeName, out Scheme? s) && s is { }) { scheme = s; @@ -180,19 +115,10 @@ public static bool TryGetScheme (string schemeName, [NotNullWhen (true)] out Sch } } - /// - /// Gets the name of the specified . Will throw an exception if - /// is not a built-in Scheme. - /// - /// - /// The name of scheme. + /// Gets the name of a built-in value. public static string? SchemesToSchemeName (Schemes schemeName) => Enum.GetName (typeof (Schemes), schemeName); - /// - /// Converts a string to a enum value. - /// - /// if the schemeName is not a built-in Scheme name. - /// if is not the name of a built-in Scheme. + /// Converts a string to a built-in scheme name, or . public static string? SchemeNameToSchemes (string schemeName) { if (Enum.TryParse (typeof (Schemes), schemeName, out object? value)) @@ -203,45 +129,106 @@ public static bool TryGetScheme (string schemeName, [NotNullWhen (true)] out Sch return null; } - /// - /// Get the dictionary of schemes from the current theme. Current means active. - /// - /// + /// Gets the dictionary of schemes for the active theme. public static Dictionary GetSchemesForCurrentTheme () { lock (_schemesLock) { - if (!ConfigurationManager.IsInitialized ()) - { - throw new InvalidOperationException ("CM Must be Initialized"); - } + return _schemes; + } + } - if (ThemeManager.GetCurrentTheme () ["Schemes"].PropertyValue is not Dictionary schemes) - { - // Most likely because "Schemes": was left out of the config - throw new InvalidOperationException ("Current Theme does not have a Scheme."); - } + /// Gets the names of the schemes in the current theme. + public static ImmutableList GetSchemeNames () + { + lock (_schemesLock) + { + return _schemes.Keys.ToImmutableList (); + } + } + + IReadOnlyList ISchemeManager.SchemeNames => GetSchemeNames (); + + Scheme? ISchemeManager.GetScheme (string name) => TryGetScheme (name, out Scheme? scheme) ? scheme : null; + + void ISchemeManager.AddScheme (string name, Scheme scheme) => AddScheme (name, scheme); + + private static Dictionary _runtimeSchemes = new (StringComparer.InvariantCultureIgnoreCase); - return schemes; + /// INTERNAL: Gets a copy of the schemes added or updated at runtime via (for tests). + internal static Dictionary GetRuntimeSchemes () + { + lock (_schemesLock) + { + return new (_runtimeSchemes, StringComparer.InvariantCultureIgnoreCase); } } - /// - /// Convenience method to get the names of the schemes. - /// - /// - public static ImmutableList GetSchemeNames () + /// INTERNAL: Replaces the runtime-added scheme dictionary (for tests). + internal static void SetRuntimeSchemes (Dictionary schemes) { lock (_schemesLock) { - return GetSchemes ().Keys.ToImmutableList (); + _runtimeSchemes = new (schemes, StringComparer.InvariantCultureIgnoreCase); } } - internal static void LoadToHardCodedDefaults () => + internal static void LoadToHardCodedDefaults () + { + ReplaceSchemes (HardCodedDictionary ()); + SetRuntimeSchemes (new (StringComparer.InvariantCultureIgnoreCase)); + } + + /// + /// Publishes the schemes for from the raw-JSON merged source + /// view: hard-coded defaults, overlaid by the root Schemes section deep-merged with the theme's + /// Schemes overlay (the root-then-overlay contract every ThemeScope section follows), overlaid + /// by schemes the app added or updated at runtime via (app wins until changed, + /// matching the documented "If the name already exists, it is updated" contract). + /// + internal static void ApplyFromConfiguration (JsonObject mergedJson, string? canonicalThemeName) + { + Dictionary next = HardCodedDictionary (); + + JsonObject combined = []; + + if (mergedJson ["Schemes"] is JsonObject rootSchemes) + { + JsonMerge.DeepMerge (combined, rootSchemes); + } + + if (canonicalThemeName is { } && mergedJson ["Themes"]? [canonicalThemeName]? ["Schemes"] is JsonObject themeSchemes) + { + JsonMerge.DeepMerge (combined, themeSchemes); + } + + foreach (KeyValuePair pair in combined) + { + if (pair.Value is not JsonObject schemeJson || schemeJson.Count == 0) + { + continue; + } + + // One bad value must not silently revert the user's whole scheme to hard-coded colors + // with zero diagnostics; Deserialize collects the error for printing at shutdown. + Scheme? parsed = ConfigurationSectionJson.Deserialize (schemeJson, $"Scheme \"{pair.Key}\""); - // BUGBUG: SchemeManager is broken and needs to be fixed to not have the hard coded schemes get overwritten. - // BUGBUG: This is a partial workaround - // BUGBUG: See https://github.com/tui-cs/Terminal.Gui/issues/4288 - SetSchemes (GetHardCodedSchemes ().ToDictionary ()); + if (parsed is null) + { + continue; + } + + next [pair.Key] = parsed; + } + + lock (_schemesLock) + { + foreach (KeyValuePair pair in _runtimeSchemes) + { + next [pair.Key] = pair.Value; + } + } + + ReplaceSchemes (next); + } } diff --git a/Terminal.Gui/Configuration/Scope.cs b/Terminal.Gui/Configuration/Scope.cs deleted file mode 100644 index 72e4b17024..0000000000 --- a/Terminal.Gui/Configuration/Scope.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -#pragma warning disable CS0618 // Obsolete - Scope uses ConfigProperty/ConfigurationPropertyAttribute internally - -namespace Terminal.Gui.Configuration; - -/// -/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define -/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings. -/// -/// When constructed, the dictionary will be populated with uninitialized configuration properties for the -/// scope ( will be ). -/// -/// -public class Scope : ConcurrentDictionary -{ - /// - /// Creates a new instance. The dictionary will be populated with uninitialized ( - /// will be ). - /// - public Scope () : base (StringComparer.InvariantCultureIgnoreCase) { } - - /// - /// INTERNAL: Adds a new ConfigProperty given a . Determines the correct PropertyInfo etc... by - /// retrieving the - /// hard coded value for . - /// - /// - /// - internal void AddValue (string name, object? value) - { - ConfigProperty? configProperty = GetHardCodedProperty (name); - - if (configProperty is null) - { - throw new InvalidOperationException ($@"{name} is not a hard coded property."); - } - - TryAdd (name, ConfigProperty.CreateCopy (configProperty)); - this [name].PropertyValue = configProperty.PropertyValue; - } - - internal ConfigProperty? GetHardCodedProperty (string name) - { - ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)! - .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name) - .Value; - - if (configProperty is null) - { - return null; - } - - var copy = ConfigProperty.CreateCopy (configProperty); - copy.PropertyValue = configProperty.PropertyValue; - - return copy; - } - - internal ConfigProperty GetUninitializedProperty (string name) - { - ConfigProperty? configProperty = ConfigurationManager.GetUninitializedConfigPropertiesByScope (typeof (T).Name)! - .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name) - .Value; - - if (configProperty is null) - { - throw new InvalidOperationException ($@"{name} is not a ConfigProperty."); - } - - var copy = ConfigProperty.CreateCopy (configProperty); - copy.PropertyValue = configProperty.PropertyValue; - - return copy; - } - - /// - /// INTERNAL: Updates the values of the properties of this scope to their corresponding static - /// properties. - /// - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "ConfigProperty.PropertyInfo values originate from ConfigPropertyHostTypes, whose public properties are statically rooted via DynamicDependency.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "ConfigProperty.PropertyInfo values originate from ConfigPropertyHostTypes, whose public properties are statically rooted via DynamicDependency.")] - internal void UpdateToCurrentValues () - { - foreach (KeyValuePair validProperties in this.Where (cp => cp.Value.PropertyInfo is { })) - { - validProperties.Value.UpdateToCurrentValue (); - } - } - - /// - /// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values. - /// - internal void LoadHardCodedDefaults () - { - foreach (KeyValuePair hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!) - { - var copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value); - TryAdd (hardCodedKeyValuePair.Key, copy); - this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue; - } - } - - /// - /// INTERNAL: Updates this scope with the values in using a deep clone. - /// - /// - /// The updated scope (this). - internal Scope? UpdateFrom (Scope scope) - { - foreach (KeyValuePair prop in scope) - { - if (!prop.Value.HasValue) - { - continue; - } - - if (!ContainsKey (prop.Key)) - { - if (!prop.Value.HasValue) - { - continue; - } - - // Add an empty (HasValue = false) property to this scope - var copy = ConfigProperty.CreateCopy (prop.Value); - copy.PropertyValue = prop.Value.PropertyValue; - TryAdd (prop.Key, copy); - } - - // Update the property value - this [prop.Key].UpdateFrom (prop.Value.PropertyValue); - } - - return this; - } - - /// - /// INTERNAL: Applies the values of the properties of this scope to their corresponding - /// properties. - /// - /// if one or more property value was applied; otherwise. - [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "ConfigProperty.PropertyInfo values originate from ConfigPropertyHostTypes, whose public properties are statically rooted via DynamicDependency.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "ConfigProperty.PropertyInfo values originate from ConfigPropertyHostTypes, whose public properties are statically rooted via DynamicDependency.")] - internal bool Apply () - { - if (!ConfigurationManager.IsEnabled) - { - Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed."); - } - - var set = false; - - foreach (KeyValuePair propWithValue in this.Where (t => t.Value.HasValue)) - { - if (propWithValue.Value.PropertyInfo != null) - { - object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null); - object? newValue = null; - - // QUESTION: Should we avoid setting if currentValue == newValue? - - if (propWithValue.Value.PropertyValue is Scope scopeSource && currentValue is Scope scopeDest) - { - newValue = scopeDest.UpdateFrom (scopeSource); - } - else - { - // Use DeepCloner to create a deep copy of the property value - newValue = DeepCloner.DeepClone (propWithValue.Value.PropertyValue); - } - - Debug.Assert (!propWithValue.Value.Immutable); - propWithValue.Value.PropertyInfo.SetValue (null, newValue); - - set = true; - } - } - - return set; - } -} diff --git a/Terminal.Gui/Configuration/ScopeJsonConverter.cs b/Terminal.Gui/Configuration/ScopeJsonConverter.cs deleted file mode 100644 index de05ef45af..0000000000 --- a/Terminal.Gui/Configuration/ScopeJsonConverter.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -#pragma warning disable CS0618 // Obsolete - ScopeJsonConverter still uses ConfigurationManager internally during transition - -namespace Terminal.Gui.Configuration; - -/// -/// Converts instances to/from JSON. Does all the heavy lifting of reading/writing config -/// data to/from JSON documents. -/// -/// -internal class ScopeJsonConverter< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor - | DynamicallyAccessedMemberTypes.PublicProperties)] -TScopeT> : JsonConverter where TScopeT : Scope -{ - [UnconditionalSuppressMessage ("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = - "Arbitrary property-level converter fallback is guarded by RuntimeFeature.IsDynamicCodeSupported and is unreachable under NativeAOT.")] - [UnconditionalSuppressMessage ("Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = - "Arbitrary property-level converter fallback is only used when a consumer opts into a custom property-level JsonConverter on JIT-supported runtimes.")] - public override TScopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException ($$"""Expected a JSON object ("{ "propName" : ... }"), but got "{{reader.TokenType}}"."""); - } - - var scope = (TScopeT)Activator.CreateInstance (typeof (TScopeT))!; - var propertyName = string.Empty; - - while (reader.Read ()) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return scope!; - } - - if (reader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException ($"After {propertyName}: Expected a JSON property name, but got \"{reader.TokenType}\""); - } - - propertyName = reader.GetString (); - reader.Read (); - - // Get the hardcoded property from the TscopeT (e.g. ThemeScope.GetHardCodedProperty) - ConfigProperty? configProperty = scope.GetHardCodedProperty (propertyName!); - - if (propertyName is { } && configProperty is { }) - { - // This property name was found in the cached hard-coded scope dict. - - // Add it, with no value - configProperty.HasValue = false; - configProperty.PropertyValue = null; - scope.TryAdd (propertyName, configProperty); - - // Figure out if it needs a JsonConverter and if so, create one - Type? propertyType = configProperty?.PropertyInfo?.PropertyType!; - - if (configProperty?.ConverterType is { } converterType) - { - if (reader.TokenType == JsonTokenType.Null && CanAcceptNullValue (propertyType!)) - { - scope! [propertyName].PropertyValue = null; - - continue; - } - - if (TryReadWithKnownConverter (ref reader, propertyType!, converterType, options, out object? convertedValue)) - { - scope! [propertyName].PropertyValue = convertedValue; - - continue; - } - - if (TryReadWithDynamicConverter (ref reader, propertyType!, converterType, options, out convertedValue)) - { - scope! [propertyName].PropertyValue = convertedValue; - - continue; - } - - throw new JsonException ($"{propertyName}: Unsupported configuration converter type \"{converterType.FullName}\" when dynamic code is unavailable."); - } - scope! [propertyName].PropertyValue = DeserializePropertyValue (ref reader, propertyType!, options); - - // Logging.Warning ($"{propertyName} = {scope! [propertyName].PropertyValue}"); - } - else - { - // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude] - // like ScopeSettings.$schema. - // If so, don't add it to the dictionary but apply it to the underlying property on - // the scopeT. - // BUGBUG: This is terrible design. The only time it's used is for $schema though. - PropertyInfo? property = typeof (TScopeT).GetProperties () - .Where (p => - { - if (p.GetCustomAttribute (typeof (JsonIncludeAttribute)) is JsonIncludeAttribute { } jia) - { - var jsonPropertyNameAttribute = - p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as - JsonPropertyNameAttribute; - - if (jsonPropertyNameAttribute?.Name == propertyName) - { - // Bit of a hack, modifying propertyName in an enumerator... - propertyName = p.Name; - - return true; - } - - return p.Name == propertyName; - } - - return false; - }) - .FirstOrDefault (); - - if (property is { }) - { - // Set the value of propertyName on the scopeT. - PropertyInfo prop = typeof (TScopeT).GetProperty (propertyName!)!; - - prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, TuiSerializerContext.Instance)); - } - else - { - // Unknown property - throw new JsonException ($"{propertyName}: Unknown property name."); - } - } - } - - throw new JsonException ($"{propertyName}: Json error in ScopeJsonConverter"); - } - - [UnconditionalSuppressMessage ("AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = - "Arbitrary property-level converter fallback is guarded by RuntimeFeature.IsDynamicCodeSupported and is unreachable under NativeAOT.")] - [UnconditionalSuppressMessage ("Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = - "Arbitrary property-level converter fallback is only used when a consumer opts into a custom property-level JsonConverter on JIT-supported runtimes.")] - public override void Write (Utf8JsonWriter writer, TScopeT scope, JsonSerializerOptions options) - { - writer.WriteStartObject (); - - IEnumerable properties = typeof (TScopeT).GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null); - - foreach (PropertyInfo p in properties) - { - writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p)); - object? prop = p.GetValue (scope); - JsonSerializer.Serialize (writer, prop, p.PropertyType, TuiSerializerContext.Instance); - } - - foreach (KeyValuePair p in from p in scope.Where (cp => cp.Value.ScopeType == typeof (TScopeT).Name) - where p.Value.HasValue - select p) - { - Type? propertyType = p.Value.PropertyInfo?.PropertyType; - object? propertyValue = p.Value.PropertyValue; - - if (ShouldSkipNullPropertyValue (propertyType, propertyValue)) - { - continue; - } - - writer.WritePropertyName (p.Key); - - if (propertyType != null && p.Value.ConverterType is { } converterType) - { - if (propertyValue is null) - { - writer.WriteNullValue (); - - continue; - } - - if (TryWriteWithKnownConverter (writer, propertyType, converterType, propertyValue, options)) - { - continue; - } - - if (TryWriteWithDynamicConverter (writer, propertyType, converterType, propertyValue, options)) - { - continue; - } - - throw new JsonException ($"{p.Key}: Unsupported configuration converter type \"{converterType.FullName}\" when dynamic code is unavailable."); - } - object? prop = propertyValue; - - if (prop == null) - { - writer.WriteNullValue (); - } - else - { - if (TryWriteEnumValue (writer, prop.GetType (), prop)) - { - continue; - } - - JsonTypeInfo? jsonTypeInfo = TuiSerializerContext.Instance.GetTypeInfo (prop.GetType ()); - - if (jsonTypeInfo is null) - { - throw new JsonException ($"{p.Key}: No source-generated JsonTypeInfo is registered for {prop.GetType ().FullName}."); - } - - JsonSerializer.Serialize (writer, prop, jsonTypeInfo); - } - } - - writer.WriteEndObject (); - } - - private static bool CanAcceptNullValue (Type propertyType) => !propertyType.IsValueType || Nullable.GetUnderlyingType (propertyType) is { }; - - private static object? DeserializePropertyValue (ref Utf8JsonReader reader, Type propertyType, JsonSerializerOptions options) - { - Type? nullableType = Nullable.GetUnderlyingType (propertyType); - Type enumType = nullableType ?? propertyType; - - if (enumType.IsEnum) - { - if (reader.TokenType == JsonTokenType.String) - { - return Enum.Parse (enumType, reader.GetString ()!, true); - } - - if (reader.TokenType == JsonTokenType.Null && nullableType is { }) - { - return null; - } - } - - JsonTypeInfo? jsonTypeInfo = TuiSerializerContext.Instance.GetTypeInfo (propertyType); - - if (jsonTypeInfo is null) - { - throw new JsonException ($"{propertyType.FullName}: No source-generated JsonTypeInfo is registered for this configuration property type."); - } - - return JsonSerializer.Deserialize (ref reader, jsonTypeInfo); - } - - private static bool ShouldSkipNullPropertyValue (Type? propertyType, object? propertyValue) - { - if (propertyValue is { }) - { - return false; - } - - if (propertyType is null) - { - return false; - } - - return propertyType.IsValueType && Nullable.GetUnderlyingType (propertyType) is null; - } - - [RequiresDynamicCode ("Instantiates arbitrary property-level JsonConverter types for JIT-only configuration paths.")] - [RequiresUnreferencedCode ("Arbitrary property-level JsonConverter types may access unreferenced members.")] - private static bool TryReadWithDynamicConverter (ref Utf8JsonReader reader, - Type propertyType, - Type converterType, - JsonSerializerOptions options, - out object? value) - { - if (!RuntimeFeature.IsDynamicCodeSupported) - { - value = null; - - return false; - } - - object converter = Activator.CreateInstance (converterType)!; - - if (converter is JsonConverterFactory factory && factory.CanConvert (propertyType)) - { - converter = factory.CreateConverter (propertyType, options)!; - } - - try - { - Type helperType = typeof (ReadHelper<>).MakeGenericType (typeof (TScopeT), propertyType); - var readHelper = (ReadHelper)Activator.CreateInstance (helperType, converter)!; - value = readHelper.Read (ref reader, propertyType, options); - - return true; - } - catch (NotSupportedException e) - { - throw new JsonException ($"{propertyType.Name}: Error reading property with converter \"{converterType.FullName}\".", e); - } - catch (TargetInvocationException) - { - value = JsonSerializer.Deserialize (ref reader, propertyType, options); - - return true; - } - } - - private static bool TryReadWithKnownConverter (ref Utf8JsonReader reader, - Type propertyType, - Type converterType, - JsonSerializerOptions options, - out object? value) - { - if (converterType == typeof (ConcurrentDictionaryJsonConverter)) - { - value = new ConcurrentDictionaryJsonConverter ().Read (ref reader, propertyType, options); - - return true; - } - - if (converterType == typeof (DictionaryJsonConverter)) - { - value = new DictionaryJsonConverter ().Read (ref reader, propertyType, options); - - return true; - } - - if (converterType == typeof (TraceCategoryJsonConverter)) - { - value = new TraceCategoryJsonConverter ().Read (ref reader, propertyType, options); - - return true; - } - - value = null; - - return false; - } - - private static bool TryWriteEnumValue (Utf8JsonWriter writer, Type propertyType, object value) - { - Type enumType = Nullable.GetUnderlyingType (propertyType) ?? propertyType; - - if (!enumType.IsEnum) - { - return false; - } - - writer.WriteStringValue (value.ToString ()); - - return true; - } - - [RequiresDynamicCode ("Instantiates arbitrary property-level JsonConverter types for JIT-only configuration paths.")] - [RequiresUnreferencedCode ("Arbitrary property-level JsonConverter types may access unreferenced members.")] - private static bool TryWriteWithDynamicConverter (Utf8JsonWriter writer, Type propertyType, Type converterType, object value, JsonSerializerOptions options) - { - if (!RuntimeFeature.IsDynamicCodeSupported) - { - return false; - } - - object converter = Activator.CreateInstance (converterType)!; - - if (converter is JsonConverterFactory factory && factory.CanConvert (propertyType)) - { - converter = factory.CreateConverter (propertyType, options)!; - } - - MethodInfo? writeMethod = converter.GetType ().GetMethod (nameof (Write), [typeof (Utf8JsonWriter), propertyType, typeof (JsonSerializerOptions)]); - - if (writeMethod is null) - { - throw new JsonException ($"{propertyType.Name}: Converter \"{converterType.FullName}\" does not expose a compatible Write method."); - } - - try - { - writeMethod.Invoke (converter, [writer, value, options]); - - return true; - } - catch (TargetInvocationException e) when (e.InnerException is { }) - { - throw new JsonException ($"{propertyType.Name}: Error writing property with converter \"{converterType.FullName}\".", e.InnerException); - } - } - - private static bool TryWriteWithKnownConverter (Utf8JsonWriter writer, Type propertyType, Type converterType, object? value, JsonSerializerOptions options) - { - if (converterType == typeof (ConcurrentDictionaryJsonConverter)) - { - new ConcurrentDictionaryJsonConverter ().Write (writer, (ConcurrentDictionary)value!, options); - - return true; - } - - if (converterType == typeof (DictionaryJsonConverter)) - { - new DictionaryJsonConverter ().Write (writer, (Dictionary)value!, options); - - return true; - } - - if (converterType == typeof (TraceCategoryJsonConverter)) - { - new TraceCategoryJsonConverter ().Write (writer, (TraceCategory)value!, options); - - return true; - } - - return false; - } - - internal abstract class ReadHelper - { - public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); - } - - [method: RequiresUnreferencedCode ("Creates delegates for arbitrary property-level JsonConverter.Read methods on JIT-only paths.")] - internal class ReadHelper (object converter) : ReadHelper - { - private readonly ReadDelegate _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); - - public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) => _readDelegate.Invoke (ref reader, type, options); - - private delegate TValue ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); - } -} diff --git a/Terminal.Gui/Configuration/Settings/ApplicationSettings.cs b/Terminal.Gui/Configuration/Settings/ApplicationSettings.cs index 0725b1cc44..5c63041801 100644 --- a/Terminal.Gui/Configuration/Settings/ApplicationSettings.cs +++ b/Terminal.Gui/Configuration/Settings/ApplicationSettings.cs @@ -4,6 +4,10 @@ namespace Terminal.Gui.Configuration; /// Settings POCO for application-level configuration (SettingsScope). /// These correspond to the static properties on and related classes. /// +/// +/// Key bindings are not properties on this POCO. Nested JSON Application.DefaultKeyBindings +/// overlays when configuration is applied. +/// public class ApplicationSettings { /// Gets or sets the application model (FullScreen or Inline). diff --git a/Terminal.Gui/Configuration/Settings/ButtonSettings.cs b/Terminal.Gui/Configuration/Settings/ButtonSettings.cs index 2ec80020f8..da913c0a55 100644 --- a/Terminal.Gui/Configuration/Settings/ButtonSettings.cs +++ b/Terminal.Gui/Configuration/Settings/ButtonSettings.cs @@ -1,19 +1,33 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class ButtonSettings +/// +/// +/// is the compile-time-known fallback (constructor defaults). +/// holds the currently effective values and is updated atomically by +/// via Volatile.Write at startup and on theme switch. Mid-render +/// consumers always observe either the previous or the next reference — never a partially populated one. +/// +/// +public sealed record ButtonSettings { - /// Gets or sets the default shadow style for buttons. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.Opaque; + /// Gets the default shadow style for buttons. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.Opaque; - /// Gets or sets the default mouse highlight states for buttons. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; + /// Gets the default mouse highlight states for buttons. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static ButtonSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static ButtonSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static ButtonSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static ButtonSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/CharMapSettings.cs b/Terminal.Gui/Configuration/Settings/CharMapSettings.cs index e476205c9e..8fe7b5c2c1 100644 --- a/Terminal.Gui/Configuration/Settings/CharMapSettings.cs +++ b/Terminal.Gui/Configuration/Settings/CharMapSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class CharMapSettings +public sealed record CharMapSettings { - /// Gets or sets the default cursor style for character map views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for character map views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static CharMapSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static CharMapSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static CharMapSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static CharMapSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs b/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs index 408d20ed43..0a7f3b3a72 100644 --- a/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs +++ b/Terminal.Gui/Configuration/Settings/CheckBoxSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class CheckBoxSettings +public sealed record CheckBoxSettings { - /// Gets or sets the default mouse highlight states for checkboxes. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; + /// Gets the default mouse highlight states for checkboxes. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static CheckBoxSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static CheckBoxSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static CheckBoxSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static CheckBoxSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/ConfigurationSectionJson.cs b/Terminal.Gui/Configuration/Settings/ConfigurationSectionJson.cs new file mode 100644 index 0000000000..6c5c8122a2 --- /dev/null +++ b/Terminal.Gui/Configuration/Settings/ConfigurationSectionJson.cs @@ -0,0 +1,117 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Configuration; + +namespace Terminal.Gui.Configuration; + +/// +/// Rebuilds a JSON tree from an MEC section so STJ can deserialize nested dictionaries and arrays. +/// MEC stores JSON arrays as numbered children ("0", "1", …); those become +/// only when the keys are exactly 0..n-1. +/// +internal static class ConfigurationSectionJson +{ + /// Converts to a tree. + public static JsonNode? ToJson (IConfigurationSection section) + { + List children = [.. section.GetChildren ()]; + + if (children.Count == 0) + { + return section.Value is null ? null : JsonValue.Create (section.Value); + } + + if (TryAsIndexArray (children, out IConfigurationSection []? ordered)) + { + JsonArray array = []; + + foreach (IConfigurationSection child in ordered) + { + array.Add (ToJson (child)); + } + + return array; + } + + JsonObject obj = []; + + foreach (IConfigurationSection child in children) + { + JsonNode? nested = ToJson (child); + + if (nested is not null) + { + obj [child.Key] = nested; + } + } + + return obj; + } + + /// + /// Deserializes as via STJ. + /// Empty sections return . JSON errors are collected in . + /// + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "T is a JsonSerializable type registered on SourceGenerationContext.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "T is a JsonSerializable type registered on SourceGenerationContext.")] + public static T? Deserialize (IConfigurationSection section, string errorLabel) where T : class + { + if (ToJson (section) is not JsonObject obj) + { + return null; + } + + return Deserialize (obj, $"{errorLabel} ({section.Path})"); + } + + /// + /// Deserializes as via STJ. + /// Null or empty objects return . JSON errors are collected in . + /// + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "T is a JsonSerializable type registered on SourceGenerationContext.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "T is a JsonSerializable type registered on SourceGenerationContext.")] + public static T? Deserialize (JsonObject? obj, string errorLabel) where T : class + { + if (obj is null || obj.Count == 0) + { + return null; + } + + try + { + return JsonSerializer.Deserialize (obj.ToJsonString (), TuiSerializerContext.Instance.Options); + } + catch (JsonException ex) + { + TuiJsonErrors.Add ($"{errorLabel}: {ex.Message}"); + + return null; + } + } + + private static bool TryAsIndexArray (List children, [NotNullWhen (true)] out IConfigurationSection []? ordered) + { + IConfigurationSection [] slots = new IConfigurationSection [children.Count]; + + foreach (IConfigurationSection child in children) + { + if (!int.TryParse (child.Key, NumberStyles.None, CultureInfo.InvariantCulture, out int index) + || index < 0 + || index >= slots.Length + || slots [index] is not null) + { + ordered = null; + + return false; + } + + slots [index] = child; + } + + ordered = slots; + + return true; + } +} diff --git a/Terminal.Gui/Configuration/Settings/DialogSettings.cs b/Terminal.Gui/Configuration/Settings/DialogSettings.cs index 60c01d6aef..34c5efe258 100644 --- a/Terminal.Gui/Configuration/Settings/DialogSettings.cs +++ b/Terminal.Gui/Configuration/Settings/DialogSettings.cs @@ -1,25 +1,31 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class DialogSettings +public sealed record DialogSettings { - /// Gets or sets the default shadow style for dialogs. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.Transparent; + /// Gets the default shadow style for dialogs. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.Transparent; - /// Gets or sets the default border style for dialogs. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + /// Gets the default border style for dialogs. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Heavy; - /// Gets or sets the default button alignment for dialogs. - public Alignment DefaultButtonAlignment { get; set; } = Alignment.End; + /// Gets the default button alignment for dialogs. + public Alignment DefaultButtonAlignment { get; init; } = Alignment.End; - /// Gets or sets the default button alignment modes for dialogs. - public AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; + /// Gets the default button alignment modes for dialogs. + public AlignmentModes DefaultButtonAlignmentModes { get; init; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static DialogSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static DialogSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static DialogSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static DialogSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/DriverSettings.cs b/Terminal.Gui/Configuration/Settings/DriverSettings.cs index 133005b7f1..a6671c68b6 100644 --- a/Terminal.Gui/Configuration/Settings/DriverSettings.cs +++ b/Terminal.Gui/Configuration/Settings/DriverSettings.cs @@ -9,7 +9,7 @@ public class DriverSettings public bool Force16Colors { get; set; } /// Gets or sets the size detection mode. - public SizeDetectionMode SizeDetection { get; set; } = SizeDetectionMode.AnsiQuery; + public SizeDetectionMode SizeDetection { get; set; } = SizeDetectionMode.Polling; /// /// The static facade instance. Always contains the current effective values. diff --git a/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs b/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs index 89748a8fe2..482ae0d9db 100644 --- a/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/FrameViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class FrameViewSettings +public sealed record FrameViewSettings { - /// Gets or sets the default border style for frame views. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Rounded; + /// Gets the default border style for frame views. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Rounded; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static FrameViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static FrameViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static FrameViewSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static FrameViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/GlyphSettings.cs b/Terminal.Gui/Configuration/Settings/GlyphSettings.cs index b604e95ea2..39d0034866 100644 --- a/Terminal.Gui/Configuration/Settings/GlyphSettings.cs +++ b/Terminal.Gui/Configuration/Settings/GlyphSettings.cs @@ -3,443 +3,449 @@ namespace Terminal.Gui.Configuration; /// /// Settings POCO for defaults (ThemeScope). /// -public class GlyphSettings +public sealed record GlyphSettings { /// Unicode replacement character; used when a wide glyph can't be output because it would be clipped. - public Rune WideGlyphReplacement { get; set; } = (Rune)' '; + public Rune WideGlyphReplacement { get; init; } = (Rune)' '; /// File icon. - public Rune File { get; set; } = (Rune)'☰'; + public Rune File { get; init; } = (Rune)'☰'; /// Folder icon. - public Rune Folder { get; set; } = (Rune)'꤉'; + public Rune Folder { get; init; } = (Rune)'꤉'; /// Horizontal Ellipsis. - public Rune HorizontalEllipsis { get; set; } = (Rune)'…'; + public Rune HorizontalEllipsis { get; init; } = (Rune)'…'; /// Vertical Four Dots. - public Rune VerticalFourDots { get; set; } = (Rune)'⁞'; + public Rune VerticalFourDots { get; init; } = (Rune)'⁞'; /// Null symbol. - public Rune Null { get; set; } = (Rune)'␀'; + public Rune Null { get; init; } = (Rune)'␀'; /// Checked indicator. - public Rune CheckStateChecked { get; set; } = (Rune)'☑'; + public Rune CheckStateChecked { get; init; } = (Rune)'☑'; /// Not Checked indicator. - public Rune CheckStateUnChecked { get; set; } = (Rune)'☐'; + public Rune CheckStateUnChecked { get; init; } = (Rune)'☐'; /// Null Checked indicator. - public Rune CheckStateNone { get; set; } = (Rune)'⬛'; + public Rune CheckStateNone { get; init; } = (Rune)'⬛'; /// Selected indicator. - public Rune Selected { get; set; } = (Rune)'◉'; + public Rune Selected { get; init; } = (Rune)'◉'; /// Not Selected indicator. - public Rune UnSelected { get; set; } = (Rune)'○'; + public Rune UnSelected { get; init; } = (Rune)'○'; /// Right arrow. - public Rune RightArrow { get; set; } = (Rune)'►'; + public Rune RightArrow { get; init; } = (Rune)'►'; /// Left arrow. - public Rune LeftArrow { get; set; } = (Rune)'◄'; + public Rune LeftArrow { get; init; } = (Rune)'◄'; /// Down arrow. - public Rune DownArrow { get; set; } = (Rune)'▼'; + public Rune DownArrow { get; init; } = (Rune)'▼'; /// Up arrow. - public Rune UpArrow { get; set; } = (Rune)'▲'; + public Rune UpArrow { get; init; } = (Rune)'▲'; /// Left default indicator. - public Rune LeftDefaultIndicator { get; set; } = (Rune)'►'; + public Rune LeftDefaultIndicator { get; init; } = (Rune)'►'; /// Right default indicator. - public Rune RightDefaultIndicator { get; set; } = (Rune)'◄'; + public Rune RightDefaultIndicator { get; init; } = (Rune)'◄'; /// Left Bracket. - public Rune LeftBracket { get; set; } = (Rune)'⟦'; + public Rune LeftBracket { get; init; } = (Rune)'⟦'; /// Right Bracket. - public Rune RightBracket { get; set; } = (Rune)'⟧'; + public Rune RightBracket { get; init; } = (Rune)'⟧'; /// Half block meter segment. - public Rune BlocksMeterSegment { get; set; } = (Rune)'▌'; + public Rune BlocksMeterSegment { get; init; } = (Rune)'▌'; /// Continuous block meter segment. - public Rune ContinuousMeterSegment { get; set; } = (Rune)'█'; + public Rune ContinuousMeterSegment { get; init; } = (Rune)'█'; /// Stipple pattern. - public Rune Stipple { get; set; } = (Rune)'░'; + public Rune Stipple { get; init; } = (Rune)'░'; /// Diamond. - public Rune Diamond { get; set; } = (Rune)'◊'; + public Rune Diamond { get; init; } = (Rune)'◊'; /// Close. - public Rune Close { get; set; } = (Rune)'✘'; + public Rune Close { get; init; } = (Rune)'✘'; /// Minimize. - public Rune Minimize { get; set; } = (Rune)'❏'; + public Rune Minimize { get; init; } = (Rune)'❏'; /// Maximize. - public Rune Maximize { get; set; } = (Rune)'✽'; + public Rune Maximize { get; init; } = (Rune)'✽'; /// Dot. - public Rune Dot { get; set; } = (Rune)'∙'; + public Rune Dot { get; init; } = (Rune)'∙'; /// Dotted Square. - public Rune DottedSquare { get; set; } = (Rune)'⬚'; + public Rune DottedSquare { get; init; } = (Rune)'⬚'; /// Black Circle. - public Rune BlackCircle { get; set; } = (Rune)'●'; + public Rune BlackCircle { get; init; } = (Rune)'●'; /// Expand. - public Rune Expand { get; set; } = (Rune)'+'; + public Rune Expand { get; init; } = (Rune)'+'; /// Collapse. - public Rune Collapse { get; set; } = (Rune)'-'; + public Rune Collapse { get; init; } = (Rune)'-'; /// Identical To. - public Rune IdenticalTo { get; set; } = (Rune)'≡'; + public Rune IdenticalTo { get; init; } = (Rune)'≡'; /// Move indicator. - public Rune Move { get; set; } = (Rune)'◊'; + public Rune Move { get; init; } = (Rune)'◊'; /// Size Horizontally indicator. - public Rune SizeHorizontal { get; set; } = (Rune)'↔'; + public Rune SizeHorizontal { get; init; } = (Rune)'↔'; /// Size Vertical indicator. - public Rune SizeVertical { get; set; } = (Rune)'↕'; + public Rune SizeVertical { get; init; } = (Rune)'↕'; /// Size Top Left indicator. - public Rune SizeTopLeft { get; set; } = (Rune)'↖'; + public Rune SizeTopLeft { get; init; } = (Rune)'↖'; /// Size Top Right indicator. - public Rune SizeTopRight { get; set; } = (Rune)'↗'; + public Rune SizeTopRight { get; init; } = (Rune)'↗'; /// Size Bottom Right indicator. - public Rune SizeBottomRight { get; set; } = (Rune)'↘'; + public Rune SizeBottomRight { get; init; } = (Rune)'↘'; /// Size Bottom Left indicator. - public Rune SizeBottomLeft { get; set; } = (Rune)'↙'; + public Rune SizeBottomLeft { get; init; } = (Rune)'↙'; /// Apple (non-BMP). - public Rune Apple { get; set; } = "🍎".ToRunes () [0]; + public Rune Apple { get; init; } = "🍎".ToRunes () [0]; /// Apple (BMP). - public Rune AppleBMP { get; set; } = (Rune)'❦'; + public Rune AppleBMP { get; init; } = (Rune)'❦'; /// Copy indicator. - public Rune Copy { get; set; } = (Rune)'⧉'; + public Rune Copy { get; init; } = (Rune)'⧉'; /// Box Drawings Horizontal Line - Light. - public Rune HLine { get; set; } = (Rune)'─'; + public Rune HLine { get; init; } = (Rune)'─'; /// Box Drawings Vertical Line - Light. - public Rune VLine { get; set; } = (Rune)'│'; + public Rune VLine { get; init; } = (Rune)'│'; /// Box Drawings Double Horizontal. - public Rune HLineDbl { get; set; } = (Rune)'═'; + public Rune HLineDbl { get; init; } = (Rune)'═'; /// Box Drawings Double Vertical. - public Rune VLineDbl { get; set; } = (Rune)'║'; + public Rune VLineDbl { get; init; } = (Rune)'║'; /// Box Drawings Heavy Double Dash Horizontal. - public Rune HLineHvDa2 { get; set; } = (Rune)'╍'; + public Rune HLineHvDa2 { get; init; } = (Rune)'╍'; /// Box Drawings Heavy Triple Dash Vertical. - public Rune VLineHvDa3 { get; set; } = (Rune)'┇'; + public Rune VLineHvDa3 { get; init; } = (Rune)'┇'; /// Box Drawings Heavy Triple Dash Horizontal. - public Rune HLineHvDa3 { get; set; } = (Rune)'┅'; + public Rune HLineHvDa3 { get; init; } = (Rune)'┅'; /// Box Drawings Heavy Quadruple Dash Horizontal. - public Rune HLineHvDa4 { get; set; } = (Rune)'┉'; + public Rune HLineHvDa4 { get; init; } = (Rune)'┉'; /// Box Drawings Heavy Double Dash Vertical. - public Rune VLineHvDa2 { get; set; } = (Rune)'╏'; + public Rune VLineHvDa2 { get; init; } = (Rune)'╏'; /// Box Drawings Heavy Quadruple Dash Vertical. - public Rune VLineHvDa4 { get; set; } = (Rune)'┋'; + public Rune VLineHvDa4 { get; init; } = (Rune)'┋'; /// Box Drawings Light Double Dash Horizontal. - public Rune HLineDa2 { get; set; } = (Rune)'╌'; + public Rune HLineDa2 { get; init; } = (Rune)'╌'; /// Box Drawings Light Triple Dash Vertical. - public Rune VLineDa3 { get; set; } = (Rune)'┆'; + public Rune VLineDa3 { get; init; } = (Rune)'┆'; /// Box Drawings Light Triple Dash Horizontal. - public Rune HLineDa3 { get; set; } = (Rune)'┄'; + public Rune HLineDa3 { get; init; } = (Rune)'┄'; /// Box Drawings Light Quadruple Dash Horizontal. - public Rune HLineDa4 { get; set; } = (Rune)'┈'; + public Rune HLineDa4 { get; init; } = (Rune)'┈'; /// Box Drawings Light Double Dash Vertical. - public Rune VLineDa2 { get; set; } = (Rune)'╎'; + public Rune VLineDa2 { get; init; } = (Rune)'╎'; /// Box Drawings Light Quadruple Dash Vertical. - public Rune VLineDa4 { get; set; } = (Rune)'┊'; + public Rune VLineDa4 { get; init; } = (Rune)'┊'; /// Box Drawings Heavy Horizontal. - public Rune HLineHv { get; set; } = (Rune)'━'; + public Rune HLineHv { get; init; } = (Rune)'━'; /// Box Drawings Heavy Vertical. - public Rune VLineHv { get; set; } = (Rune)'┃'; + public Rune VLineHv { get; init; } = (Rune)'┃'; /// Box Drawings Light Left. - public Rune HalfLeftLine { get; set; } = (Rune)'╴'; + public Rune HalfLeftLine { get; init; } = (Rune)'╴'; /// Box Drawings Light Up. - public Rune HalfTopLine { get; set; } = (Rune)'╵'; + public Rune HalfTopLine { get; init; } = (Rune)'╵'; /// Box Drawings Light Right. - public Rune HalfRightLine { get; set; } = (Rune)'╶'; + public Rune HalfRightLine { get; init; } = (Rune)'╶'; /// Box Drawings Light Down. - public Rune HalfBottomLine { get; set; } = (Rune)'╷'; + public Rune HalfBottomLine { get; init; } = (Rune)'╷'; /// Box Drawings Heavy Left. - public Rune HalfLeftLineHv { get; set; } = (Rune)'╸'; + public Rune HalfLeftLineHv { get; init; } = (Rune)'╸'; /// Box Drawings Heavy Up. - public Rune HalfTopLineHv { get; set; } = (Rune)'╹'; + public Rune HalfTopLineHv { get; init; } = (Rune)'╹'; /// Box Drawings Heavy Right. - public Rune HalfRightLineHv { get; set; } = (Rune)'╺'; + public Rune HalfRightLineHv { get; init; } = (Rune)'╺'; /// Box Drawings Light Down Heavy. - public Rune HalfBottomLineLt { get; set; } = (Rune)'╻'; + public Rune HalfBottomLineLt { get; init; } = (Rune)'╻'; /// Box Drawings Light Horizontal and Heavy Horizontal. - public Rune RightSideLineLtHv { get; set; } = (Rune)'╼'; + public Rune RightSideLineLtHv { get; init; } = (Rune)'╼'; /// Box Drawings Light Vertical and Heavy Horizontal. - public Rune BottomSideLineLtHv { get; set; } = (Rune)'╽'; + public Rune BottomSideLineLtHv { get; init; } = (Rune)'╽'; /// Box Drawings Heavy Left and Light Horizontal. - public Rune LeftSideLineHvLt { get; set; } = (Rune)'╾'; + public Rune LeftSideLineHvLt { get; init; } = (Rune)'╾'; /// Box Drawings Heavy Vertical and Light Horizontal. - public Rune TopSideLineHvLt { get; set; } = (Rune)'╿'; + public Rune TopSideLineHvLt { get; init; } = (Rune)'╿'; /// Box Drawings Upper Left Corner - Light. - public Rune ULCorner { get; set; } = (Rune)'┌'; + public Rune ULCorner { get; init; } = (Rune)'┌'; /// Box Drawings Upper Left Corner - Double. - public Rune ULCornerDbl { get; set; } = (Rune)'╔'; + public Rune ULCornerDbl { get; init; } = (Rune)'╔'; /// Box Drawings Upper Left Corner - Rounded. - public Rune ULCornerR { get; set; } = (Rune)'╭'; + public Rune ULCornerR { get; init; } = (Rune)'╭'; /// Box Drawings Upper Left Corner - Heavy. - public Rune ULCornerHv { get; set; } = (Rune)'┏'; + public Rune ULCornerHv { get; init; } = (Rune)'┏'; /// Box Drawings Upper Left Corner - Heavy Vertical Light Horizontal. - public Rune ULCornerHvLt { get; set; } = (Rune)'┎'; + public Rune ULCornerHvLt { get; init; } = (Rune)'┎'; /// Box Drawings Upper Left Corner - Light Vertical Heavy Horizontal. - public Rune ULCornerLtHv { get; set; } = (Rune)'┍'; + public Rune ULCornerLtHv { get; init; } = (Rune)'┍'; /// Box Drawings Upper Left Corner - Double Down Single Horizontal. - public Rune ULCornerDblSingle { get; set; } = (Rune)'╓'; + public Rune ULCornerDblSingle { get; init; } = (Rune)'╓'; /// Box Drawings Upper Left Corner - Single Down Double Horizontal. - public Rune ULCornerSingleDbl { get; set; } = (Rune)'╒'; + public Rune ULCornerSingleDbl { get; init; } = (Rune)'╒'; /// Box Drawings Lower Left Corner - Light. - public Rune LLCorner { get; set; } = (Rune)'└'; + public Rune LLCorner { get; init; } = (Rune)'└'; /// Box Drawings Lower Left Corner - Heavy. - public Rune LLCornerHv { get; set; } = (Rune)'┗'; + public Rune LLCornerHv { get; init; } = (Rune)'┗'; /// Box Drawings Lower Left Corner - Heavy Vertical Light Horizontal. - public Rune LLCornerHvLt { get; set; } = (Rune)'┖'; + public Rune LLCornerHvLt { get; init; } = (Rune)'┖'; /// Box Drawings Lower Left Corner - Light Vertical Heavy Horizontal. - public Rune LLCornerLtHv { get; set; } = (Rune)'┕'; + public Rune LLCornerLtHv { get; init; } = (Rune)'┕'; /// Box Drawings Lower Left Corner - Double. - public Rune LLCornerDbl { get; set; } = (Rune)'╚'; + public Rune LLCornerDbl { get; init; } = (Rune)'╚'; /// Box Drawings Lower Left Corner - Single Vertical Double Horizontal. - public Rune LLCornerSingleDbl { get; set; } = (Rune)'╘'; + public Rune LLCornerSingleDbl { get; init; } = (Rune)'╘'; /// Box Drawings Lower Left Corner - Double Vertical Single Horizontal. - public Rune LLCornerDblSingle { get; set; } = (Rune)'╙'; + public Rune LLCornerDblSingle { get; init; } = (Rune)'╙'; /// Box Drawings Lower Left Corner - Rounded. - public Rune LLCornerR { get; set; } = (Rune)'╰'; + public Rune LLCornerR { get; init; } = (Rune)'╰'; /// Box Drawings Upper Right Corner - Light. - public Rune URCorner { get; set; } = (Rune)'┐'; + public Rune URCorner { get; init; } = (Rune)'┐'; /// Box Drawings Upper Right Corner - Double. - public Rune URCornerDbl { get; set; } = (Rune)'╗'; + public Rune URCornerDbl { get; init; } = (Rune)'╗'; /// Box Drawings Upper Right Corner - Rounded. - public Rune URCornerR { get; set; } = (Rune)'╮'; + public Rune URCornerR { get; init; } = (Rune)'╮'; /// Box Drawings Upper Right Corner - Heavy. - public Rune URCornerHv { get; set; } = (Rune)'┓'; + public Rune URCornerHv { get; init; } = (Rune)'┓'; /// Box Drawings Upper Right Corner - Heavy Vertical Light Horizontal. - public Rune URCornerHvLt { get; set; } = (Rune)'┑'; + public Rune URCornerHvLt { get; init; } = (Rune)'┑'; /// Box Drawings Upper Right Corner - Light Vertical Heavy Horizontal. - public Rune URCornerLtHv { get; set; } = (Rune)'┒'; + public Rune URCornerLtHv { get; init; } = (Rune)'┒'; /// Box Drawings Upper Right Corner - Double Vertical Single Horizontal. - public Rune URCornerDblSingle { get; set; } = (Rune)'╖'; + public Rune URCornerDblSingle { get; init; } = (Rune)'╖'; /// Box Drawings Upper Right Corner - Single Vertical Double Horizontal. - public Rune URCornerSingleDbl { get; set; } = (Rune)'╕'; + public Rune URCornerSingleDbl { get; init; } = (Rune)'╕'; /// Box Drawings Lower Right Corner - Light. - public Rune LRCorner { get; set; } = (Rune)'┘'; + public Rune LRCorner { get; init; } = (Rune)'┘'; /// Box Drawings Lower Right Corner - Double. - public Rune LRCornerDbl { get; set; } = (Rune)'╝'; + public Rune LRCornerDbl { get; init; } = (Rune)'╝'; /// Box Drawings Lower Right Corner - Rounded. - public Rune LRCornerR { get; set; } = (Rune)'╯'; + public Rune LRCornerR { get; init; } = (Rune)'╯'; /// Box Drawings Lower Right Corner - Heavy. - public Rune LRCornerHv { get; set; } = (Rune)'┛'; + public Rune LRCornerHv { get; init; } = (Rune)'┛'; /// Box Drawings Lower Right Corner - Double Vertical Single Horizontal. - public Rune LRCornerDblSingle { get; set; } = (Rune)'╜'; + public Rune LRCornerDblSingle { get; init; } = (Rune)'╜'; /// Box Drawings Lower Right Corner - Single Vertical Double Horizontal. - public Rune LRCornerSingleDbl { get; set; } = (Rune)'╛'; + public Rune LRCornerSingleDbl { get; init; } = (Rune)'╛'; /// Box Drawings Lower Right Corner - Light Vertical Heavy Horizontal. - public Rune LRCornerLtHv { get; set; } = (Rune)'┙'; + public Rune LRCornerLtHv { get; init; } = (Rune)'┙'; /// Box Drawings Lower Right Corner - Heavy Vertical Light Horizontal. - public Rune LRCornerHvLt { get; set; } = (Rune)'┚'; + public Rune LRCornerHvLt { get; init; } = (Rune)'┚'; /// Box Drawings Left Tee - Light. - public Rune LeftTee { get; set; } = (Rune)'├'; + public Rune LeftTee { get; init; } = (Rune)'├'; /// Box Drawings Left Tee - Single Vertical Double Horizontal. - public Rune LeftTeeDblH { get; set; } = (Rune)'╞'; + public Rune LeftTeeDblH { get; init; } = (Rune)'╞'; /// Box Drawings Left Tee - Double Vertical Single Horizontal. - public Rune LeftTeeDblV { get; set; } = (Rune)'╟'; + public Rune LeftTeeDblV { get; init; } = (Rune)'╟'; /// Box Drawings Left Tee - Double. - public Rune LeftTeeDbl { get; set; } = (Rune)'╠'; + public Rune LeftTeeDbl { get; init; } = (Rune)'╠'; /// Box Drawings Left Tee - Heavy Horizontal Light Vertical. - public Rune LeftTeeHvH { get; set; } = (Rune)'┝'; + public Rune LeftTeeHvH { get; init; } = (Rune)'┝'; /// Box Drawings Left Tee - Light Horizontal Heavy Vertical. - public Rune LeftTeeHvV { get; set; } = (Rune)'┠'; + public Rune LeftTeeHvV { get; init; } = (Rune)'┠'; /// Box Drawings Left Tee - Heavy. - public Rune LeftTeeHvDblH { get; set; } = (Rune)'┣'; + public Rune LeftTeeHvDblH { get; init; } = (Rune)'┣'; /// Box Drawings Right Tee - Light. - public Rune RightTee { get; set; } = (Rune)'┤'; + public Rune RightTee { get; init; } = (Rune)'┤'; /// Box Drawings Right Tee - Single Vertical Double Horizontal. - public Rune RightTeeDblH { get; set; } = (Rune)'╡'; + public Rune RightTeeDblH { get; init; } = (Rune)'╡'; /// Box Drawings Right Tee - Double Vertical Single Horizontal. - public Rune RightTeeDblV { get; set; } = (Rune)'╢'; + public Rune RightTeeDblV { get; init; } = (Rune)'╢'; /// Box Drawings Right Tee - Double. - public Rune RightTeeDbl { get; set; } = (Rune)'╣'; + public Rune RightTeeDbl { get; init; } = (Rune)'╣'; /// Box Drawings Right Tee - Heavy Horizontal Light Vertical. - public Rune RightTeeHvH { get; set; } = (Rune)'┥'; + public Rune RightTeeHvH { get; init; } = (Rune)'┥'; /// Box Drawings Right Tee - Light Horizontal Heavy Vertical. - public Rune RightTeeHvV { get; set; } = (Rune)'┨'; + public Rune RightTeeHvV { get; init; } = (Rune)'┨'; /// Box Drawings Right Tee - Heavy. - public Rune RightTeeHvDblH { get; set; } = (Rune)'┫'; + public Rune RightTeeHvDblH { get; init; } = (Rune)'┫'; /// Box Drawings Top Tee - Light. - public Rune TopTee { get; set; } = (Rune)'┬'; + public Rune TopTee { get; init; } = (Rune)'┬'; /// Box Drawings Top Tee - Single Vertical Double Horizontal. - public Rune TopTeeDblH { get; set; } = (Rune)'╤'; + public Rune TopTeeDblH { get; init; } = (Rune)'╤'; /// Box Drawings Top Tee - Double Vertical Single Horizontal. - public Rune TopTeeDblV { get; set; } = (Rune)'╥'; + public Rune TopTeeDblV { get; init; } = (Rune)'╥'; /// Box Drawings Top Tee - Double. - public Rune TopTeeDbl { get; set; } = (Rune)'╦'; + public Rune TopTeeDbl { get; init; } = (Rune)'╦'; /// Box Drawings Top Tee - Heavy Horizontal Light Vertical. - public Rune TopTeeHvH { get; set; } = (Rune)'┯'; + public Rune TopTeeHvH { get; init; } = (Rune)'┯'; /// Box Drawings Top Tee - Light Horizontal Heavy Vertical. - public Rune TopTeeHvV { get; set; } = (Rune)'┰'; + public Rune TopTeeHvV { get; init; } = (Rune)'┰'; /// Box Drawings Top Tee - Heavy. - public Rune TopTeeHvDblH { get; set; } = (Rune)'┳'; + public Rune TopTeeHvDblH { get; init; } = (Rune)'┳'; /// Box Drawings Bottom Tee - Light. - public Rune BottomTee { get; set; } = (Rune)'┴'; + public Rune BottomTee { get; init; } = (Rune)'┴'; /// Box Drawings Bottom Tee - Single Vertical Double Horizontal. - public Rune BottomTeeDblH { get; set; } = (Rune)'╧'; + public Rune BottomTeeDblH { get; init; } = (Rune)'╧'; /// Box Drawings Bottom Tee - Double Vertical Single Horizontal. - public Rune BottomTeeDblV { get; set; } = (Rune)'╨'; + public Rune BottomTeeDblV { get; init; } = (Rune)'╨'; /// Box Drawings Bottom Tee - Double. - public Rune BottomTeeDbl { get; set; } = (Rune)'╩'; + public Rune BottomTeeDbl { get; init; } = (Rune)'╩'; /// Box Drawings Bottom Tee - Heavy Horizontal Light Vertical. - public Rune BottomTeeHvH { get; set; } = (Rune)'┷'; + public Rune BottomTeeHvH { get; init; } = (Rune)'┷'; /// Box Drawings Bottom Tee - Light Horizontal Heavy Vertical. - public Rune BottomTeeHvV { get; set; } = (Rune)'┸'; + public Rune BottomTeeHvV { get; init; } = (Rune)'┸'; /// Box Drawings Bottom Tee - Heavy. - public Rune BottomTeeHvDblH { get; set; } = (Rune)'┻'; + public Rune BottomTeeHvDblH { get; init; } = (Rune)'┻'; /// Box Drawings Cross - Light. - public Rune Cross { get; set; } = (Rune)'┼'; + public Rune Cross { get; init; } = (Rune)'┼'; /// Box Drawings Cross - Single Vertical Double Horizontal. - public Rune CrossDblH { get; set; } = (Rune)'╪'; + public Rune CrossDblH { get; init; } = (Rune)'╪'; /// Box Drawings Cross - Double Vertical Single Horizontal. - public Rune CrossDblV { get; set; } = (Rune)'╫'; + public Rune CrossDblV { get; init; } = (Rune)'╫'; /// Box Drawings Cross - Double. - public Rune CrossDbl { get; set; } = (Rune)'╬'; + public Rune CrossDbl { get; init; } = (Rune)'╬'; /// Box Drawings Cross - Heavy Horizontal Light Vertical. - public Rune CrossHvH { get; set; } = (Rune)'┿'; + public Rune CrossHvH { get; init; } = (Rune)'┿'; /// Box Drawings Cross - Light Horizontal Heavy Vertical. - public Rune CrossHvV { get; set; } = (Rune)'╂'; + public Rune CrossHvV { get; init; } = (Rune)'╂'; /// Box Drawings Cross - Heavy. - public Rune CrossHv { get; set; } = (Rune)'╋'; + public Rune CrossHv { get; init; } = (Rune)'╋'; /// Shadow - Vertical Start. - public Rune ShadowVerticalStart { get; set; } = (Rune)'▖'; + public Rune ShadowVerticalStart { get; init; } = (Rune)'▖'; /// Shadow - Vertical. - public Rune ShadowVertical { get; set; } = (Rune)'▌'; + public Rune ShadowVertical { get; init; } = (Rune)'▌'; /// Shadow - Horizontal Start. - public Rune ShadowHorizontalStart { get; set; } = (Rune)'▝'; + public Rune ShadowHorizontalStart { get; init; } = (Rune)'▝'; /// Shadow - Horizontal. - public Rune ShadowHorizontal { get; set; } = (Rune)'▀'; + public Rune ShadowHorizontal { get; init; } = (Rune)'▀'; /// Shadow - Horizontal End. - public Rune ShadowHorizontalEnd { get; set; } = (Rune)'▘'; + public Rune ShadowHorizontalEnd { get; init; } = (Rune)'▘'; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static GlyphSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static GlyphSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static GlyphSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static GlyphSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/HexViewSettings.cs b/Terminal.Gui/Configuration/Settings/HexViewSettings.cs index 10c7721a3e..9e255aa41b 100644 --- a/Terminal.Gui/Configuration/Settings/HexViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/HexViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class HexViewSettings +public sealed record HexViewSettings { - /// Gets or sets the default cursor style for hex views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for hex views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static HexViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static HexViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static HexViewSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static HexViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/JsonMerge.cs b/Terminal.Gui/Configuration/Settings/JsonMerge.cs new file mode 100644 index 0000000000..0ab7b23485 --- /dev/null +++ b/Terminal.Gui/Configuration/Settings/JsonMerge.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Nodes; + +namespace Terminal.Gui.Configuration; + +/// +/// Deep-merges JSON objects: object values merge recursively; any other value — including arrays — +/// replaces the target wholesale. This gives configuration sources atomic array overrides (a +/// higher-priority source's shorter array fully replaces a lower-priority source's array), unlike +/// MEC's per-index key merge. +/// +internal static class JsonMerge +{ + /// Merges into . Source nodes are cloned. + public static void DeepMerge (JsonObject target, JsonObject source) + { + foreach (KeyValuePair pair in source) + { + if (target [pair.Key] is JsonObject existing && pair.Value is JsonObject incoming) + { + DeepMerge (existing, incoming); + + continue; + } + + target [pair.Key] = pair.Value?.DeepClone (); + } + } +} diff --git a/Terminal.Gui/Configuration/Settings/KeyBindingConfiguration.cs b/Terminal.Gui/Configuration/Settings/KeyBindingConfiguration.cs new file mode 100644 index 0000000000..756c3a6ccc --- /dev/null +++ b/Terminal.Gui/Configuration/Settings/KeyBindingConfiguration.cs @@ -0,0 +1,174 @@ +using System.Text.Json.Nodes; +using Terminal.Gui.App; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; + +namespace Terminal.Gui.Configuration; + +/// +/// Overlays nested JSON key-binding dictionaries onto +/// , , +/// and . Unmentioned commands keep hard-coded defaults. +/// Each apply first reverts the previous configuration overlay, so a binding removed from +/// configuration regains its prior value on reload instead of persisting until restart. +/// +internal static class KeyBindingConfiguration +{ + private static Dictionary? _appPrior; + private static Dictionary? _viewPrior; + private static Dictionary>? _viewTypesPrior; + + /// Applies key-binding sections from the raw-JSON merged source view to the static facades. + public static void Apply (JsonObject mergedJson) + { + Application.DefaultKeyBindings = OverlayTracked ( + Application.DefaultKeyBindings, + BindCommands (mergedJson, "Application"), + ref _appPrior); + + View.DefaultKeyBindings = OverlayTracked ( + View.DefaultKeyBindings, + BindCommands (mergedJson, "View"), + ref _viewPrior); + + OverlayViewKeyBindings (BindViewTypes (mergedJson)); + } + + /// INTERNAL: Captures the overlay-revert tracking state (for test snapshot/restore). + internal static (Dictionary?, Dictionary?, + Dictionary>?) SnapshotOverlayTracking () => + (_appPrior is null ? null : new (_appPrior), + _viewPrior is null ? null : new (_viewPrior), + _viewTypesPrior?.ToDictionary (p => p.Key, p => new Dictionary (p.Value), StringComparer.OrdinalIgnoreCase)); + + /// INTERNAL: Restores overlay-revert tracking state captured by . + internal static void RestoreOverlayTracking ( + (Dictionary?, Dictionary?, + Dictionary>?) state) + { + (_appPrior, _viewPrior, _viewTypesPrior) = state; + } + + private static Dictionary? BindCommands (JsonObject mergedJson, string sectionName) => + ConfigurationSectionJson.Deserialize> ( + mergedJson [sectionName]? ["DefaultKeyBindings"] as JsonObject, + $"Key bindings ({sectionName}:DefaultKeyBindings)"); + + private static Dictionary>? BindViewTypes (JsonObject mergedJson) => + ConfigurationSectionJson.Deserialize>> ( + mergedJson ["View"]? ["ViewKeyBindings"] as JsonObject, + "ViewKeyBindings (View:ViewKeyBindings)"); + + /// + /// Reverts the previous configuration overlay recorded in , then overlays + /// and records what each overlaid command replaced. App-code mutations to + /// commands the configuration does not mention are preserved. + /// + private static Dictionary OverlayTracked ( + Dictionary? current, + Dictionary? overlay, + ref Dictionary? prior) + { + Dictionary merged = current is { } existing ? new (existing) : []; + + if (prior is { }) + { + foreach (KeyValuePair pair in prior) + { + if (pair.Value is { } previous) + { + merged [pair.Key] = previous; + } + else + { + merged.Remove (pair.Key); + } + } + } + + prior = null; + + if (overlay is null || overlay.Count == 0) + { + return merged; + } + + prior = []; + + foreach (KeyValuePair pair in overlay) + { + prior [pair.Key] = merged.TryGetValue (pair.Key, out PlatformKeyBinding? replaced) ? replaced : null; + merged [pair.Key] = pair.Value; + } + + return merged; + } + + private static void OverlayViewKeyBindings (Dictionary>? overlay) + { + Dictionary> merged = + View.ViewKeyBindings is { } existing + ? existing.ToDictionary (p => p.Key, p => new Dictionary (p.Value), StringComparer.OrdinalIgnoreCase) + : new (StringComparer.OrdinalIgnoreCase); + + if (_viewTypesPrior is { }) + { + foreach (KeyValuePair> typePair in _viewTypesPrior) + { + if (!merged.TryGetValue (typePair.Key, out Dictionary? typeBindings)) + { + continue; + } + + foreach (KeyValuePair pair in typePair.Value) + { + if (pair.Value is { } previous) + { + typeBindings [pair.Key] = previous; + } + else + { + typeBindings.Remove (pair.Key); + } + } + + if (typeBindings.Count == 0) + { + merged.Remove (typePair.Key); + } + } + } + + _viewTypesPrior = null; + + if (overlay is null || overlay.Count == 0) + { + View.ViewKeyBindings = merged.Count == 0 ? null : merged; + + return; + } + + _viewTypesPrior = new (StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair> typePair in overlay) + { + if (!merged.TryGetValue (typePair.Key, out Dictionary? typeBindings)) + { + typeBindings = []; + merged [typePair.Key] = typeBindings; + } + + Dictionary priors = []; + + foreach (KeyValuePair pair in typePair.Value) + { + priors [pair.Key] = typeBindings.TryGetValue (pair.Key, out PlatformKeyBinding? replaced) ? replaced : null; + typeBindings [pair.Key] = pair.Value; + } + + _viewTypesPrior [typePair.Key] = priors; + } + + View.ViewKeyBindings = merged; + } +} diff --git a/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs b/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs index 5c155dc3f9..5b69119ce2 100644 --- a/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs +++ b/Terminal.Gui/Configuration/Settings/LinearRangeSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class LinearRangeSettings +public sealed record LinearRangeSettings { - /// Gets or sets the default cursor style for linear range views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBlock; + /// Gets the default cursor style for linear range views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBlock; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static LinearRangeSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static LinearRangeSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static LinearRangeSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static LinearRangeSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MecSchemeManager.cs b/Terminal.Gui/Configuration/Settings/MecSchemeManager.cs deleted file mode 100644 index b8dd80a3ec..0000000000 --- a/Terminal.Gui/Configuration/Settings/MecSchemeManager.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Terminal.Gui.Configuration; - -/// -/// MEC-backed implementation of . -/// During the transition period, this delegates to the existing static -/// for scheme data. -/// -public class MecSchemeManager : ISchemeManager -{ - /// - public IReadOnlyList SchemeNames - { - get - { - try - { - return SchemeManager.GetSchemeNames ().ToList (); - } - catch - { - return []; - } - } - } - - /// - public Scheme? GetScheme (string name) - { - try - { - return SchemeManager.GetScheme (name); - } - catch - { - return null; - } - } - - /// - public void AddScheme (string name, Scheme scheme) - { - SchemeManager.AddScheme (name, scheme); - } -} diff --git a/Terminal.Gui/Configuration/Settings/MecThemeManager.cs b/Terminal.Gui/Configuration/Settings/MecThemeManager.cs index 29256b3499..0accf6f521 100644 --- a/Terminal.Gui/Configuration/Settings/MecThemeManager.cs +++ b/Terminal.Gui/Configuration/Settings/MecThemeManager.cs @@ -1,12 +1,8 @@ -#pragma warning disable CS0618 // Obsolete - MecThemeManager forwards from legacy ThemeManager during transition - namespace Terminal.Gui.Configuration; /// -/// MEC-backed implementation of . -/// During the transition period (PR #5411), this delegates writes to the legacy static -/// because the runtime theme/scheme dictionary is still owned by . -/// The Phase A2 work in #5416 will let this type own the theme/scheme data directly. +/// Per-builder . Process-wide callers use the static +/// facade (ThemeManager.Theme =). /// public class MecThemeManager : IThemeManager { @@ -16,35 +12,20 @@ public class MecThemeManager : IThemeManager /// Initializes a new instance of . public MecThemeManager (TuiConfigurationBuilder builder) { _builder = builder; } - private void OnLegacyThemeChanged (object? sender, App.EventArgs e) => _themeChanged?.Invoke (this, e); + private void OnStaticThemeChanged (object? sender, App.EventArgs e) => _themeChanged?.Invoke (this, e); /// public string CurrentThemeName => ThemeSettings.Defaults.Theme; /// - public IReadOnlyList ThemeNames - { - get - { - // During transition, delegate to existing ThemeManager - try - { - return ThemeManager.GetThemeNames (); - } - catch (InvalidOperationException) - { - return [ThemeManager.DEFAULT_THEME_NAME]; - } - } - } + public IReadOnlyList ThemeNames => ThemeCatalog.GetNames (_builder.Configuration); private EventHandler>? _themeChanged; /// /// - /// Forwarding from the legacy static event is wired up only while - /// this instance has at least one subscriber. This avoids leaking the instance through the static event - /// (which would otherwise keep every alive for the lifetime of the process). + /// Forwards only while this instance has a subscriber, + /// so unused instances are not kept alive by the static event. /// public event EventHandler>? ThemeChanged { @@ -54,7 +35,7 @@ public event EventHandler>? ThemeChanged { if (_themeChanged is null) { - ThemeManager.ThemeChanged += OnLegacyThemeChanged; + ThemeManager.ThemeChanged += OnStaticThemeChanged; } _themeChanged += value; @@ -68,49 +49,12 @@ public event EventHandler>? ThemeChanged if (_themeChanged is null) { - ThemeManager.ThemeChanged -= OnLegacyThemeChanged; + ThemeManager.ThemeChanged -= OnStaticThemeChanged; } } } } /// - public bool SwitchTheme (string themeName) - { - if (string.IsNullOrEmpty (themeName)) - { - return false; - } - - // Verify the theme exists before switching - if (!ThemeNames.Contains (themeName)) - { - return false; - } - - // During transition, also update the existing ThemeManager. Its setter raises - // ThemeManager.ThemeChanged, which is forwarded to subscribers via OnLegacyThemeChanged - // (only while this instance has subscribers; see the ThemeChanged event above). - try - { - ThemeManager.Theme = themeName; - } - catch (InvalidOperationException) - { - return false; - } - catch (KeyNotFoundException) - { - return false; - } - - // Re-apply all ThemeScope POCOs from configuration. This may rebind the scalar "Theme" key - // from config, so the switched theme is recorded afterwards to ensure the selection persists. - _builder.ApplyToStaticFacades (); - - // Record the switched theme on success, overriding any value the config re-apply may have set. - ThemeSettings.Defaults.Theme = themeName; - - return true; - } + public bool SwitchTheme (string themeName) => _builder.TrySwitchTheme (themeName); } diff --git a/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs b/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs index 342497936a..95c2248b48 100644 --- a/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MenuBarSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults. +/// Immutable settings record for defaults. /// -public class MenuBarSettings +public sealed record MenuBarSettings { - /// Gets or sets the default border style for menu bars. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.None; + /// Gets the default border style for menu bars. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.None; - /// Gets or sets the default activation key for menu bars. - public Key DefaultKey { get; set; } = Key.F10; + /// Gets the default activation key for menu bars. + public Key DefaultKey { get; init; } = Key.F10; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MenuBarSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MenuBarSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MenuBarSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static MenuBarSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MenuSettings.cs b/Terminal.Gui/Configuration/Settings/MenuSettings.cs index 65fdc8f7ec..60ecf14eff 100644 --- a/Terminal.Gui/Configuration/Settings/MenuSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MenuSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class MenuSettings +public sealed record MenuSettings { - /// Gets or sets the default border style for menus. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.None; + /// Gets the default border style for menus. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.None; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MenuSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MenuSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MenuSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static MenuSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs b/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs index 4e5ff7af13..fdbf6297fa 100644 --- a/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs +++ b/Terminal.Gui/Configuration/Settings/MessageBoxSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class MessageBoxSettings +public sealed record MessageBoxSettings { - /// Gets or sets the default border style for message boxes. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; + /// Gets the default border style for message boxes. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Heavy; - /// Gets or sets the default button alignment for message boxes. - public Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; + /// Gets the default button alignment for message boxes. + public Alignment DefaultButtonAlignment { get; init; } = Alignment.Center; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static MessageBoxSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static MessageBoxSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static MessageBoxSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static MessageBoxSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs b/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs index b5a4a287c3..0bac34c5a4 100644 --- a/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs +++ b/Terminal.Gui/Configuration/Settings/NerdFontsSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class NerdFontsSettings +public sealed record NerdFontsSettings { - /// Gets or sets whether Nerd Fonts glyphs are enabled. - public bool Enable { get; set; } = false; + /// Gets whether Nerd Fonts glyphs are enabled. + public bool Enable { get; init; } = false; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static NerdFontsSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static NerdFontsSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static NerdFontsSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static NerdFontsSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs b/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs index 6cb41b741d..36250870c1 100644 --- a/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs +++ b/Terminal.Gui/Configuration/Settings/PopoverMenuSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (SettingsScope). +/// Immutable settings record for defaults (SettingsScope). /// -public class PopoverMenuSettings +public sealed record PopoverMenuSettings { - /// Gets or sets the default activation key for popover menus. - public Key DefaultKey { get; set; } = Key.F10.WithShift; + /// Gets the default activation key for popover menus. + public Key DefaultKey { get; init; } = Key.F10.WithShift; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static PopoverMenuSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static PopoverMenuSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static PopoverMenuSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static PopoverMenuSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs b/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs index 9c00011337..80c0af365c 100644 --- a/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs +++ b/Terminal.Gui/Configuration/Settings/SelectorBaseSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class SelectorBaseSettings +public sealed record SelectorBaseSettings { - /// Gets or sets the default mouse highlight states for selectors. - public MouseState DefaultMouseHighlightStates { get; set; } = MouseState.In; + /// Gets the default mouse highlight states for selectors. + public MouseState DefaultMouseHighlightStates { get; init; } = MouseState.In; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static SelectorBaseSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static SelectorBaseSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static SelectorBaseSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static SelectorBaseSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs b/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs index 51932645ce..02c2faa30d 100644 --- a/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs +++ b/Terminal.Gui/Configuration/Settings/StatusBarSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class StatusBarSettings +public sealed record StatusBarSettings { - /// Gets or sets the default separator line style for status bars. - public LineStyle DefaultSeparatorLineStyle { get; set; } = LineStyle.Single; + /// Gets the default separator line style for status bars. + public LineStyle DefaultSeparatorLineStyle { get; init; } = LineStyle.Single; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static StatusBarSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static StatusBarSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static StatusBarSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static StatusBarSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs b/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs index c0caf63bf7..cc74959425 100644 --- a/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs +++ b/Terminal.Gui/Configuration/Settings/TextFieldSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class TextFieldSettings +public sealed record TextFieldSettings { - /// Gets or sets the default cursor style for text fields. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBar; + /// Gets the default cursor style for text fields. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBar; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static TextFieldSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static TextFieldSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static TextFieldSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static TextFieldSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/TextViewSettings.cs b/Terminal.Gui/Configuration/Settings/TextViewSettings.cs index 5505dedb2b..3ba2ac74d3 100644 --- a/Terminal.Gui/Configuration/Settings/TextViewSettings.cs +++ b/Terminal.Gui/Configuration/Settings/TextViewSettings.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for defaults (ThemeScope). +/// Immutable settings record for defaults (ThemeScope). /// -public class TextViewSettings +public sealed record TextViewSettings { - /// Gets or sets the default cursor style for text views. - public CursorStyle DefaultCursorStyle { get; set; } = CursorStyle.BlinkingBar; + /// Gets the default cursor style for text views. + public CursorStyle DefaultCursorStyle { get; init; } = CursorStyle.BlinkingBar; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static TextViewSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static TextViewSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static TextViewSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static TextViewSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/Settings/ThemeCatalog.cs b/Terminal.Gui/Configuration/Settings/ThemeCatalog.cs new file mode 100644 index 0000000000..a00e4e4ff6 --- /dev/null +++ b/Terminal.Gui/Configuration/Settings/ThemeCatalog.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Configuration; + +namespace Terminal.Gui.Configuration; + +/// +/// Nested Themes dictionary helpers. Legacy array-of-single-key (pre-MEC) sources are +/// skipped at add time (see ); convert those files with +/// Tools/MigrateConfig. +/// +internal static class ThemeCatalog +{ + /// + /// Gets theme names from a nested Themes object, with first. + /// + public static IReadOnlyList GetNames (IConfiguration config) + { + List names = []; + + foreach (IConfigurationSection child in config.GetSection ("Themes").GetChildren ()) + { + if (!names.Contains (child.Key, StringComparer.OrdinalIgnoreCase)) + { + names.Add (child.Key); + } + } + + names.RemoveAll (n => string.Equals (n, ThemeManager.DEFAULT_THEME_NAME, StringComparison.OrdinalIgnoreCase)); + names.Insert (0, ThemeManager.DEFAULT_THEME_NAME); + + return names; + } + + /// + /// Resolves to the canonical key in Themes, or + /// if it is not a nested theme name. + /// + public static string? CanonicalName (IConfiguration config, string themeName) + { + if (string.IsNullOrEmpty (themeName)) + { + return null; + } + + return GetNames (config) + .FirstOrDefault (n => string.Equals (n, themeName, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets the nested Themes:{name} section, or if missing. + /// + public static IConfigurationSection? Find (IConfiguration config, string themeName) + { + string? canonical = CanonicalName (config, themeName); + + if (canonical is null) + { + return null; + } + + IConfigurationSection named = config.GetSection ("Themes").GetSection (canonical); + + return named.Exists () ? named : null; + } +} diff --git a/Terminal.Gui/Configuration/Settings/ThemeChanges.cs b/Terminal.Gui/Configuration/Settings/ThemeChanges.cs index cef9969c42..10c7ac4eff 100644 --- a/Terminal.Gui/Configuration/Settings/ThemeChanges.cs +++ b/Terminal.Gui/Configuration/Settings/ThemeChanges.cs @@ -1,40 +1,18 @@ -#pragma warning disable CS0618 // Obsolete - bridges legacy ConfigurationManager.Applied and ThemeManager.ThemeChanged into the MEC-facing facade - namespace Terminal.Gui.Configuration; /// -/// Static facade that raises when the active theme — or any setting that affects views -/// reading from *Settings.Defaults — has changed. Provided for consumers that cannot -/// take a dependency (typically subclasses -/// constructed before an is available). +/// Static facade that raises when the active theme has changed. Provided for consumers that cannot +/// take an dependency (typically subclasses). /// -/// -/// -/// This class is the MEC-friendly replacement for the obsolete -/// ConfigurationManager.Applied event subscriptions in subclasses. -/// It raises after any settings re-application (theme switch, runtime config reload, -/// ConfigurationManager.Reset(), etc.) so that handlers re-read their static defaults. -/// -/// -/// The bridge to the legacy events is established the first time this class is referenced; -/// the subscription persists for the lifetime of the process and never needs explicit teardown. -/// -/// public static class ThemeChanges { static ThemeChanges () { - // ConfigurationManager.Apply() writes ConfigProperty values directly (bypassing the - // ThemeManager.Theme setter), so the broad Applied event is required to catch the - // most common change path (Init, Reset, RuntimeConfig). - ConfigurationManager.Applied += (_, _) => ThemeChanged?.Invoke (null, new App.EventArgs (ThemeManager.GetCurrentThemeName ())); - - // The narrow event also forwards, so explicit ThemeManager.Theme = X calls still surface here. ThemeManager.ThemeChanged += (_, e) => ThemeChanged?.Invoke (null, e); } /// - /// Raised after the active theme — or any setting affecting *Settings.Defaults — has changed. + /// Raised after the active theme has changed. /// The is the name of the currently-active theme. /// public static event EventHandler>? ThemeChanged; diff --git a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs index a8f8176ddb..aa2af890b4 100644 --- a/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs +++ b/Terminal.Gui/Configuration/Settings/TuiConfigurationBuilder.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json.Nodes; using Microsoft.Extensions.Configuration; namespace Terminal.Gui.Configuration; @@ -10,7 +11,7 @@ namespace Terminal.Gui.Configuration; /// /// /// -/// This is the MEC-based replacement for the legacy . +/// This is the MEC-based configuration entry point. /// It provides the same multi-source precedence (library defaults → app defaults → user files /// → environment variables → runtime config) using standard Microsoft.Extensions.Configuration. /// @@ -45,15 +46,39 @@ namespace Terminal.Gui.Configuration; /// public class TuiConfigurationBuilder { + /// + /// Process-wide builder used by and module initialization. + /// + public static TuiConfigurationBuilder Shared { get; } = new (); + private readonly string? _appName; + private readonly string? _currentDirectory; + private readonly bool _includeUserSources; private string? _runtimeConfig; private IConfiguration? _configuration; + private JsonObject? _mergedSourceJson; /// Initializes a new instance of . /// The application name for app-specific config file discovery. If null, uses entry assembly name. - public TuiConfigurationBuilder (string? appName = null) + /// + /// The directory ./.tui/ config files are resolved against. If null, uses + /// . + /// + public TuiConfigurationBuilder (string? appName = null, string? currentDirectory = null) + : this (appName, currentDirectory, includeUserSources: true) + { } + + /// + /// INTERNAL: Also controls whether user files (~/.tui, ./.tui) and the + /// TUI_CONFIG environment variable are loaded. Tests pass + /// so machine-local + /// configuration cannot change test outcomes. + /// + internal TuiConfigurationBuilder (string? appName, string? currentDirectory, bool includeUserSources) { _appName = appName ?? System.Reflection.Assembly.GetEntryAssembly ()?.GetName ().Name; + _currentDirectory = currentDirectory; + _includeUserSources = includeUserSources; } /// @@ -79,21 +104,74 @@ public string? RuntimeConfig /// /// Builds the configuration from all sources in precedence order. /// + /// + /// A malformed source must never crash the app — the build runs from a module initializer. + /// Per-source failures are collected in and skipped; if the build + /// still fails, the library's embedded defaults are used and the error is printed at shutdown. + /// /// The built configuration root. public IConfiguration Build () { - IConfigurationBuilder builder = new ConfigurationBuilder () - .AddTuiLibraryDefaults () - .AddTuiAppDefaults (_appName) - .AddTuiUserFiles (_appName) - .AddTuiEnvironmentVariable () - .AddTuiRuntimeConfig (_runtimeConfig); + // Errors describe the current effective configuration; without this, a watcher re-applying a + // persistently malformed source would re-add the identical error on every rebuild. + TuiJsonErrors.Clear (); + + List jsonSources = []; + IConfigurationBuilder builder = new ConfigurationBuilder (); + TuiConfigurationExtensions.AddTuiLibraryDefaults (builder, jsonSources); + TuiConfigurationExtensions.AddTuiAppDefaults (builder, _appName, jsonSources); + + if (_includeUserSources) + { + TuiConfigurationExtensions.AddTuiUserFiles (builder, _appName, _currentDirectory, jsonSources); + TuiConfigurationExtensions.AddTuiEnvironmentVariable (builder, jsonSources); + } + + TuiConfigurationExtensions.AddTuiRuntimeConfig (builder, _runtimeConfig, jsonSources); + + try + { + _configuration = builder.Build (); + } + catch (Exception ex) + { + TuiJsonErrors.Add ($"Configuration build failed: {ex.Message}. Falling back to library defaults."); + _configuration = new ConfigurationBuilder ().AddTuiLibraryDefaults ().Build (); + } + + JsonObject merged = []; + + foreach (JsonObject source in jsonSources) + { + JsonMerge.DeepMerge (merged, source); + } - _configuration = builder.Build (); + _mergedSourceJson = merged; return _configuration; } + /// + /// Gets a raw-JSON deep merge of all sources (arrays replace wholesale, unlike MEC's per-index + /// merge). Used for sections bound via STJ — key bindings and schemes — where a higher-priority + /// source's array must atomically replace a lower-priority source's. + /// + internal JsonObject MergedSourceJson + { + get + { + _ = Configuration; + + return _mergedSourceJson!; + } + } + + /// + /// Invalidates the cached configuration so the next access to rebuilds + /// from all sources. Use after a configuration file changes on disk. + /// + public void Reload () => _configuration = null; + /// /// Gets the MEC-backed theme manager instance for this builder. /// @@ -103,58 +181,116 @@ public IConfiguration Build () /// /// Gets the MEC-backed scheme manager instance for this builder. /// - public ISchemeManager SchemeManager => _schemeManager ??= new MecSchemeManager (); + public ISchemeManager SchemeManager => _schemeManager ??= new SchemeManager (); private ISchemeManager? _schemeManager; /// - /// Applies the current configuration to all static settings facades. - /// Call this after building or rebuilding to push MEC values to the static Defaults properties. + /// Applies configuration sources to SettingsScope facades, then publishes the active theme's overlays. + /// Always raises afterwards — the facades were re-published + /// wholesale, so subscribers must re-render even when the theme name is unchanged (e.g. a hot reload + /// that edits the current theme's colors). /// public void ApplyToStaticFacades () { IConfiguration config = Configuration; - - // ThemeSettings is special: the active theme is a scalar "Theme" key, not a nested section. BindThemeScalar (config); + ApplicationSettings.Defaults = BindSection (config, "Application"); + DriverSettings.Defaults = BindSection (config, "Driver"); + FileDialogSettings.Defaults = BindSection (config, "FileDialog"); + FileDialogStyleSettings.Defaults = BindSection (config, "FileDialogStyle"); + KeySettings.Defaults = BindSection (config, "Key"); + TraceSettings.Defaults = BindSection (config, "Trace"); + KeyBindingConfiguration.Apply (MergedSourceJson); + ApplyActiveThemeOverlays (); + global::Terminal.Gui.Configuration.ThemeManager.RaiseThemeChanged (ThemeSettings.Defaults.Theme); + } - // SettingsScope POCOs - BindSection (config, "Application", s => ApplicationSettings.Defaults = s); - BindSection (config, "Driver", s => DriverSettings.Defaults = s); - BindSection (config, "FileDialog", s => FileDialogSettings.Defaults = s); - BindSection (config, "FileDialogStyle", s => FileDialogStyleSettings.Defaults = s); - BindSection (config, "Key", s => KeySettings.Defaults = s); - BindSection (config, "Trace", s => TraceSettings.Defaults = s); - - // ThemeScope POCOs - BindSection (config, "Button", s => ButtonSettings.Defaults = s); - BindSection (config, "CheckBox", s => CheckBoxSettings.Defaults = s); - BindSection (config, "CharMap", s => CharMapSettings.Defaults = s); - BindSection (config, "Dialog", s => DialogSettings.Defaults = s); - BindSection (config, "FrameView", s => FrameViewSettings.Defaults = s); - BindSection (config, "HexView", s => HexViewSettings.Defaults = s); - BindSection (config, "LinearRange", s => LinearRangeSettings.Defaults = s); - BindSection (config, "MenuBar", s => MenuBarSettings.Defaults = s); - BindSection (config, "Menu", s => MenuSettings.Defaults = s); - BindSection (config, "MessageBox", s => MessageBoxSettings.Defaults = s); - BindSection (config, "NerdFonts", s => NerdFontsSettings.Defaults = s); - BindSection (config, "PopoverMenu", s => PopoverMenuSettings.Defaults = s); - BindSection (config, "SelectorBase", s => SelectorBaseSettings.Defaults = s); - BindSection (config, "StatusBar", s => StatusBarSettings.Defaults = s); - BindSection (config, "TextField", s => TextFieldSettings.Defaults = s); - BindSection (config, "TextView", s => TextViewSettings.Defaults = s); - BindSection (config, "Window", s => WindowSettings.Defaults = s); - BindSection (config, "Glyphs", s => GlyphSettings.Defaults = s); + /// + /// Publishes ThemeScope Current values and schemes for .Theme. + /// Does not re-read the Theme scalar from configuration. + /// + public void ApplyActiveThemeOverlays () + { + IConfiguration config = Configuration; + string activeTheme = ThemeSettings.Defaults.Theme ?? global::Terminal.Gui.Configuration.ThemeManager.DEFAULT_THEME_NAME; + + ButtonSettings button = BindThemeScope (config, "Button", activeTheme); + CheckBoxSettings checkBox = BindThemeScope (config, "CheckBox", activeTheme); + CharMapSettings charMap = BindThemeScope (config, "CharMap", activeTheme); + DialogSettings dialog = BindThemeScope (config, "Dialog", activeTheme); + FrameViewSettings frameView = BindThemeScope (config, "FrameView", activeTheme); + HexViewSettings hexView = BindThemeScope (config, "HexView", activeTheme); + LinearRangeSettings linearRange = BindThemeScope (config, "LinearRange", activeTheme); + MenuBarSettings menuBar = BindThemeScope (config, "MenuBar", activeTheme); + MenuSettings menu = BindThemeScope (config, "Menu", activeTheme); + MessageBoxSettings messageBox = BindThemeScope (config, "MessageBox", activeTheme); + NerdFontsSettings nerdFonts = BindThemeScope (config, "NerdFonts", activeTheme); + PopoverMenuSettings popoverMenu = BindThemeScope (config, "PopoverMenu", activeTheme); + SelectorBaseSettings selectorBase = BindThemeScope (config, "SelectorBase", activeTheme); + StatusBarSettings statusBar = BindThemeScope (config, "StatusBar", activeTheme); + TextFieldSettings textField = BindThemeScope (config, "TextField", activeTheme); + TextViewSettings textView = BindThemeScope (config, "TextView", activeTheme); + WindowSettings window = BindThemeScope (config, "Window", activeTheme); + GlyphSettings glyphs = BindThemeScope (config, "Glyphs", activeTheme); + + ButtonSettings.Current = button; + CheckBoxSettings.Current = checkBox; + CharMapSettings.Current = charMap; + DialogSettings.Current = dialog; + FrameViewSettings.Current = frameView; + HexViewSettings.Current = hexView; + LinearRangeSettings.Current = linearRange; + MenuBarSettings.Current = menuBar; + MenuSettings.Current = menu; + MessageBoxSettings.Current = messageBox; + NerdFontsSettings.Current = nerdFonts; + PopoverMenuSettings.Current = popoverMenu; + SelectorBaseSettings.Current = selectorBase; + StatusBarSettings.Current = statusBar; + TextFieldSettings.Current = textField; + TextViewSettings.Current = textView; + WindowSettings.Current = window; + GlyphSettings.Current = glyphs; + + string? canonicalTheme = ThemeCatalog.CanonicalName (config, activeTheme); + global::Terminal.Gui.Configuration.SchemeManager.ApplyFromConfiguration (MergedSourceJson, canonicalTheme); + } + + /// + /// Sets the active theme, publishes overlays, and raises . + /// Unknown names are ignored. + /// + /// if the theme exists (already active or newly applied). + internal bool TrySwitchTheme (string themeName) + { + string? canonical = ThemeCatalog.CanonicalName (Configuration, themeName); + + if (canonical is null) + { + return false; + } + + if (string.Equals (ThemeSettings.Defaults.Theme, canonical, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + ThemeSettings.Defaults = new () { Theme = canonical }; + ApplyActiveThemeOverlays (); + global::Terminal.Gui.Configuration.ThemeManager.RaiseThemeChanged (canonical); + + return true; } /// /// Binds a custom application settings section from the configuration to a POCO instance. - /// This is the MEC replacement for the legacy . + /// This is the MEC replacement for application-specific settings sections. /// /// The settings POCO type. /// The JSON section name to bind from. /// Action to apply the bound settings (typically update a static Defaults property). /// This builder for chaining. - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple types preserved by DynamicDependency in ConfigPropertyHostTypes.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple public types; Bind only walks declared properties.")] [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Settings POCOs are simple types; no generic instantiation needed at runtime.")] public TuiConfigurationBuilder BindAppSettings (string sectionName, Action apply) where T : new () { @@ -165,36 +301,67 @@ public void ApplyToStaticFacades () return this; } - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple types preserved by DynamicDependency in ConfigPropertyHostTypes.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple public types; Bind only walks declared properties.")] [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Settings POCOs are simple types; no generic instantiation needed at runtime.")] - private static void BindSection<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] T> (IConfiguration config, string sectionName, Action apply) where T : new () + private static T BindSection<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] T> (IConfiguration config, string sectionName) where T : new () { T settings = new (); IConfigurationSection section = config.GetSection (sectionName); - if (section.Exists ()) + if (!section.Exists ()) + { + return settings; + } + + section.Bind (settings); + BindDirectProperties (section, settings); + + return settings; + } + + /// + /// Two-pass overlay bind for ThemeScope POCOs. Binds the nested root section, then overlays + /// Themes::. + /// + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple public types; Bind only walks declared properties.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Settings POCOs are simple types; no generic instantiation needed at runtime.")] + private static T BindThemeScope<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] T> (IConfiguration config, string sectionName, string activeTheme) where T : new () + { + T settings = BindSection (config, sectionName); + IConfigurationSection? themeObject = ThemeCatalog.Find (config, activeTheme); + + if (themeObject is null) { - // Nested object format: { "Driver": { "Force16Colors": true } } - section.Bind (settings); + return settings; } - else + + IConfigurationSection overlay = themeObject.GetSection (sectionName); + + if (!overlay.Exists ()) { - // Flat dotted-key format: { "Driver.Force16Colors": true }. The MEC JSON provider stores - // these literally (a dot is not a section separator), so map them to properties by hand. - BindFlatDottedKeys (config, sectionName, settings); + return settings; } - apply (settings); + overlay.Bind (settings); + BindDirectProperties (overlay, settings); + + return settings; } /// /// Binds the scalar Theme key from configuration to . - /// Unlike other settings, the active theme is a scalar value (e.g. "Dark"), not a nested section. + /// Accepts either a root scalar ("Theme": "Dark") or a nested section + /// ("Theme": { "Theme": "Dark" }). /// private static void BindThemeScalar (IConfiguration config) { string? themeValue = config ["Theme"]; + if (string.IsNullOrEmpty (themeValue)) + { + themeValue = config.GetSection ("Theme") ["Theme"]; + } + if (string.IsNullOrEmpty (themeValue)) { return; @@ -204,14 +371,13 @@ private static void BindThemeScalar (IConfiguration config) } /// - /// Binds flat dotted keys (e.g. Driver.Force16Colors) from the configuration root to the - /// corresponding properties on the settings POCO. 's public properties are - /// preserved for trimming via the on the type parameter. + /// Binds scalar properties whose names match keys on (no dotted prefix). + /// Used after Bind so types MEC does not convert (notably ) still apply. /// - private static void BindFlatDottedKeys<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] T> (IConfiguration config, string sectionName, T settings) + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Settings POCOs are simple public types; Bind only walks declared properties.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Settings POCOs are simple types; no generic instantiation needed at runtime.")] + private static void BindDirectProperties<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicProperties)] T> (IConfiguration section, T settings) { - string prefix = sectionName + "."; - foreach (PropertyInfo prop in typeof (T).GetProperties (BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanWrite) @@ -219,7 +385,7 @@ private static void BindThemeScalar (IConfiguration config) continue; } - string? value = config [prefix + prop.Name]; + string? value = section [prop.Name]; if (value is null) { @@ -248,7 +414,7 @@ private static void BindThemeScalar (IConfiguration config) /// TypeDescriptor.GetConverter (which is / /// and breaks NativeAOT/trimmed consumers). New non-scalar /// settings property types must be added here explicitly. Unsupported types return - /// and are skipped by . + /// and are skipped by . /// private static object? ConvertValue (string value, Type targetType) { @@ -269,7 +435,20 @@ private static void BindThemeScalar (IConfiguration config) if (targetType == typeof (Rune)) { - return value.Length > 0 ? new Rune (value [0]) : new Rune ('+'); + // Glyph forms first ("6" is the glyph '6', "U+2611" is ☑). Multi-digit strings are legacy + // JSON-number codepoints flattened to strings by MEC (e.g. "9733" is ★); the glyph parser + // rejects them, so they fall through to the codepoint parse. + if (RuneJsonConverter.TryParse (value, out Rune rune)) + { + return rune; + } + + if (uint.TryParse (value, out uint codePoint) && Rune.IsValid (codePoint)) + { + return new Rune (codePoint); + } + + return null; } if (targetType == typeof (Key)) diff --git a/Terminal.Gui/Configuration/Settings/TuiConfigurationExtensions.cs b/Terminal.Gui/Configuration/Settings/TuiConfigurationExtensions.cs index 9d2b5f8a3a..228eb8adc3 100644 --- a/Terminal.Gui/Configuration/Settings/TuiConfigurationExtensions.cs +++ b/Terminal.Gui/Configuration/Settings/TuiConfigurationExtensions.cs @@ -1,11 +1,14 @@ using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.Extensions.Configuration; +using Terminal.Gui.App; namespace Terminal.Gui.Configuration; /// /// Extension methods for that add Terminal.Gui configuration sources -/// in the correct precedence order (matching the existing behavior). +/// in the correct precedence order (matching MEC provider order). /// public static class TuiConfigurationExtensions { @@ -24,19 +27,24 @@ public static class TuiConfigurationExtensions /// /// The configuration builder. /// The builder for chaining. - public static IConfigurationBuilder AddTuiLibraryDefaults (this IConfigurationBuilder builder) + public static IConfigurationBuilder AddTuiLibraryDefaults (this IConfigurationBuilder builder) => AddTuiLibraryDefaults (builder, null); + + internal static IConfigurationBuilder AddTuiLibraryDefaults (IConfigurationBuilder builder, List? jsonSources) { Assembly libraryAssembly = typeof (TuiConfigurationExtensions).Assembly; string resourceName = $"Terminal.Gui.Resources.{CONFIG_FILENAME}"; Stream? stream = libraryAssembly.GetManifestResourceStream (resourceName); - if (stream is not null) + if (stream is null) { - builder.AddJsonStream (stream); + return builder; } - return builder; + using StreamReader reader = new (stream); + string json = reader.ReadToEnd (); + + return AddTuiInlineJson (builder, json, $"Embedded resource \"{resourceName}\"", jsonSources); } /// @@ -45,7 +53,10 @@ public static IConfigurationBuilder AddTuiLibraryDefaults (this IConfigurationBu /// The configuration builder. /// The application name (used for app-specific config files). /// The builder for chaining. - public static IConfigurationBuilder AddTuiAppDefaults (this IConfigurationBuilder builder, string? appName = null) + public static IConfigurationBuilder AddTuiAppDefaults (this IConfigurationBuilder builder, string? appName = null) => + AddTuiAppDefaults (builder, appName, null); + + internal static IConfigurationBuilder AddTuiAppDefaults (IConfigurationBuilder builder, string? appName, List? jsonSources) { Assembly? entryAssembly = Assembly.GetEntryAssembly (); @@ -65,12 +76,15 @@ public static IConfigurationBuilder AddTuiAppDefaults (this IConfigurationBuilde Stream? stream = entryAssembly.GetManifestResourceStream (resourceName); - if (stream is not null) + if (stream is null) { - builder.AddJsonStream (stream); + return builder; } - return builder; + using StreamReader reader = new (stream); + string json = reader.ReadToEnd (); + + return AddTuiInlineJson (builder, json, $"Embedded resource \"{resourceName}\"", jsonSources); } /// @@ -82,26 +96,31 @@ public static IConfigurationBuilder AddTuiAppDefaults (this IConfigurationBuilde /// ./.tui/{appName}.config.json (AppCurrent) /// /// Files are optional — missing files are silently skipped. Later files override earlier ones. + /// Files that fail to read or parse are collected in and skipped; + /// legacy-shaped files are warned about and skipped. /// /// The configuration builder. /// The application name for app-specific config files. If null, app-specific files are skipped. + /// + /// The directory the ./.tui/ paths are resolved against. If null, uses + /// (never the app's install directory). + /// /// The builder for chaining. - public static IConfigurationBuilder AddTuiUserFiles (this IConfigurationBuilder builder, string? appName = null) + public static IConfigurationBuilder AddTuiUserFiles (this IConfigurationBuilder builder, string? appName = null, string? currentDirectory = null) => + AddTuiUserFiles (builder, appName, currentDirectory, null); + + internal static IConfigurationBuilder AddTuiUserFiles (IConfigurationBuilder builder, string? appName, string? currentDirectory, List? jsonSources) { string homeDir = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile); - string globalHomePath = Path.Combine (homeDir, TUI_CONFIG_FOLDER, CONFIG_FILENAME); - string globalCurrentPath = Path.Combine (".", TUI_CONFIG_FOLDER, CONFIG_FILENAME); + string currentDir = Path.GetFullPath (currentDirectory ?? Environment.CurrentDirectory); - builder.AddJsonFile (globalHomePath, optional: true, reloadOnChange: false); - builder.AddJsonFile (globalCurrentPath, optional: true, reloadOnChange: false); + AddUserFile (builder, Path.Combine (homeDir, TUI_CONFIG_FOLDER, CONFIG_FILENAME), jsonSources); + AddUserFile (builder, Path.Combine (currentDir, TUI_CONFIG_FOLDER, CONFIG_FILENAME), jsonSources); if (!string.IsNullOrEmpty (appName)) { - string appHomePath = Path.Combine (homeDir, TUI_CONFIG_FOLDER, $"{appName}.{CONFIG_FILENAME}"); - string appCurrentPath = Path.Combine (".", TUI_CONFIG_FOLDER, $"{appName}.{CONFIG_FILENAME}"); - - builder.AddJsonFile (appHomePath, optional: true, reloadOnChange: false); - builder.AddJsonFile (appCurrentPath, optional: true, reloadOnChange: false); + AddUserFile (builder, Path.Combine (homeDir, TUI_CONFIG_FOLDER, $"{appName}.{CONFIG_FILENAME}"), jsonSources); + AddUserFile (builder, Path.Combine (currentDir, TUI_CONFIG_FOLDER, $"{appName}.{CONFIG_FILENAME}"), jsonSources); } return builder; @@ -113,7 +132,9 @@ public static IConfigurationBuilder AddTuiUserFiles (this IConfigurationBuilder /// /// The configuration builder. /// The builder for chaining. - public static IConfigurationBuilder AddTuiEnvironmentVariable (this IConfigurationBuilder builder) + public static IConfigurationBuilder AddTuiEnvironmentVariable (this IConfigurationBuilder builder) => AddTuiEnvironmentVariable (builder, null); + + internal static IConfigurationBuilder AddTuiEnvironmentVariable (IConfigurationBuilder builder, List? jsonSources) { string? envConfig = Environment.GetEnvironmentVariable (TUI_CONFIG_ENV); @@ -122,31 +143,150 @@ public static IConfigurationBuilder AddTuiEnvironmentVariable (this IConfigurati return builder; } - byte[] bytes = System.Text.Encoding.UTF8.GetBytes (envConfig); - MemoryStream stream = new (bytes); - builder.AddJsonStream (stream); - - return builder; + return AddTuiInlineJson (builder, envConfig, $"{TUI_CONFIG_ENV} environment variable", jsonSources); } /// /// Adds an in-memory JSON string as the highest-priority configuration source. - /// Equivalent to the legacy ConfigurationManager.RuntimeConfig property. + /// Highest-priority in-memory JSON overlay (UICatalog, tests, and app startup). /// /// The configuration builder. /// A JSON string containing configuration overrides. /// The builder for chaining. - public static IConfigurationBuilder AddTuiRuntimeConfig (this IConfigurationBuilder builder, string? json) + public static IConfigurationBuilder AddTuiRuntimeConfig (this IConfigurationBuilder builder, string? json) => + AddTuiRuntimeConfig (builder, json, null); + + internal static IConfigurationBuilder AddTuiRuntimeConfig (IConfigurationBuilder builder, string? json, List? jsonSources) { if (string.IsNullOrEmpty (json)) { return builder; } - byte[] bytes = System.Text.Encoding.UTF8.GetBytes (json); + return AddTuiInlineJson (builder, json, "RuntimeConfig", jsonSources); + } + + /// + /// Adds inline JSON as a stream source after validating it (parsed exactly once). Malformed JSON and + /// non-object roots are collected in and skipped; legacy-shaped JSON is + /// warned about and skipped. A source must never crash the build — it runs from a module initializer. + /// When is provided, the parsed root object is appended to it so callers + /// can build a raw-JSON merged view of all sources (atomic array overrides). + /// + internal static IConfigurationBuilder AddTuiInlineJson (IConfigurationBuilder builder, string json, string sourceName, List? jsonSources = null) + { + JsonObject obj; + + try + { + JsonNode? node = JsonNode.Parse (json, documentOptions: new () { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + + if (node is not JsonObject parsed) + { + TuiJsonErrors.Add ($"{sourceName}: The top-level JSON element must be an object."); + + return builder; + } + + if (IsLegacyShape (parsed)) + { + Logging.Warning ( + $"{sourceName} uses a pre-MEC (flat-key or array-Themes) shape and is not applied. Convert it with Tools/MigrateConfig. See docfx/docs/migrate-cm-to-mec.md."); + + return builder; + } + + obj = parsed; + } + catch (Exception ex) when (ex is JsonException or ArgumentException or InvalidOperationException) + { + // ArgumentException/InvalidOperationException: JsonObject materialization rejects duplicate + // keys (which MEC's own parser would also reject — but inside builder.Build(), past every + // per-source handler). Skip the source here so one bad source cannot discard the rest. + TuiJsonErrors.Add ($"{sourceName}: {ex.Message}"); + + return builder; + } + + byte [] bytes = System.Text.Encoding.UTF8.GetBytes (json); MemoryStream stream = new (bytes); builder.AddJsonStream (stream); + jsonSources?.Add (obj); return builder; } + + private static void AddUserFile (IConfigurationBuilder builder, string path, List? jsonSources) + { + if (!File.Exists (path)) + { + return; + } + + string text; + + try + { + text = File.ReadAllText (path); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) + { + // An unreadable file must not crash the build (it runs from a module initializer); + // the error is printed at shutdown. + TuiJsonErrors.Add ($"Configuration file \"{path}\": {ex.Message}"); + + return; + } + + AddTuiInlineJson (builder, text, $"Configuration file \"{path}\"", jsonSources); + } + + /// + /// Returns when uses a pre-MEC shape: + /// top-level dotted keys (e.g. Button.DefaultShadow) or Themes/Schemes + /// as an array of single-key objects. + /// + public static bool IsLegacyConfigShape (string json) + { + if (string.IsNullOrWhiteSpace (json)) + { + return false; + } + + JsonNode? node; + + try + { + node = JsonNode.Parse (json, documentOptions: new () { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + } + catch (JsonException) + { + return false; + } + + if (node is not JsonObject obj) + { + return false; + } + + return IsLegacyShape (obj); + } + + private static bool IsLegacyShape (JsonObject obj) + { + foreach (KeyValuePair pair in obj) + { + if (pair.Key.Contains ('.')) + { + return true; + } + + if (pair.Key is "Themes" or "Schemes" && pair.Value is JsonArray) + { + return true; + } + } + + return false; + } } diff --git a/Terminal.Gui/Configuration/Settings/WindowSettings.cs b/Terminal.Gui/Configuration/Settings/WindowSettings.cs index 13ace188ff..238b66da84 100644 --- a/Terminal.Gui/Configuration/Settings/WindowSettings.cs +++ b/Terminal.Gui/Configuration/Settings/WindowSettings.cs @@ -1,19 +1,25 @@ namespace Terminal.Gui.Configuration; /// -/// Settings POCO for visual defaults (ThemeScope). +/// Immutable settings record for visual defaults (ThemeScope). /// -public class WindowSettings +public sealed record WindowSettings { - /// Gets or sets the default shadow style for windows. - public ShadowStyles DefaultShadow { get; set; } = ShadowStyles.None; + /// Gets the default shadow style for windows. + public ShadowStyles DefaultShadow { get; init; } = ShadowStyles.None; - /// Gets or sets the default border style for windows. - public LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + /// Gets the default border style for windows. + public LineStyle DefaultBorderStyle { get; init; } = LineStyle.Single; - /// - /// The static facade instance. Always contains the current effective values. - /// Updated by the MEC binding at initialization. - /// - public static WindowSettings Defaults { get; set; } = new (); + /// The compile-time-known defaults. + public static WindowSettings Default { get; } = new (); + + /// The currently effective values, updated atomically by . + public static WindowSettings Current + { + get => Volatile.Read (ref _current); + set => Volatile.Write (ref _current, value); + } + + private static WindowSettings _current = Default; } diff --git a/Terminal.Gui/Configuration/SettingsScope.cs b/Terminal.Gui/Configuration/SettingsScope.cs deleted file mode 100644 index 0789e85f52..0000000000 --- a/Terminal.Gui/Configuration/SettingsScope.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Text.Json.Serialization; - -#pragma warning disable CS0618 // Obsolete - SettingsScope uses ConfigProperty internally - -namespace Terminal.Gui.Configuration; - -// TODO: Change to internal to prevent app usage -/// -/// INTERNAL: The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties attributed -/// with -/// . -/// -/// -/// -/// { -/// "$schema" : "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", -/// "Application.UseSystemConsole" : true, -/// "Theme" : "Default", -/// "Themes": { -/// }, -/// }, -/// -/// -/// -[JsonConverter (typeof (ScopeJsonConverter))] -[Obsolete ("Being replaced by Microsoft.Extensions.Configuration. Will be removed in a future version.")] -public class SettingsScope : Scope -{ - /// - /// Initializes a new instance. The dictionary will be populated with uninitialized ( - /// - /// - public SettingsScope () - { - ConfigProperty? configProperty = GetUninitializedProperty ("Theme"); - - TryAdd ("Theme", configProperty); - - configProperty = GetUninitializedProperty ("Themes"); - - TryAdd ("Themes", configProperty); - } - - /// Points to our JSON schema. - [JsonInclude] - [JsonPropertyName ("$schema")] - public string Schema { get; set; } = "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json"; -} diff --git a/Terminal.Gui/Configuration/SourceGenerationContext.cs b/Terminal.Gui/Configuration/SourceGenerationContext.cs index 800dc4dad7..1aa08489dd 100644 --- a/Terminal.Gui/Configuration/SourceGenerationContext.cs +++ b/Terminal.Gui/Configuration/SourceGenerationContext.cs @@ -2,8 +2,6 @@ using System.Text.Json.Serialization; using Terminal.Gui.Input; -#pragma warning disable CS0618 // Obsolete - SourceGenerationContext registers serialization types used during transition - namespace Terminal.Gui.Configuration; /// @@ -23,7 +21,6 @@ namespace Terminal.Gui.Configuration; [JsonSerializable (typeof (Color))] [JsonSerializable (typeof (Key))] [JsonSerializable (typeof (Key []))] -[JsonSerializable (typeof (Glyphs))] [JsonSerializable (typeof (Alignment))] [JsonSerializable (typeof (AlignmentModes))] [JsonSerializable (typeof (LineStyle))] @@ -34,18 +31,6 @@ namespace Terminal.Gui.Configuration; [JsonSerializable (typeof (Dictionary))] [JsonSerializable (typeof (Dictionary))] -[JsonSerializable (typeof (Dictionary))] -[JsonSerializable (typeof (ConcurrentDictionary))] -[JsonSerializable (typeof (ConfigProperty))] - -[JsonSerializable (typeof (Scope))] -[JsonSerializable (typeof (AppSettingsScope))] -[JsonSerializable (typeof (SettingsScope))] -[JsonSerializable (typeof (ThemeScope))] -[JsonSerializable (typeof (Scope))] -[JsonSerializable (typeof (Scope))] -[JsonSerializable (typeof (Scope))] -[JsonSerializable (typeof (ConcurrentDictionary))] [JsonSerializable (typeof (Scheme))] [JsonSerializable (typeof (Dictionary))] diff --git a/Terminal.Gui/Configuration/SourcesManager.cs b/Terminal.Gui/Configuration/SourcesManager.cs deleted file mode 100644 index 75e5163f78..0000000000 --- a/Terminal.Gui/Configuration/SourcesManager.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Text.Json; -using Trace = Terminal.Gui.Tracing.Trace; - -#pragma warning disable CS0618 // Obsolete - SourcesManager still uses ConfigurationManager internally during transition - -namespace Terminal.Gui.Configuration; - -/// -/// Manages the Sources and provides the API for loading them. Source is a location -/// where a configuration can be stored. Sources are defined in . -/// -public class SourcesManager -{ - /// - /// Gets the name of the TUI configuration folder. - /// - public const string TUI_CONFIG_FOLDER_S = ".tui"; - - /// - /// Gets the name of the TUI configuration environment variable. - /// - public const string TUI_CONFIG_ENV_S = "TUI_CONFIG"; - - /// - /// Provides a map from each of the to file system and resource paths that have been - /// loaded by . - /// - public ConcurrentDictionary Sources { get; } = new (); - - /// - /// Cached array of all defined ConfigLocations values (excluding None and All) ordered by bit value. - /// Only includes single-flag values (powers of 2). - /// - private static readonly ConfigLocations [] _sortedLocations = Enum.GetValues () - .Where (loc => loc != ConfigLocations.None && IsPowerOfTwo ((int)loc)) - .OrderBy (loc => (int)loc) - .ToArray (); - - /// - /// Checks if a number is a power of 2 (i.e., a single flag value). - /// - private static bool IsPowerOfTwo (int value) { return value > 0 && (value & (value - 1)) == 0; } - - /// - /// INTERNAL: Loads configuration from the specified locations in the order defined by the - /// enum bit values (lower bit values are loaded first, higher bit values override). - /// - /// The Settings Scope object that will be updated. - /// The locations to load from. - internal void LoadFromLocations (SettingsScope? settingsScope, ConfigLocations locations) - { - if (settingsScope is null) - { - return; - } - - foreach (ConfigLocations location in _sortedLocations) - { - if (!locations.HasFlag (location)) - { - continue; - } - - switch (location) - { - case ConfigLocations.HardCoded: - // HardCoded is handled separately in ConfigurationManager.Load - break; - - case ConfigLocations.LibraryResources: - Load ( - settingsScope, - typeof (ConfigurationManager).Assembly, - $"Terminal.Gui.Resources.{ConfigurationManager.ConfigFilename}", - ConfigLocations.LibraryResources); - - break; - - case ConfigLocations.AppResources: - string? embeddedStylesResourceName = Assembly.GetEntryAssembly () - ?.GetManifestResourceNames () - .FirstOrDefault (x => x.EndsWith (ConfigurationManager.ConfigFilename)); - - if (string.IsNullOrEmpty (embeddedStylesResourceName)) - { - embeddedStylesResourceName = ConfigurationManager.ConfigFilename; - } - - Load (settingsScope, Assembly.GetEntryAssembly ()!, embeddedStylesResourceName, ConfigLocations.AppResources); - - break; - - case ConfigLocations.GlobalHome: - Load (settingsScope, $"~/{TUI_CONFIG_FOLDER_S}/{ConfigurationManager.ConfigFilename}", ConfigLocations.GlobalHome); - - break; - - case ConfigLocations.GlobalCurrent: - Load (settingsScope, $"./{TUI_CONFIG_FOLDER_S}/{ConfigurationManager.ConfigFilename}", ConfigLocations.GlobalCurrent); - - break; - - case ConfigLocations.AppHome: - Load (settingsScope, $"~/{TUI_CONFIG_FOLDER_S}/{ConfigurationManager.AppName}.{ConfigurationManager.ConfigFilename}", ConfigLocations.AppHome); - - break; - - case ConfigLocations.AppCurrent: - Load (settingsScope, $"./{TUI_CONFIG_FOLDER_S}/{ConfigurationManager.AppName}.{ConfigurationManager.ConfigFilename}", ConfigLocations.AppCurrent); - - break; - - case ConfigLocations.Env: - string? envConfig = Environment.GetEnvironmentVariable (TUI_CONFIG_ENV_S); - - if (!string.IsNullOrEmpty (envConfig)) - { - Load (settingsScope, envConfig, $"Environment.GetEnvironmentVariable(\"{TUI_CONFIG_ENV_S}\")", ConfigLocations.Env); - } - - break; - - case ConfigLocations.Runtime: - if (!string.IsNullOrEmpty (ConfigurationManager.RuntimeConfig)) - { - Load (settingsScope, ConfigurationManager.RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime); - } - - break; - } - } - } - - /// INTERNAL: Loads into the specified . - /// The Settings Scope object that will be loaded into. - /// Json document to update the settings with. - /// The source (filename/resource name) the Json document was read from. - /// The Config Location corresponding to - /// if the settingsScope was updated. - internal bool Load (SettingsScope? settingsScope, Stream stream, string source, ConfigLocations location) - { - if (settingsScope is null) - { - return false; - } - - // Update the existing settings with the new settings. - try - { -#if DEBUG - string? json = new StreamReader (stream).ReadToEnd (); - stream.Position = 0; - Debug.Assert (json != null); -#endif - SettingsScope? scope = JsonSerializer.Deserialize (stream, TuiSerializerContext.Instance.SettingsScope); - - if (scope is null) - { - throw new JsonException ("Configuration JSON root must not be null."); - } - - settingsScope.UpdateFrom (scope); - ConfigurationManager.OnUpdated (); - - AddSource (location, source); - - Logging.Trace ($"Read configuration from \"{source}\" - ConfigLocation: {location}"); - - Trace.Configuration (source, "Load", $"location={location}"); - - return true; - } - catch (JsonException e) - { - if (ConfigurationManager.ThrowOnJsonErrors ?? false) - { - throw; - } - - Trace.Configuration (source, "LoadError", e.Message); - ConfigurationManager.AddJsonError ($"Error reading {source}: {e.Message}"); - } - - return false; - } - - internal void AddSource (ConfigLocations location, string source) - { - // ConcurrentDictionary's AddOrUpdate is thread-safe - Sources.AddOrUpdate (location, source, (_, _) => source); - } - - /// - /// INTERNAL: Loads the `config.json` file a into the specified - /// . - /// - /// The Settings Scope object that will be loaded into. - /// Json document to update the settings with. - /// The Config Location corresponding to - /// if the settingsScope was updated. - internal bool Load (SettingsScope? settingsScope, string filePath, ConfigLocations location) - { - string realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); - - if (!File.Exists (realPath)) - { - //Logging.Warning ($"\"{realPath}\" does not exist."); - - // Always add the source even if it doesn't exist. - AddSource (location, filePath); - - return true; - } - - var retryCount = 0; - - // Sometimes when the config file is written by an external agent, the change notification comes - // before the file is closed. This works around that. - while (retryCount < 2) - { - try - { - FileStream stream = File.OpenRead (realPath); - - bool ret = Load (settingsScope, stream, filePath, location); - stream.Close (); - stream.Dispose (); - - return ret; - } - catch (IOException ioe) - { - Logging.Warning ($"{ioe.Message}. Retrying..."); - Task.Delay (100); - retryCount++; - } - } - - return false; - } - - /// INTERNAL: Loads the Json document in into the specified . - /// The Settings Scope object that will be loaded into. - /// Json document to update the settings with. - /// The source (filename/resource name) the Json document was read from. - /// The Config Location corresponding to - /// if the settingsScope was updated. - internal bool Load (SettingsScope? settingsScope, string? json, string source, ConfigLocations location) - { - Debug.Assert (location != ConfigLocations.All); - - if (string.IsNullOrEmpty (json)) - { - return false; - } - - var stream = new MemoryStream (); - var writer = new StreamWriter (stream); - writer.Write (json); - writer.Flush (); - stream.Position = 0; - - return Load (settingsScope, stream, source, location); - } - - /// - /// INTERNAL: Loads the Json document from the resource named from - /// into the specified . - /// - /// The Settings Scope object that will be loaded into. - /// The assembly containing the resource. - /// The name of the resource containing the Json document was read from. - /// The Config Location corresponding to - /// if the settingsScope was updated. - internal bool Load (SettingsScope? settingsScope, Assembly assembly, string resourceName, ConfigLocations location) - { - if (string.IsNullOrEmpty (resourceName)) - { - Logging.Warning ($"{resourceName} must not be null or empty."); - - return false; - } - - using Stream? stream = assembly.GetManifestResourceStream (resourceName); - - if (stream is null) - { - Logging.Warning ($"Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\"."); - - return false; - } - - return Load (settingsScope, stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}", location); - } - - /// - /// INTERNAL: Returns a JSON document with the configuration specified. - /// - /// - internal string ToJson (SettingsScope? scope) - { - return JsonSerializer.Serialize (scope, TuiSerializerContext.Instance.SettingsScope); - } - - /// - /// INTERNAL: Returns a stream with the configuration specified. - /// - /// - internal Stream ToStream (SettingsScope? scope) - { - string json = JsonSerializer.Serialize (scope, TuiSerializerContext.Instance.SettingsScope); - - // turn it into a stream - var stream = new MemoryStream (); - var writer = new StreamWriter (stream); - writer.Write (json); - writer.Flush (); - stream.Position = 0; - - return stream; - } -} diff --git a/Terminal.Gui/Configuration/ThemeManager.cs b/Terminal.Gui/Configuration/ThemeManager.cs index 0b6b9c88ad..97ba8520a1 100644 --- a/Terminal.Gui/Configuration/ThemeManager.cs +++ b/Terminal.Gui/Configuration/ThemeManager.cs @@ -1,321 +1,45 @@ -using System.Collections.Concurrent; +// Grok - grok-4.6 using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -#pragma warning disable CS0618 // Obsolete - ThemeManager still uses ConfigurationManager/ConfigProperty/ThemeScope internally during transition namespace Terminal.Gui.Configuration; -/// Manages Themes. -/// -/// A Theme is a collection of settings that are named. The default theme is named "Default". -/// The property is used to determine the currently active theme. -/// The property is a dictionary of themes. -/// +/// +/// Static facade for the active theme. Backed by and +/// . +/// public static class ThemeManager { - /// - /// Convenience method to get the current theme. The current theme is the item in the dictionary, - /// with the key of . - /// - /// - public static ThemeScope GetCurrentTheme () { return Themes! [Theme]; } - - /// - /// INTERNAL: Getter for . - /// Convenience method to get the themes dictionary. The themes dictionary is a dictionary of - /// objects, with the key being the name of the theme. - /// - /// - /// - private static ConcurrentDictionary GetThemes () - { - if (!ConfigurationManager.IsInitialized ()) - { - // We're being called from the module initializer. - // We need to provide a dictionary of themes containing the hard-coded theme. - return GetHardCodedThemes ()!; - } - - if (ConfigurationManager.Settings is null) - { - throw new InvalidOperationException ("Settings is null."); - } - - if (ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes)) - { - if (themes.HasValue) - { - return (themes.PropertyValue as ConcurrentDictionary)!; - } - - return GetHardCodedThemes ()!; - } - - throw new InvalidOperationException ("Settings has no Themes property."); - } - - /// - /// INTERNAL: Convenience method to get a list of theme names. - /// - /// - /// - public static ImmutableList GetThemeNames () - { - if (!ConfigurationManager.IsInitialized ()) - { - // We're being called from the module initializer. - // We need to provide a dictionary of themes containing the hard-coded theme. - return GetHardCodedThemes ()!.Keys.ToImmutableList (); - } - - if (ConfigurationManager.Settings is null) - { - throw new InvalidOperationException ("Settings is null."); - } - - if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes)) - { - throw new InvalidOperationException ("Settings has no Themes property."); - } - - ConcurrentDictionary? returnConcurrentDictionary; - - if (themes.HasValue) - { - returnConcurrentDictionary = themes.PropertyValue as ConcurrentDictionary; - } - else - { - returnConcurrentDictionary = GetHardCodedThemes (); - } - - return returnConcurrentDictionary!.Keys - .OrderBy (key => key == DEFAULT_THEME_NAME ? string.Empty : key) // Ensure DEFAULT_THEME_NAME is first - .ToImmutableList (); - - } - - /// - /// Convenience method to get the current theme name. The current theme name is the value of . - /// - /// - public static string GetCurrentThemeName () { return Theme!; } - - // TODO: Add a lock around Theme and Themes - // TODO: For now, this test can't run in parallel with other tests that access Theme or Themes. - // TODO: ThemeScopeList_WithThemes_ClonesSuccessfully - - /// - /// Gets the Themes dictionary. is preferred. - /// The backing store is ["Themes"]. - /// However, if is false, this property will return the - /// hard-coded themes. - /// - /// - [JsonConverter (typeof (ConcurrentDictionaryJsonConverter))] - [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] - public static ConcurrentDictionary? Themes - { - // Note: This property getter must be public; DeepClone depends on it. - get => GetThemes (); - internal set => SetThemes (value); - } - - /// - /// INTERNAL: Setter for . - /// - /// - /// - private static void SetThemes (ConcurrentDictionary? dictionary) - { - if (dictionary is { } && !dictionary.ContainsKey (DEFAULT_THEME_NAME)) - { - throw new InvalidOperationException ($"Themes must include an item named {DEFAULT_THEME_NAME}"); - } - - if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themes)) - { - ConfigurationManager.Settings ["Themes"].PropertyValue = dictionary; - - return; - } - - throw new InvalidOperationException ("Settings is null."); - } - - /// - /// INTERNAL: Returns the hard-coded Themes dictionary. - /// - /// - /// - private static ConcurrentDictionary? GetHardCodedThemes () - { - ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope (); - - if (hardCodedThemeScope is null) - { - throw new InvalidOperationException ("Hard coded theme scope is null."); - } - - return new (new Dictionary { { DEFAULT_THEME_NAME, hardCodedThemeScope } }, StringComparer.InvariantCultureIgnoreCase); - } - - /// - /// INTERNAL: Returns the ThemeScope containing the hard-coded Themes. - /// - /// - private static ThemeScope GetHardCodedThemeScope () - { - IEnumerable>? hardCodedThemeProperties = ConfigurationManager.GetHardCodedConfigPropertiesByScope ("ThemeScope"); - - if (hardCodedThemeProperties is null) - { - throw new InvalidOperationException ("Hard coded theme properties are null."); - } - - var hardCodedThemeScope = new ThemeScope (); - foreach (KeyValuePair p in hardCodedThemeProperties) - { - hardCodedThemeScope.AddValue (p.Key, p.Value.PropertyValue); - } - - return hardCodedThemeScope; - } - - /// - /// The name of the default theme ("Default"). - /// + /// The name of the default theme. public const string DEFAULT_THEME_NAME = "Default"; /// - /// The currently selected theme. The backing store is ["Theme"]. + /// Gets or sets the currently selected theme name. Setting this applies ThemeScope overlays + /// and raises . /// - [JsonInclude] - [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] - [JsonPropertyName ("Theme")] public static string Theme { - get - { - if (!ConfigurationManager.IsInitialized ()) - { - // We're being called from the module initializer. - // Hard coded default value - return DEFAULT_THEME_NAME; - } - - if (ConfigurationManager.Settings is { } && ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp)) - { - if (themeCp.HasValue) - { - return (themeCp.PropertyValue as string)!; - } - - return DEFAULT_THEME_NAME; - } - - throw new InvalidOperationException ("Settings is null."); - } - + get => ThemeSettings.Defaults.Theme ?? DEFAULT_THEME_NAME; set { - if (!ConfigurationManager.IsInitialized ()) - { - throw new InvalidOperationException ("Theme cannot be set before ConfigurationManager is initialized."); - } - - if (ConfigurationManager.Settings is null || !ConfigurationManager.Settings.TryGetValue ("Theme", out ConfigProperty? themeCp)) - { - throw new InvalidOperationException ("Settings is null."); - } - - if (themeCp is null || !themeCp.HasValue) - { - throw new InvalidOperationException ("Theme has no value."); - } - - if (!ConfigurationManager.Settings.TryGetValue ("Themes", out ConfigProperty? themesCp)) - { - throw new InvalidOperationException ("Settings has no Themes property."); - } - - string previousThemeValue = GetCurrentThemeName (); - - if (value == previousThemeValue) + if (string.IsNullOrEmpty (value)) { return; } - if (!Themes!.ContainsKey (value)) - { - Logging.Warning ($"{value} is not a valid theme name."); - } - - // Update the backing store - ConfigurationManager.Settings! ["Theme"].PropertyValue = value; - - OnThemeChanged (previousThemeValue, value); + TuiConfigurationBuilder.Shared.TrySwitchTheme (value); } } - /// - /// INTERNAL: Updates to the current values of the static - /// properties. - /// - internal static void UpdateToCurrentValues () - { - // BUGBUG: This corrupts _hardCodedDefaults. See #4288 - Themes! [Theme].UpdateToCurrentValues (); - } - - /// - /// INTERNAL: Loads all Themes to their hard-coded default values. - /// - internal static void LoadHardCodedDefaults () - { - if (!ConfigurationManager.IsInitialized ()) - { - throw new InvalidOperationException ("ThemeManager is not initialized."); - } - - if (ConfigurationManager.Settings is null) - { - return; - } + /// Raised after changes and overlays have been published. + public static event EventHandler>? ThemeChanged; - ThemeScope? hardCodedThemeScope = GetHardCodedThemeScope (); + /// Raises after overlays were published by a non-Shared builder. + internal static void RaiseThemeChanged (string themeName) => + ThemeChanged?.Invoke (null, new App.EventArgs (themeName)); - if (hardCodedThemeScope is null) - { - throw new InvalidOperationException ("Hard coded theme scope is null."); - } - - ConcurrentDictionary hardCodedThemes = new ( - new Dictionary - { - { Theme, hardCodedThemeScope } - }, - StringComparer.InvariantCultureIgnoreCase); - - // BUGBUG: SchemeManager is broken and needs to be fixed to not have the hard coded schemes get overwritten. - // BUGBUG: This is a partial workaround - // BUGBUG: See https://github.com/tui-cs/Terminal.Gui/issues/4288 - SchemeManager.LoadToHardCodedDefaults (); - - ConfigurationManager.Settings ["Themes"].PropertyValue = hardCodedThemes; - ConfigurationManager.Settings ["Theme"].PropertyValue = DEFAULT_THEME_NAME; - - } - - /// Called when the selected theme has changed. Fires the event. - internal static void OnThemeChanged (string previousThemeName, string newThemeName) - { - EventArgs args = new (newThemeName); - ThemeChanged?.Invoke (null, args); - } + /// Gets the current theme name. + public static string GetCurrentThemeName () => Theme; - /// Raised when the selected theme has changed. - public static event EventHandler>? ThemeChanged; + /// Gets the available theme names from configuration, with first. + public static ImmutableList GetThemeNames () => [.. ThemeCatalog.GetNames (TuiConfigurationBuilder.Shared.Configuration)]; } diff --git a/Terminal.Gui/Configuration/ThemeScope.cs b/Terminal.Gui/Configuration/ThemeScope.cs deleted file mode 100644 index cfd6f907f7..0000000000 --- a/Terminal.Gui/Configuration/ThemeScope.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json.Serialization; - -#pragma warning disable CS0618 // Obsolete - ThemeScope references Scope internally - -namespace Terminal.Gui.Configuration; - -// TODO: Change to internal to prevent app usage -/// -/// The root object for a Theme. A Theme is a set of settings that are applied to the running -/// as a group. -/// -/// -/// -/// -/// -/// -/// "Default": { -/// "Schemes": [ -/// { -/// "Accent": { -/// "Normal": { -/// "Foreground": "BrightGreen", -/// "Background": "Black" -/// }, -/// "Focus": { -/// "Foreground": "White", -/// "Background": "Cyan" -/// -/// }, -/// "HotNormal": { -/// "Foreground": "Yellow", -/// "Background": "Black" -/// -/// }, -/// "HotFocus": { -/// "Foreground": "Blue", -/// "Background": "Cyan" -/// }, -/// "Disabled": { -/// "Foreground": "DarkGray", -/// "Background": "Black" -/// -/// } -/// } -/// -/// -[JsonConverter (typeof (ScopeJsonConverter))] -[Obsolete ("Being replaced by Microsoft.Extensions.Configuration. Will be removed in a future version.")] -public class ThemeScope : Scope -{ -} - - diff --git a/Terminal.Gui/Configuration/TuiJsonErrors.cs b/Terminal.Gui/Configuration/TuiJsonErrors.cs new file mode 100644 index 0000000000..b34e109c3d --- /dev/null +++ b/Terminal.Gui/Configuration/TuiJsonErrors.cs @@ -0,0 +1,64 @@ +// Grok - grok-4.6 +using Terminal.Gui.App; + +namespace Terminal.Gui.Configuration; + +/// +/// Collects JSON load errors from MEC sources so they can be printed at shutdown +/// instead of failing fast (the v2 contract). +/// +public static class TuiJsonErrors +{ + private static readonly Lock _lock = new (); + private static readonly List _errors = []; + + /// Adds a load-error message. Ignored if is empty. + public static void Add (string message) + { + if (string.IsNullOrWhiteSpace (message)) + { + return; + } + + lock (_lock) + { + _errors.Add (message); + } + } + + /// + /// INTERNAL: Clears collected errors. Called at the start of each configuration build so the + /// list describes the current effective configuration instead of accumulating across rebuilds + /// (e.g. from a file watcher re-applying a persistently malformed source). + /// + internal static void Clear () + { + lock (_lock) + { + _errors.Clear (); + } + } + + /// Gets a snapshot of collected errors. + public static IReadOnlyList GetErrors () + { + lock (_lock) + { + return [.._errors]; + } + } + + /// Writes collected errors via and clears the list. + public static void Print () + { + lock (_lock) + { + foreach (string error in _errors) + { + Logging.Warning (error); + } + + _errors.Clear (); + } + } +} diff --git a/Terminal.Gui/Configuration/TuiSerializerContext.cs b/Terminal.Gui/Configuration/TuiSerializerContext.cs index 83f4cb6b88..0104218de2 100644 --- a/Terminal.Gui/Configuration/TuiSerializerContext.cs +++ b/Terminal.Gui/Configuration/TuiSerializerContext.cs @@ -13,9 +13,7 @@ namespace Terminal.Gui.Configuration; /// ). /// /// -/// Internal: this is the non-obsolete home for what was historically -/// ConfigurationManager.SerializerContext. Converters and consumers that previously -/// referenced the obsolete static should reference here instead. +/// Internal: converters and serializers should reference . /// [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "Internal serializer context")] internal static class TuiSerializerContext diff --git a/Terminal.Gui/Drawing/Color/Color.cs b/Terminal.Gui/Drawing/Color/Color.cs index abe63fcd05..a1b6f7b083 100644 --- a/Terminal.Gui/Drawing/Color/Color.cs +++ b/Terminal.Gui/Drawing/Color/Color.cs @@ -7,8 +7,6 @@ using ColorHelper; using ColorConverter = ColorHelper.ColorConverter; -#pragma warning disable CS0618 // Obsolete - Color still uses ConfigurationPropertyAttribute during transition - namespace Terminal.Gui.Drawing; /// @@ -183,7 +181,6 @@ public Color (string colorString) public Color () => Argb = 0u; /// Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values. - [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] public static Dictionary Colors16 { get => diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index d39e7fde09..4c948b57b0 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -8,736 +8,268 @@ namespace Terminal.Gui.Drawing; /// ). /// /// -/// The default glyphs can be changed per- in . Within a config.json -/// file the Json property name is the property name prefixed with "Glyphs.". +/// Overlay glyphs in nested JSON under Glyphs, or per-theme under Themes:{name}:Glyphs. /// /// -/// The Json property can be one of: -/// - unicode glyph in a string (e.g. "☑") -/// - U+hex format in a string (e.g. "U+2611") -/// - \u format in a string (e.g. "\\u2611") -/// - A decimal number (e.g. 97 for "a") +/// Values follow : a glyph string, U+hex (U+2611), +/// \u2611, or a decimal code point. /// /// public class Glyphs { - // IMPORTANT: If you change these, make sure to update the ./Resources/config.json file as - // IMPORTANT: it is the source of truth for the default glyphs at runtime. - // IMPORTANT: Configuration Manager test SaveDefaults uses this class to generate the default config file - // IMPORTANT: in ./UnitTests/bin/Debug/netX.0/config.json + // The default glyph values live on the GlyphSettings record's `init` defaults. + // Resources/config.json is the source of truth for the runtime glyphs; the embedded + // config is loaded and applied via TuiConfigurationBuilder.ApplyToStaticFacades, with + // theme overlays composed under "Themes::Glyphs". /// Unicode replacement character; used by Drivers when rendering in cases where a wide glyph can't /// be output because it would be clipped. Defaults to ' ' (Space). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune WideGlyphReplacement - { - get => GlyphSettings.Defaults.WideGlyphReplacement; - set => GlyphSettings.Defaults.WideGlyphReplacement = value; - } + public static Rune WideGlyphReplacement => GlyphSettings.Current.WideGlyphReplacement; /// File icon. Defaults to ☰ (Trigram For Heaven) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune File - { - get => GlyphSettings.Defaults.File; - set => GlyphSettings.Defaults.File = value; - } + public static Rune File => GlyphSettings.Current.File; /// Folder icon. Defaults to ꤉ (Kayah Li Digit Nine) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Folder - { - get => GlyphSettings.Defaults.Folder; - set => GlyphSettings.Defaults.Folder = value; - } + public static Rune Folder => GlyphSettings.Current.Folder; /// Horizontal Ellipsis - … U+2026 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HorizontalEllipsis - { - get => GlyphSettings.Defaults.HorizontalEllipsis; - set => GlyphSettings.Defaults.HorizontalEllipsis = value; - } + public static Rune HorizontalEllipsis => GlyphSettings.Current.HorizontalEllipsis; /// Vertical Four Dots - ⁞ U+205e - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VerticalFourDots - { - get => GlyphSettings.Defaults.VerticalFourDots; - set => GlyphSettings.Defaults.VerticalFourDots = value; - } + public static Rune VerticalFourDots => GlyphSettings.Current.VerticalFourDots; #region ----------------- Single Glyphs ----------------- /// Null symbol ('␀') - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Null - { - get => GlyphSettings.Defaults.Null; - set => GlyphSettings.Defaults.Null = value; - } + public static Rune Null => GlyphSettings.Current.Null; /// Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateChecked - { - get => GlyphSettings.Defaults.CheckStateChecked; - set => GlyphSettings.Defaults.CheckStateChecked = value; - } + public static Rune CheckStateChecked => GlyphSettings.Current.CheckStateChecked; /// Not Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateUnChecked - { - get => GlyphSettings.Defaults.CheckStateUnChecked; - set => GlyphSettings.Defaults.CheckStateUnChecked = value; - } + public static Rune CheckStateUnChecked => GlyphSettings.Current.CheckStateUnChecked; /// Null Checked indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateNone - { - get => GlyphSettings.Defaults.CheckStateNone; - set => GlyphSettings.Defaults.CheckStateNone = value; - } + public static Rune CheckStateNone => GlyphSettings.Current.CheckStateNone; /// Selected indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Selected - { - get => GlyphSettings.Defaults.Selected; - set => GlyphSettings.Defaults.Selected = value; - } + public static Rune Selected => GlyphSettings.Current.Selected; /// Not Selected indicator (e.g. for and ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune UnSelected - { - get => GlyphSettings.Defaults.UnSelected; - set => GlyphSettings.Defaults.UnSelected = value; - } + public static Rune UnSelected => GlyphSettings.Current.UnSelected; /// Horizontal arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightArrow - { - get => GlyphSettings.Defaults.RightArrow; - set => GlyphSettings.Defaults.RightArrow = value; - } + public static Rune RightArrow => GlyphSettings.Current.RightArrow; /// Left arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftArrow - { - get => GlyphSettings.Defaults.LeftArrow; - set => GlyphSettings.Defaults.LeftArrow = value; - } + public static Rune LeftArrow => GlyphSettings.Current.LeftArrow; /// Down arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune DownArrow - { - get => GlyphSettings.Defaults.DownArrow; - set => GlyphSettings.Defaults.DownArrow = value; - } + public static Rune DownArrow => GlyphSettings.Current.DownArrow; /// Vertical arrow. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune UpArrow - { - get => GlyphSettings.Defaults.UpArrow; - set => GlyphSettings.Defaults.UpArrow = value; - } + public static Rune UpArrow => GlyphSettings.Current.UpArrow; /// Left default indicator (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftDefaultIndicator - { - get => GlyphSettings.Defaults.LeftDefaultIndicator; - set => GlyphSettings.Defaults.LeftDefaultIndicator = value; - } + public static Rune LeftDefaultIndicator => GlyphSettings.Current.LeftDefaultIndicator; /// Horizontal default indicator (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightDefaultIndicator - { - get => GlyphSettings.Defaults.RightDefaultIndicator; - set => GlyphSettings.Defaults.RightDefaultIndicator = value; - } + public static Rune RightDefaultIndicator => GlyphSettings.Current.RightDefaultIndicator; /// Left Bracket (e.g. for . Default is (U+005B) - [. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftBracket - { - get => GlyphSettings.Defaults.LeftBracket; - set => GlyphSettings.Defaults.LeftBracket = value; - } + public static Rune LeftBracket => GlyphSettings.Current.LeftBracket; /// Horizontal Bracket (e.g. for . Default is (U+005D) - ]. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightBracket - { - get => GlyphSettings.Defaults.RightBracket; - set => GlyphSettings.Defaults.RightBracket = value; - } + public static Rune RightBracket => GlyphSettings.Current.RightBracket; /// Half block meter segment (e.g. for ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BlocksMeterSegment - { - get => GlyphSettings.Defaults.BlocksMeterSegment; - set => GlyphSettings.Defaults.BlocksMeterSegment = value; - } + public static Rune BlocksMeterSegment => GlyphSettings.Current.BlocksMeterSegment; /// Continuous block meter segment (e.g. for ). - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ContinuousMeterSegment - { - get => GlyphSettings.Defaults.ContinuousMeterSegment; - set => GlyphSettings.Defaults.ContinuousMeterSegment = value; - } + public static Rune ContinuousMeterSegment => GlyphSettings.Current.ContinuousMeterSegment; /// Stipple pattern (e.g. for ). Default is Light Shade (U+2591) - â–‘. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Stipple - { - get => GlyphSettings.Defaults.Stipple; - set => GlyphSettings.Defaults.Stipple = value; - } + public static Rune Stipple => GlyphSettings.Current.Stipple; /// Diamond. Default is Lozenge (U+25CA) - â—Š. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Diamond - { - get => GlyphSettings.Defaults.Diamond; - set => GlyphSettings.Defaults.Diamond = value; - } + public static Rune Diamond => GlyphSettings.Current.Diamond; /// Close. Default is Heavy Ballot X (U+2718) - ✘. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Close - { - get => GlyphSettings.Defaults.Close; - set => GlyphSettings.Defaults.Close = value; - } + public static Rune Close => GlyphSettings.Current.Close; /// Minimize. Default is Lower Horizontal Shadowed White Circle (U+274F) - ❏. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Minimize - { - get => GlyphSettings.Defaults.Minimize; - set => GlyphSettings.Defaults.Minimize = value; - } + public static Rune Minimize => GlyphSettings.Current.Minimize; /// Maximize. Default is Upper Horizontal Shadowed White Circle (U+273D) - ✽. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Maximize - { - get => GlyphSettings.Defaults.Maximize; - set => GlyphSettings.Defaults.Maximize = value; - } + public static Rune Maximize => GlyphSettings.Current.Maximize; /// Dot. Default is (U+2219) - ∙. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Dot - { - get => GlyphSettings.Defaults.Dot; - set => GlyphSettings.Defaults.Dot = value; - } + public static Rune Dot => GlyphSettings.Current.Dot; /// Dotted Square - ⬚ U+02b1a┝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune DottedSquare - { - get => GlyphSettings.Defaults.DottedSquare; - set => GlyphSettings.Defaults.DottedSquare = value; - } + public static Rune DottedSquare => GlyphSettings.Current.DottedSquare; /// Black Circle . Default is (U+025cf) - ●. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BlackCircle // Black Circle - ● U+025cf - { - get => GlyphSettings.Defaults.BlackCircle; - set => GlyphSettings.Defaults.BlackCircle = value; - } + public static Rune BlackCircle => GlyphSettings.Current.BlackCircle; /// Expand (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Expand - { - get => GlyphSettings.Defaults.Expand; - set => GlyphSettings.Defaults.Expand = value; - } + public static Rune Expand => GlyphSettings.Current.Expand; /// Expand (e.g. for . - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Collapse - { - get => GlyphSettings.Defaults.Collapse; - set => GlyphSettings.Defaults.Collapse = value; - } + public static Rune Collapse => GlyphSettings.Current.Collapse; /// Identical To (U+226) - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune IdenticalTo - { - get => GlyphSettings.Defaults.IdenticalTo; - set => GlyphSettings.Defaults.IdenticalTo = value; - } + public static Rune IdenticalTo => GlyphSettings.Current.IdenticalTo; /// Move indicator. Default is Lozenge (U+25CA) - â—Š. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Move - { - get => GlyphSettings.Defaults.Move; - set => GlyphSettings.Defaults.Move = value; - } + public static Rune Move => GlyphSettings.Current.Move; /// Size Horizontally indicator. Default is ┥Left Right Arrow - ↔ U+02194 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeHorizontal - { - get => GlyphSettings.Defaults.SizeHorizontal; - set => GlyphSettings.Defaults.SizeHorizontal = value; - } + public static Rune SizeHorizontal => GlyphSettings.Current.SizeHorizontal; /// Size Vertical indicator. Default Up Down Arrow - ↕ U+02195 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeVertical - { - get => GlyphSettings.Defaults.SizeVertical; - set => GlyphSettings.Defaults.SizeVertical = value; - } + public static Rune SizeVertical => GlyphSettings.Current.SizeVertical; /// Size Top Left indicator. North West Arrow - ↖ U+02196 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeTopLeft - { - get => GlyphSettings.Defaults.SizeTopLeft; - set => GlyphSettings.Defaults.SizeTopLeft = value; - } + public static Rune SizeTopLeft => GlyphSettings.Current.SizeTopLeft; /// Size Top Right indicator. North East Arrow - ↗ U+02197 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeTopRight - { - get => GlyphSettings.Defaults.SizeTopRight; - set => GlyphSettings.Defaults.SizeTopRight = value; - } + public static Rune SizeTopRight => GlyphSettings.Current.SizeTopRight; /// Size Bottom Right indicator. South East Arrow - ↘ U+02198 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeBottomRight - { - get => GlyphSettings.Defaults.SizeBottomRight; - set => GlyphSettings.Defaults.SizeBottomRight = value; - } + public static Rune SizeBottomRight => GlyphSettings.Current.SizeBottomRight; /// Size Bottom Left indicator. South West Arrow - ↙ U+02199 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune SizeBottomLeft - { - get => GlyphSettings.Defaults.SizeBottomLeft; - set => GlyphSettings.Defaults.SizeBottomLeft = value; - } + public static Rune SizeBottomLeft => GlyphSettings.Current.SizeBottomLeft; /// Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Apple // nonBMP - { - get => GlyphSettings.Defaults.Apple; - set => GlyphSettings.Defaults.Apple = value; - } + public static Rune Apple => GlyphSettings.Current.Apple; /// Apple (BMP). Because snek. See Issue #2610. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune AppleBMP - { - get => GlyphSettings.Defaults.AppleBMP; - set => GlyphSettings.Defaults.AppleBMP = value; - } + public static Rune AppleBMP => GlyphSettings.Current.AppleBMP; /// Copy indicator. Two Joined Squares - ⧉ U+29C9. Used for code block copy buttons. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Copy - { - get => GlyphSettings.Defaults.Copy; - set => GlyphSettings.Defaults.Copy = value; - } + public static Rune Copy => GlyphSettings.Current.Copy; #endregion #region ----------------- Lines ----------------- /// Box Drawings Horizontal Line - Light (U+2500) - ─ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLine - { - get => GlyphSettings.Defaults.HLine; - set => GlyphSettings.Defaults.HLine = value; - } + public static Rune HLine => GlyphSettings.Current.HLine; /// Box Drawings Vertical Line - Light (U+2502) - │ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLine - { - get => GlyphSettings.Defaults.VLine; - set => GlyphSettings.Defaults.VLine = value; - } + public static Rune VLine => GlyphSettings.Current.VLine; /// Box Drawings Double Horizontal (U+2550) - ═ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDbl - { - get => GlyphSettings.Defaults.HLineDbl; - set => GlyphSettings.Defaults.HLineDbl = value; - } + public static Rune HLineDbl => GlyphSettings.Current.HLineDbl; /// Box Drawings Double Vertical (U+2551) - â•‘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDbl - - { - - get => GlyphSettings.Defaults.VLineDbl; - - set => GlyphSettings.Defaults.VLineDbl = value; - - } + public static Rune VLineDbl => GlyphSettings.Current.VLineDbl; /// Box Drawings Heavy Double Dash Horizontal (U+254D) - ╍ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa2 - - { - - get => GlyphSettings.Defaults.HLineHvDa2; - - set => GlyphSettings.Defaults.HLineHvDa2 = value; - - } + public static Rune HLineHvDa2 => GlyphSettings.Current.HLineHvDa2; /// Box Drawings Heavy Triple Dash Vertical (U+2507) - ┇ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa3 - { - get => GlyphSettings.Defaults.VLineHvDa3; - set => GlyphSettings.Defaults.VLineHvDa3 = value; - } + public static Rune VLineHvDa3 => GlyphSettings.Current.VLineHvDa3; /// Box Drawings Heavy Triple Dash Horizontal (U+2505) - â”… - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa3 - - { - - get => GlyphSettings.Defaults.HLineHvDa3; - - set => GlyphSettings.Defaults.HLineHvDa3 = value; - - } + public static Rune HLineHvDa3 => GlyphSettings.Current.HLineHvDa3; /// Box Drawings Heavy Quadruple Dash Horizontal (U+2509) - ┉ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHvDa4 - - { - - get => GlyphSettings.Defaults.HLineHvDa4; - - set => GlyphSettings.Defaults.HLineHvDa4 = value; - - } + public static Rune HLineHvDa4 => GlyphSettings.Current.HLineHvDa4; /// Box Drawings Heavy Double Dash Vertical (U+254F) - ╏ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa2 - - { - - get => GlyphSettings.Defaults.VLineHvDa2; - - set => GlyphSettings.Defaults.VLineHvDa2 = value; - - } + public static Rune VLineHvDa2 => GlyphSettings.Current.VLineHvDa2; /// Box Drawings Heavy Quadruple Dash Vertical (U+250B) - ┋ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHvDa4 - - { - - get => GlyphSettings.Defaults.VLineHvDa4; - - set => GlyphSettings.Defaults.VLineHvDa4 = value; - - } + public static Rune VLineHvDa4 => GlyphSettings.Current.VLineHvDa4; /// Box Drawings Light Double Dash Horizontal (U+254C) - ╌ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa2 - - { - - get => GlyphSettings.Defaults.HLineDa2; - - set => GlyphSettings.Defaults.HLineDa2 = value; - - } + public static Rune HLineDa2 => GlyphSettings.Current.HLineDa2; /// Box Drawings Light Triple Dash Vertical (U+2506) - ┆ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa3 - - { - - get => GlyphSettings.Defaults.VLineDa3; - - set => GlyphSettings.Defaults.VLineDa3 = value; - - } + public static Rune VLineDa3 => GlyphSettings.Current.VLineDa3; /// Box Drawings Light Triple Dash Horizontal (U+2504) - ┄ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa3 - - { - - get => GlyphSettings.Defaults.HLineDa3; - - set => GlyphSettings.Defaults.HLineDa3 = value; - - } + public static Rune HLineDa3 => GlyphSettings.Current.HLineDa3; /// Box Drawings Light Quadruple Dash Horizontal (U+2508) - ┈ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineDa4 - - { - - get => GlyphSettings.Defaults.HLineDa4; - - set => GlyphSettings.Defaults.HLineDa4 = value; - - } + public static Rune HLineDa4 => GlyphSettings.Current.HLineDa4; /// Box Drawings Light Double Dash Vertical (U+254E) - ╎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa2 - - { - - get => GlyphSettings.Defaults.VLineDa2; - - set => GlyphSettings.Defaults.VLineDa2 = value; - - } + public static Rune VLineDa2 => GlyphSettings.Current.VLineDa2; /// Box Drawings Light Quadruple Dash Vertical (U+250A) - ┊ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineDa4 - - { - - get => GlyphSettings.Defaults.VLineDa4; - - set => GlyphSettings.Defaults.VLineDa4 = value; - - } + public static Rune VLineDa4 => GlyphSettings.Current.VLineDa4; /// Box Drawings Heavy Horizontal (U+2501) - ━ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HLineHv - - { - - get => GlyphSettings.Defaults.HLineHv; - - set => GlyphSettings.Defaults.HLineHv = value; - - } + public static Rune HLineHv => GlyphSettings.Current.HLineHv; /// Box Drawings Heavy Vertical (U+2503) - ┃ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune VLineHv - - { - - get => GlyphSettings.Defaults.VLineHv; - - set => GlyphSettings.Defaults.VLineHv = value; - - } + public static Rune VLineHv => GlyphSettings.Current.VLineHv; /// Box Drawings Light Left (U+2574) - â•´ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfLeftLine - - { - - get => GlyphSettings.Defaults.HalfLeftLine; - - set => GlyphSettings.Defaults.HalfLeftLine = value; - - } + public static Rune HalfLeftLine => GlyphSettings.Current.HalfLeftLine; /// Box Drawings Light Vertical (U+2575) - ╵ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfTopLine - - { - - get => GlyphSettings.Defaults.HalfTopLine; - - set => GlyphSettings.Defaults.HalfTopLine = value; - - } + public static Rune HalfTopLine => GlyphSettings.Current.HalfTopLine; /// Box Drawings Light Horizontal (U+2576) - â•¶ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfRightLine - - { - - get => GlyphSettings.Defaults.HalfRightLine; - - set => GlyphSettings.Defaults.HalfRightLine = value; - - } + public static Rune HalfRightLine => GlyphSettings.Current.HalfRightLine; /// Box Drawings Light Down (U+2577) - â•· - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfBottomLine - - { - - get => GlyphSettings.Defaults.HalfBottomLine; - - set => GlyphSettings.Defaults.HalfBottomLine = value; - - } + public static Rune HalfBottomLine => GlyphSettings.Current.HalfBottomLine; /// Box Drawings Heavy Left (U+2578) - ╸ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfLeftLineHv - - { - - get => GlyphSettings.Defaults.HalfLeftLineHv; - - set => GlyphSettings.Defaults.HalfLeftLineHv = value; - - } + public static Rune HalfLeftLineHv => GlyphSettings.Current.HalfLeftLineHv; /// Box Drawings Heavy Vertical (U+2579) - ╹ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfTopLineHv - - { - - get => GlyphSettings.Defaults.HalfTopLineHv; - - set => GlyphSettings.Defaults.HalfTopLineHv = value; - - } + public static Rune HalfTopLineHv => GlyphSettings.Current.HalfTopLineHv; /// Box Drawings Heavy Horizontal (U+257A) - ╺ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfRightLineHv - - { - - get => GlyphSettings.Defaults.HalfRightLineHv; - - set => GlyphSettings.Defaults.HalfRightLineHv = value; - - } + public static Rune HalfRightLineHv => GlyphSettings.Current.HalfRightLineHv; /// Box Drawings Light Vertical and Horizontal (U+257B) - â•» - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune HalfBottomLineLt - - { - - get => GlyphSettings.Defaults.HalfBottomLineLt; - - set => GlyphSettings.Defaults.HalfBottomLineLt = value; - - } + public static Rune HalfBottomLineLt => GlyphSettings.Current.HalfBottomLineLt; /// Box Drawings Light Horizontal and Heavy Horizontal (U+257C) - ╼ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightSideLineLtHv - - { - - get => GlyphSettings.Defaults.RightSideLineLtHv; - - set => GlyphSettings.Defaults.RightSideLineLtHv = value; - - } + public static Rune RightSideLineLtHv => GlyphSettings.Current.RightSideLineLtHv; /// Box Drawings Light Vertical and Heavy Horizontal (U+257D) - ╽ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomSideLineLtHv - - { - - get => GlyphSettings.Defaults.BottomSideLineLtHv; - - set => GlyphSettings.Defaults.BottomSideLineLtHv = value; - - } + public static Rune BottomSideLineLtHv => GlyphSettings.Current.BottomSideLineLtHv; /// Box Drawings Heavy Left and Light Horizontal (U+257E) - ╾ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftSideLineHvLt - - { - - get => GlyphSettings.Defaults.LeftSideLineHvLt; - - set => GlyphSettings.Defaults.LeftSideLineHvLt = value; - - } + public static Rune LeftSideLineHvLt => GlyphSettings.Current.LeftSideLineHvLt; /// Box Drawings Heavy Vertical and Light Horizontal (U+257F) - â•¿ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopSideLineHvLt - - { - - get => GlyphSettings.Defaults.TopSideLineHvLt; - - set => GlyphSettings.Defaults.TopSideLineHvLt = value; - - } + public static Rune TopSideLineHvLt => GlyphSettings.Current.TopSideLineHvLt; #endregion @@ -745,107 +277,35 @@ public static Rune TopSideLineHvLt /// Box Drawings Upper Left Corner - Light Vertical and Light Horizontal (U+250C) - ┌ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCorner - - { - - get => GlyphSettings.Defaults.ULCorner; - - set => GlyphSettings.Defaults.ULCorner = value; - - } + public static Rune ULCorner => GlyphSettings.Current.ULCorner; /// Box Drawings Upper Left Corner - Double (U+2554) - â•” - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerDbl - - { - - get => GlyphSettings.Defaults.ULCornerDbl; - - set => GlyphSettings.Defaults.ULCornerDbl = value; - - } + public static Rune ULCornerDbl => GlyphSettings.Current.ULCornerDbl; /// Box Drawings Upper Left Corner - Light Arc Down and Horizontal (U+256D) - â•­ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerR - - { - - get => GlyphSettings.Defaults.ULCornerR; - - set => GlyphSettings.Defaults.ULCornerR = value; - - } + public static Rune ULCornerR => GlyphSettings.Current.ULCornerR; /// Box Drawings Heavy Down and Horizontal (U+250F) - ┏ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerHv - - { - - get => GlyphSettings.Defaults.ULCornerHv; - - set => GlyphSettings.Defaults.ULCornerHv = value; - - } + public static Rune ULCornerHv => GlyphSettings.Current.ULCornerHv; /// Box Drawings Down Heavy and Horizontal Light (U+251E) - ┎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerHvLt - - { - - get => GlyphSettings.Defaults.ULCornerHvLt; - - set => GlyphSettings.Defaults.ULCornerHvLt = value; - - } + public static Rune ULCornerHvLt => GlyphSettings.Current.ULCornerHvLt; /// Box Drawings Down Light and Horizontal Heavy (U+250D) - ┎ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerLtHv - - { - - get => GlyphSettings.Defaults.ULCornerLtHv; - - set => GlyphSettings.Defaults.ULCornerLtHv = value; - - } + public static Rune ULCornerLtHv => GlyphSettings.Current.ULCornerLtHv; /// Box Drawings Double Down and Single Horizontal (U+2553) - â•“ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerDblSingle - - { - - get => GlyphSettings.Defaults.ULCornerDblSingle; - - set => GlyphSettings.Defaults.ULCornerDblSingle = value; - - } + public static Rune ULCornerDblSingle => GlyphSettings.Current.ULCornerDblSingle; /// Box Drawings Single Down and Double Horizontal (U+2552) - â•’ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ULCornerSingleDbl - - { - - get => GlyphSettings.Defaults.ULCornerSingleDbl; - - set => GlyphSettings.Defaults.ULCornerSingleDbl = value; - - } + public static Rune ULCornerSingleDbl => GlyphSettings.Current.ULCornerSingleDbl; #endregion @@ -853,107 +313,35 @@ public static Rune ULCornerSingleDbl /// Box Drawings Lower Left Corner - Light Vertical and Light Horizontal (U+2514) - â”” - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCorner - - { - - get => GlyphSettings.Defaults.LLCorner; - - set => GlyphSettings.Defaults.LLCorner = value; - - } + public static Rune LLCorner => GlyphSettings.Current.LLCorner; /// Box Drawings Heavy Vertical and Horizontal (U+2517) - â”— - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerHv - - { - - get => GlyphSettings.Defaults.LLCornerHv; - - set => GlyphSettings.Defaults.LLCornerHv = value; - - } + public static Rune LLCornerHv => GlyphSettings.Current.LLCornerHv; /// Box Drawings Heavy Vertical and Horizontal Light (U+2516) - â”– - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerHvLt - - { - - get => GlyphSettings.Defaults.LLCornerHvLt; - - set => GlyphSettings.Defaults.LLCornerHvLt = value; - - } + public static Rune LLCornerHvLt => GlyphSettings.Current.LLCornerHvLt; /// Box Drawings Vertical Light and Horizontal Heavy (U+2511) - ┕ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerLtHv - - { - - get => GlyphSettings.Defaults.LLCornerLtHv; - - set => GlyphSettings.Defaults.LLCornerLtHv = value; - - } + public static Rune LLCornerLtHv => GlyphSettings.Current.LLCornerLtHv; /// Box Drawings Double Vertical and Double Left (U+255A) - ╚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerDbl - - { - - get => GlyphSettings.Defaults.LLCornerDbl; - - set => GlyphSettings.Defaults.LLCornerDbl = value; - - } + public static Rune LLCornerDbl => GlyphSettings.Current.LLCornerDbl; /// Box Drawings Single Vertical and Double Left (U+2558) - ╘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerSingleDbl - - { - - get => GlyphSettings.Defaults.LLCornerSingleDbl; - - set => GlyphSettings.Defaults.LLCornerSingleDbl = value; - - } + public static Rune LLCornerSingleDbl => GlyphSettings.Current.LLCornerSingleDbl; /// Box Drawings Double Down and Single Left (U+2559) - â•™ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerDblSingle - - { - - get => GlyphSettings.Defaults.LLCornerDblSingle; - - set => GlyphSettings.Defaults.LLCornerDblSingle = value; - - } + public static Rune LLCornerDblSingle => GlyphSettings.Current.LLCornerDblSingle; /// Box Drawings Upper Left Corner - Light Arc Down and Left (U+2570) - â•° - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LLCornerR - - { - - get => GlyphSettings.Defaults.LLCornerR; - - set => GlyphSettings.Defaults.LLCornerR = value; - - } + public static Rune LLCornerR => GlyphSettings.Current.LLCornerR; #endregion @@ -961,107 +349,35 @@ public static Rune LLCornerR /// Box Drawings Upper Horizontal Corner - Light Vertical and Light Horizontal (U+2510) - ┐ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCorner - - { - - get => GlyphSettings.Defaults.URCorner; - - set => GlyphSettings.Defaults.URCorner = value; - - } + public static Rune URCorner => GlyphSettings.Current.URCorner; /// Box Drawings Upper Horizontal Corner - Double Vertical and Double Horizontal (U+2557) - â•— - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerDbl - - { - - get => GlyphSettings.Defaults.URCornerDbl; - - set => GlyphSettings.Defaults.URCornerDbl = value; - - } + public static Rune URCornerDbl => GlyphSettings.Current.URCornerDbl; /// Box Drawings Upper Horizontal Corner - Light Arc Vertical and Horizontal (U+256E) - â•® - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerR - - { - - get => GlyphSettings.Defaults.URCornerR; - - set => GlyphSettings.Defaults.URCornerR = value; - - } + public static Rune URCornerR => GlyphSettings.Current.URCornerR; /// Box Drawings Heavy Down and Left (U+2513) - ┓ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerHv - - { - - get => GlyphSettings.Defaults.URCornerHv; - - set => GlyphSettings.Defaults.URCornerHv = value; - - } + public static Rune URCornerHv => GlyphSettings.Current.URCornerHv; /// Box Drawings Heavy Vertical and Left Down Light (U+2511) - ┑ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerHvLt - - { - - get => GlyphSettings.Defaults.URCornerHvLt; - - set => GlyphSettings.Defaults.URCornerHvLt = value; - - } + public static Rune URCornerHvLt => GlyphSettings.Current.URCornerHvLt; /// Box Drawings Down Light and Horizontal Heavy (U+2514) - â”’ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerLtHv - - { - - get => GlyphSettings.Defaults.URCornerLtHv; - - set => GlyphSettings.Defaults.URCornerLtHv = value; - - } + public static Rune URCornerLtHv => GlyphSettings.Current.URCornerLtHv; /// Box Drawings Double Vertical and Single Left (U+2556) - â•– - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerDblSingle - - { - - get => GlyphSettings.Defaults.URCornerDblSingle; - - set => GlyphSettings.Defaults.URCornerDblSingle = value; - - } + public static Rune URCornerDblSingle => GlyphSettings.Current.URCornerDblSingle; /// Box Drawings Single Vertical and Double Left (U+2555) - â•• - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune URCornerSingleDbl - - { - - get => GlyphSettings.Defaults.URCornerSingleDbl; - - set => GlyphSettings.Defaults.URCornerSingleDbl = value; - - } + public static Rune URCornerSingleDbl => GlyphSettings.Current.URCornerSingleDbl; #endregion @@ -1069,107 +385,35 @@ public static Rune URCornerSingleDbl /// Box Drawings Lower Right Corner - Light (U+2518) - ┘ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCorner - - { - - get => GlyphSettings.Defaults.LRCorner; - - set => GlyphSettings.Defaults.LRCorner = value; - - } + public static Rune LRCorner => GlyphSettings.Current.LRCorner; /// Box Drawings Lower Right Corner - Double (U+255D) - ╝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerDbl - - { - - get => GlyphSettings.Defaults.LRCornerDbl; - - set => GlyphSettings.Defaults.LRCornerDbl = value; - - } + public static Rune LRCornerDbl => GlyphSettings.Current.LRCornerDbl; /// Box Drawings Lower Right Corner - Rounded (U+256F) - ╯ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerR - - { - - get => GlyphSettings.Defaults.LRCornerR; - - set => GlyphSettings.Defaults.LRCornerR = value; - - } + public static Rune LRCornerR => GlyphSettings.Current.LRCornerR; /// Box Drawings Lower Right Corner - Heavy (U+251B) - â”› - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerHv - - { - - get => GlyphSettings.Defaults.LRCornerHv; - - set => GlyphSettings.Defaults.LRCornerHv = value; - - } + public static Rune LRCornerHv => GlyphSettings.Current.LRCornerHv; /// Box Drawings Lower Right Corner - Double Vertical and Single Horizontal (U+255C) - ╜ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerDblSingle - - { - - get => GlyphSettings.Defaults.LRCornerDblSingle; - - set => GlyphSettings.Defaults.LRCornerDblSingle = value; - - } + public static Rune LRCornerDblSingle => GlyphSettings.Current.LRCornerDblSingle; /// Box Drawings Lower Right Corner - Single Vertical and Double Horizontal (U+255B) - â•› - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerSingleDbl - - { - - get => GlyphSettings.Defaults.LRCornerSingleDbl; - - set => GlyphSettings.Defaults.LRCornerSingleDbl = value; - - } + public static Rune LRCornerSingleDbl => GlyphSettings.Current.LRCornerSingleDbl; /// Box Drawings Lower Right Corner - Light Vertical and Heavy Horizontal (U+2519) - â”™ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerLtHv - - { - - get => GlyphSettings.Defaults.LRCornerLtHv; - - set => GlyphSettings.Defaults.LRCornerLtHv = value; - - } + public static Rune LRCornerLtHv => GlyphSettings.Current.LRCornerLtHv; /// Box Drawings Lower Right Corner - Heavy Vertical and Light Horizontal (U+251A) - ┚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LRCornerHvLt - - { - - get => GlyphSettings.Defaults.LRCornerHvLt; - - set => GlyphSettings.Defaults.LRCornerHvLt = value; - - } + public static Rune LRCornerHvLt => GlyphSettings.Current.LRCornerHvLt; #endregion @@ -1177,367 +421,115 @@ public static Rune LRCornerHvLt /// Box Drawings Left Tee - Single Vertical and Single Horizontal (U+251C) - ├ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTee - - { - - get => GlyphSettings.Defaults.LeftTee; - - set => GlyphSettings.Defaults.LeftTee = value; - - } + public static Rune LeftTee => GlyphSettings.Current.LeftTee; /// Box Drawings Left Tee - Single Vertical and Double Horizontal (U+255E) - ╞ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDblH - - { - - get => GlyphSettings.Defaults.LeftTeeDblH; - - set => GlyphSettings.Defaults.LeftTeeDblH = value; - - } + public static Rune LeftTeeDblH => GlyphSettings.Current.LeftTeeDblH; /// Box Drawings Left Tee - Double Vertical and Single Horizontal (U+255F) - ╟ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDblV - - { - - get => GlyphSettings.Defaults.LeftTeeDblV; - - set => GlyphSettings.Defaults.LeftTeeDblV = value; - - } + public static Rune LeftTeeDblV => GlyphSettings.Current.LeftTeeDblV; /// Box Drawings Left Tee - Double Vertical and Double Horizontal (U+2560) - â•  - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeDbl - - { - - get => GlyphSettings.Defaults.LeftTeeDbl; - - set => GlyphSettings.Defaults.LeftTeeDbl = value; - - } + public static Rune LeftTeeDbl => GlyphSettings.Current.LeftTeeDbl; /// Box Drawings Left Tee - Heavy Horizontal and Light Vertical (U+2523) - ┝ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvH - - { - - get => GlyphSettings.Defaults.LeftTeeHvH; - - set => GlyphSettings.Defaults.LeftTeeHvH = value; - - } + public static Rune LeftTeeHvH => GlyphSettings.Current.LeftTeeHvH; /// Box Drawings Left Tee - Light Horizontal and Heavy Vertical (U+252B) - â”  - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvV - - { - - get => GlyphSettings.Defaults.LeftTeeHvV; - - set => GlyphSettings.Defaults.LeftTeeHvV = value; - - } + public static Rune LeftTeeHvV => GlyphSettings.Current.LeftTeeHvV; /// Box Drawings Left Tee - Heavy Vertical and Heavy Horizontal (U+2527) - ┣ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune LeftTeeHvDblH - - { - - get => GlyphSettings.Defaults.LeftTeeHvDblH; - - set => GlyphSettings.Defaults.LeftTeeHvDblH = value; - - } + public static Rune LeftTeeHvDblH => GlyphSettings.Current.LeftTeeHvDblH; /// Box Drawings Right Tee - Single Vertical and Single Horizontal (U+2524) - ┤ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTee - - { - - get => GlyphSettings.Defaults.RightTee; - - set => GlyphSettings.Defaults.RightTee = value; - - } + public static Rune RightTee => GlyphSettings.Current.RightTee; /// Box Drawings Right Tee - Single Vertical and Double Horizontal (U+2561) - â•¡ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDblH - - { - - get => GlyphSettings.Defaults.RightTeeDblH; - - set => GlyphSettings.Defaults.RightTeeDblH = value; - - } + public static Rune RightTeeDblH => GlyphSettings.Current.RightTeeDblH; /// Box Drawings Right Tee - Double Vertical and Single Horizontal (U+2562) - â•¢ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDblV - - { - - get => GlyphSettings.Defaults.RightTeeDblV; - - set => GlyphSettings.Defaults.RightTeeDblV = value; - - } + public static Rune RightTeeDblV => GlyphSettings.Current.RightTeeDblV; /// Box Drawings Right Tee - Double Vertical and Double Horizontal (U+2563) - â•£ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeDbl - - { - - get => GlyphSettings.Defaults.RightTeeDbl; - - set => GlyphSettings.Defaults.RightTeeDbl = value; - - } + public static Rune RightTeeDbl => GlyphSettings.Current.RightTeeDbl; /// Box Drawings Right Tee - Heavy Horizontal and Light Vertical (U+2528) - ┥ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvH - - { - - get => GlyphSettings.Defaults.RightTeeHvH; - - set => GlyphSettings.Defaults.RightTeeHvH = value; - - } + public static Rune RightTeeHvH => GlyphSettings.Current.RightTeeHvH; /// Box Drawings Right Tee - Light Horizontal and Heavy Vertical (U+2530) - ┨ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvV - - { - - get => GlyphSettings.Defaults.RightTeeHvV; - - set => GlyphSettings.Defaults.RightTeeHvV = value; - - } + public static Rune RightTeeHvV => GlyphSettings.Current.RightTeeHvV; /// Box Drawings Right Tee - Heavy Vertical and Heavy Horizontal (U+252C) - ┫ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune RightTeeHvDblH - - { - - get => GlyphSettings.Defaults.RightTeeHvDblH; - - set => GlyphSettings.Defaults.RightTeeHvDblH = value; - - } + public static Rune RightTeeHvDblH => GlyphSettings.Current.RightTeeHvDblH; /// Box Drawings Top Tee - Single Vertical and Single Horizontal (U+252C) - ┬ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTee - - { - - get => GlyphSettings.Defaults.TopTee; - - set => GlyphSettings.Defaults.TopTee = value; - - } + public static Rune TopTee => GlyphSettings.Current.TopTee; /// Box Drawings Top Tee - Single Vertical and Double Horizontal (U+2564) - ╤ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDblH - - { - - get => GlyphSettings.Defaults.TopTeeDblH; - - set => GlyphSettings.Defaults.TopTeeDblH = value; - - } + public static Rune TopTeeDblH => GlyphSettings.Current.TopTeeDblH; /// Box Drawings Top Tee - Double Vertical and Single Horizontal (U+2565) - â•¥ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDblV - - { - - get => GlyphSettings.Defaults.TopTeeDblV; - - set => GlyphSettings.Defaults.TopTeeDblV = value; - - } + public static Rune TopTeeDblV => GlyphSettings.Current.TopTeeDblV; /// Box Drawings Top Tee - Double Vertical and Double Horizontal (U+2566) - ╦ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeDbl - - { - - get => GlyphSettings.Defaults.TopTeeDbl; - - set => GlyphSettings.Defaults.TopTeeDbl = value; - - } + public static Rune TopTeeDbl => GlyphSettings.Current.TopTeeDbl; /// Box Drawings Top Tee - Heavy Horizontal and Light Vertical (U+252F) - ┯ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvH - - { - - get => GlyphSettings.Defaults.TopTeeHvH; - - set => GlyphSettings.Defaults.TopTeeHvH = value; - - } + public static Rune TopTeeHvH => GlyphSettings.Current.TopTeeHvH; /// Box Drawings Top Tee - Light Horizontal and Heavy Vertical (U+2537) - â”° - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvV - - { - - get => GlyphSettings.Defaults.TopTeeHvV; - - set => GlyphSettings.Defaults.TopTeeHvV = value; - - } + public static Rune TopTeeHvV => GlyphSettings.Current.TopTeeHvV; /// Box Drawings Top Tee - Heavy Vertical and Heavy Horizontal (U+2533) - ┳ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune TopTeeHvDblH - - { - - get => GlyphSettings.Defaults.TopTeeHvDblH; - - set => GlyphSettings.Defaults.TopTeeHvDblH = value; - - } + public static Rune TopTeeHvDblH => GlyphSettings.Current.TopTeeHvDblH; /// Box Drawings Bottom Tee - Single Vertical and Single Horizontal (U+2534) - â”´ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTee - - { - - get => GlyphSettings.Defaults.BottomTee; - - set => GlyphSettings.Defaults.BottomTee = value; - - } + public static Rune BottomTee => GlyphSettings.Current.BottomTee; /// Box Drawings Bottom Tee - Single Vertical and Double Horizontal (U+2567) - â•§ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDblH - - { - - get => GlyphSettings.Defaults.BottomTeeDblH; - - set => GlyphSettings.Defaults.BottomTeeDblH = value; - - } + public static Rune BottomTeeDblH => GlyphSettings.Current.BottomTeeDblH; /// Box Drawings Bottom Tee - Double Vertical and Single Horizontal (U+2568) - ╨ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDblV - - { - - get => GlyphSettings.Defaults.BottomTeeDblV; - - set => GlyphSettings.Defaults.BottomTeeDblV = value; - - } + public static Rune BottomTeeDblV => GlyphSettings.Current.BottomTeeDblV; /// Box Drawings Bottom Tee - Double Vertical and Double Horizontal (U+2569) - â•© - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeDbl - - { - - get => GlyphSettings.Defaults.BottomTeeDbl; - - set => GlyphSettings.Defaults.BottomTeeDbl = value; - - } + public static Rune BottomTeeDbl => GlyphSettings.Current.BottomTeeDbl; /// Box Drawings Bottom Tee - Heavy Horizontal and Light Vertical (U+2535) - â”· - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvH - - { - - get => GlyphSettings.Defaults.BottomTeeHvH; - - set => GlyphSettings.Defaults.BottomTeeHvH = value; - - } + public static Rune BottomTeeHvH => GlyphSettings.Current.BottomTeeHvH; /// Box Drawings Bottom Tee - Light Horizontal and Heavy Vertical (U+253D) - ┸ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvV - - { - - get => GlyphSettings.Defaults.BottomTeeHvV; - - set => GlyphSettings.Defaults.BottomTeeHvV = value; - - } + public static Rune BottomTeeHvV => GlyphSettings.Current.BottomTeeHvV; /// Box Drawings Bottom Tee - Heavy Vertical and Heavy Horizontal (U+2539) - â”» - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune BottomTeeHvDblH - - { - - get => GlyphSettings.Defaults.BottomTeeHvDblH; - - set => GlyphSettings.Defaults.BottomTeeHvDblH = value; - - } + public static Rune BottomTeeHvDblH => GlyphSettings.Current.BottomTeeHvDblH; #endregion @@ -1545,94 +537,31 @@ public static Rune BottomTeeHvDblH /// Box Drawings Cross - Single Vertical and Single Horizontal (U+253C) - ┼ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune Cross - - { - - get => GlyphSettings.Defaults.Cross; - - set => GlyphSettings.Defaults.Cross = value; - - } + public static Rune Cross => GlyphSettings.Current.Cross; /// Box Drawings Cross - Single Vertical and Double Horizontal (U+256A) - ╪ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDblH - - { - - get => GlyphSettings.Defaults.CrossDblH; - - set => GlyphSettings.Defaults.CrossDblH = value; - - } + public static Rune CrossDblH => GlyphSettings.Current.CrossDblH; /// Box Drawings Cross - Double Vertical and Single Horizontal (U+256B) - â•« - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDblV - - { - - get => GlyphSettings.Defaults.CrossDblV; - - set => GlyphSettings.Defaults.CrossDblV = value; - - } + public static Rune CrossDblV => GlyphSettings.Current.CrossDblV; /// Box Drawings Cross - Double Vertical and Double Horizontal (U+256C) - ╬ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossDbl - - { - - get => GlyphSettings.Defaults.CrossDbl; - - set => GlyphSettings.Defaults.CrossDbl = value; - - } + public static Rune CrossDbl => GlyphSettings.Current.CrossDbl; /// Box Drawings Cross - Heavy Horizontal and Light Vertical (U+253F) - ┿ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHvH - - { - - get => GlyphSettings.Defaults.CrossHvH; - - set => GlyphSettings.Defaults.CrossHvH = value; - - } + public static Rune CrossHvH => GlyphSettings.Current.CrossHvH; /// Box Drawings Cross - Light Horizontal and Heavy Vertical (U+2541) - â•‚ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHvV - - { - - get => GlyphSettings.Defaults.CrossHvV; - - set => GlyphSettings.Defaults.CrossHvV = value; - - } + public static Rune CrossHvV => GlyphSettings.Current.CrossHvV; /// Box Drawings Cross - Heavy Vertical and Heavy Horizontal (U+254B) - â•‹ - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CrossHv - - { - - get => GlyphSettings.Defaults.CrossHv; - - set => GlyphSettings.Defaults.CrossHv = value; - - } + public static Rune CrossHv => GlyphSettings.Current.CrossHv; #endregion @@ -1640,68 +569,23 @@ public static Rune CrossHv /// Shadow - Vertical Start - Left Half Block - â–Œ U+0258c - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowVerticalStart // Half: '\u2596' â––; - - { - - get => GlyphSettings.Defaults.ShadowVerticalStart; - - set => GlyphSettings.Defaults.ShadowVerticalStart = value; - - } + public static Rune ShadowVerticalStart => GlyphSettings.Current.ShadowVerticalStart; /// Shadow - Vertical - Left Half Block - â–Œ U+0258c - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowVertical - - { - - get => GlyphSettings.Defaults.ShadowVertical; - - set => GlyphSettings.Defaults.ShadowVertical = value; - - } + public static Rune ShadowVertical => GlyphSettings.Current.ShadowVertical; /// Shadow - Horizontal Start - Upper Half Block - â–€ U+02580 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontalStart // Half: ▝ U+0259d; - - { - - get => GlyphSettings.Defaults.ShadowHorizontalStart; - - set => GlyphSettings.Defaults.ShadowHorizontalStart = value; - - } + public static Rune ShadowHorizontalStart => GlyphSettings.Current.ShadowHorizontalStart; /// Shadow - Horizontal - Upper Half Block - â–€ U+02580 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontal - - { - - get => GlyphSettings.Defaults.ShadowHorizontal; - - set => GlyphSettings.Defaults.ShadowHorizontal = value; - - } + public static Rune ShadowHorizontal => GlyphSettings.Current.ShadowHorizontal; /// Shadow - Horizontal End - Quadrant Upper Left - â–˜ U+02598 - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune ShadowHorizontalEnd - - { - - get => GlyphSettings.Defaults.ShadowHorizontalEnd; - - set => GlyphSettings.Defaults.ShadowHorizontalEnd = value; - - } + public static Rune ShadowHorizontalEnd => GlyphSettings.Current.ShadowHorizontalEnd; #endregion } \ No newline at end of file diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index a67ee8dd16..818433828c 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -1100,7 +1100,7 @@ static bool AnyWithOrientationAndAnyLineStyle (ReadOnlySpan - /// Sets the glyphs used. Call this method after construction and any time ConfigurationManager has updated the + /// Sets the glyphs used. Call this method after construction and any time theme overlays have updated the /// settings. /// public abstract void SetGlyphs (); diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index 7590cdf9cb..03ffac2a02 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -172,7 +172,7 @@ public record Scheme : IEqualityOperators { /// /// INTERNAL: Gets the hard-coded set of s. Used for generating the built-in config.json and for - /// unit tests that don't depend on ConfigurationManager. + /// unit tests that don't depend on configuration. /// /// internal static ImmutableSortedDictionary GetHardCodedSchemes () diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiComponentFactory.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiComponentFactory.cs index 15c3a4379d..d20258f5c3 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiComponentFactory.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiComponentFactory.cs @@ -27,6 +27,7 @@ public class AnsiComponentFactory : ComponentFactoryImpl private readonly AnsiInput? _input; private readonly IOutput? _output; private readonly ISizeMonitor? _injectedSizeMonitor; + private readonly Func? _nativeSizeQuery; /// /// Creates a new ANSIComponentFactory with optional output capture. @@ -38,10 +39,15 @@ public class AnsiComponentFactory : ComponentFactoryImpl /// ). /// public AnsiComponentFactory (AnsiInput? input = null, IOutput? output = null, ISizeMonitor? sizeMonitor = null) + : this (input, output, sizeMonitor, null) + { } + + internal AnsiComponentFactory (AnsiInput? input, IOutput? output, ISizeMonitor? sizeMonitor, Func? nativeSizeQuery) { _input = input; _output = output; _injectedSizeMonitor = sizeMonitor; + _nativeSizeQuery = nativeSizeQuery; } /// @@ -60,15 +66,33 @@ public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBu if (Driver.SizeDetection != SizeDetectionMode.Polling) { + ansiOutput.NativeSizeQuery = null; + return new AnsiSizeMonitor (ansiOutput); } - // Polling mode: wire up a platform-native size query so that - // AnsiOutput.GetSize() returns the real terminal size via - // ioctl(TIOCGWINSZ) on Unix or the Console API on Windows. - ansiOutput.NativeSizeQuery = CreateNativeSizeQuery (); + if (ansiOutput.NativeSizeQuery is null && !TryConfigureNativeSizeQuery (ansiOutput)) + { + return new AnsiSizeMonitor (ansiOutput); + } - return new SizeMonitorImpl (ansiOutput); + return new AnsiSizeMonitor (ansiOutput, queryTerminalSize: false); + } + + private bool TryConfigureNativeSizeQuery (AnsiOutput output) + { + Func nativeSizeQuery = output.NativeSizeQuery ?? _nativeSizeQuery ?? CreateNativeSizeQuery (); + Size? initialSize = nativeSizeQuery (); + + if (initialSize is null) + { + return false; + } + + output.NativeSizeQuery = nativeSizeQuery; + output.SetSize (initialSize.Value.Width, initialSize.Value.Height); + + return true; } /// @@ -78,7 +102,7 @@ public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBu /// internal static Func CreateNativeSizeQuery () { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (OperatingSystem.IsWindows ()) { return () => { @@ -109,5 +133,16 @@ public override IInputProcessor CreateInputProcessor (ConcurrentQueue inpu new AnsiInputProcessor (inputBuffer, timeProvider); /// - public override IOutput CreateOutput () => _output ?? new AnsiOutput (AppModel); + public override IOutput CreateOutput () + { + IOutput output = _output ?? new AnsiOutput (AppModel); + + if (Driver.SizeDetection == SizeDetectionMode.Polling + && output is AnsiOutput { NativeSizeQuery: null } ansiOutput) + { + TryConfigureNativeSizeQuery (ansiOutput); + } + + return output; + } } diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs index b40923314c..3ef9aa05d7 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs @@ -94,7 +94,7 @@ public AnsiInput () } // Initialize platform-specific input helpers - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { _windowsVTInput = new WindowsVTInputHelper (); @@ -111,7 +111,7 @@ public AnsiInput () } _platform = AnsiPlatform.WindowsVT; } - else if (PlatformDetection.IsUnixLike ()) + else { try { @@ -139,10 +139,6 @@ public AnsiInput () $"Failed to enable Unix raw input mode. libc not available: {ex.Message}. Running in degraded mode."); } } - else - { - Trace.Lifecycle (nameof (AnsiInput), "Init", "Unknown OS platform. Terminal input will not work. Running in degraded mode."); - } // NOTE: Output operations (alternate buffer, cursor visibility, mouse events) // NOTE: are handled by ANSIOutput, not here. ANSIInput only handles input. diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs index b4a800a87d..775f357c74 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs @@ -39,8 +39,9 @@ namespace Terminal.Gui.Drivers; /// public class AnsiInputProcessor : InputProcessorImpl { - private bool _isKittyKeyboardEnabled; - private string _pendingPrintableSuppression = string.Empty; + private long _inputPosition; + private string _pendingKittyPrintableSuppression = string.Empty; + private long _pendingKittyPrintableSuppressionStartPosition; /// /// The input buffer to process. @@ -53,6 +54,9 @@ public AnsiInputProcessor (ConcurrentQueue inputBuffer, ITimeProvider? tim /// protected override void Process (char input) { + _inputPosition++; + InvalidatePendingKittyPrintableSuppression (input); + foreach (Tuple released in Parser.ProcessInput (Tuple.Create (input, input))) { ProcessAfterParsing (released.Item2); @@ -60,11 +64,16 @@ protected override void Process (char input) } /// - protected override Key OnKeyboardEventParsed (Key keyEvent) + private protected override Key OnKeyboardEventParsed (AnsiKeyboardParserPattern pattern, Key keyEvent) { - _pendingPrintableSuppression = string.Empty; - - if (keyEvent.EventType != KeyEventType.Press || keyEvent.IsAlt || keyEvent.IsCtrl || keyEvent.IsModifierOnly) + keyEvent = base.OnKeyboardEventParsed (pattern, keyEvent); + ClearPendingKittyPrintableSuppression (); + + if (pattern is not KittyKeyboardPattern + || keyEvent.EventType != KeyEventType.Press + || keyEvent.IsAlt + || keyEvent.IsCtrl + || keyEvent.IsModifierOnly) { return keyEvent; } @@ -73,7 +82,8 @@ protected override Key OnKeyboardEventParsed (Key keyEvent) if (!string.IsNullOrEmpty (printableText)) { - _pendingPrintableSuppression = printableText; + _pendingKittyPrintableSuppression = printableText; + _pendingKittyPrintableSuppressionStartPosition = _inputPosition + 1; } return keyEvent; @@ -82,29 +92,46 @@ protected override Key OnKeyboardEventParsed (Key keyEvent) /// protected override bool ShouldSuppressFallbackKeyDown (Key key) { - if (string.IsNullOrEmpty (_pendingPrintableSuppression)) + string printableText = key.GetPrintableText (); + string pendingPrintableText = _pendingKittyPrintableSuppression; + long pendingStartPosition = _pendingKittyPrintableSuppressionStartPosition; + ClearPendingKittyPrintableSuppression (); + + if (string.IsNullOrEmpty (pendingPrintableText)) { - if (_isKittyKeyboardEnabled) - { - string fallbackPrintableText = key.GetPrintableText (); + return false; + } - if (!string.IsNullOrEmpty (fallbackPrintableText)) - { - _pendingPrintableSuppression = fallbackPrintableText; - } - } + long expectedLastPosition = pendingStartPosition + pendingPrintableText.Length - 1; - return false; + return _inputPosition == expectedLastPosition + && string.Equals (printableText, pendingPrintableText, StringComparison.Ordinal); + } + + private void InvalidatePendingKittyPrintableSuppression (char input) + { + if (string.IsNullOrEmpty (_pendingKittyPrintableSuppression)) + { + return; } - string printableText = key.GetPrintableText (); - bool suppress = string.Equals (printableText, _pendingPrintableSuppression, StringComparison.Ordinal); - _pendingPrintableSuppression = string.Empty; + long offset = _inputPosition - _pendingKittyPrintableSuppressionStartPosition; + + if (offset >= 0 + && offset < _pendingKittyPrintableSuppression.Length + && input == _pendingKittyPrintableSuppression [(int)offset]) + { + return; + } - return suppress; + ClearPendingKittyPrintableSuppression (); } - internal void SetKittyKeyboardEnabled (bool enabled) => _isKittyKeyboardEnabled = enabled; + private void ClearPendingKittyPrintableSuppression () + { + _pendingKittyPrintableSuppression = string.Empty; + _pendingKittyPrintableSuppressionStartPosition = 0; + } /// public override void InjectKeyDownEvent (Key key) diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs index 50647a7614..5dce76a584 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs @@ -94,7 +94,7 @@ public AnsiOutput (AppModel appModel = AppModel.FullScreen) } // Initialize platform-specific output helpers - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { _windowsVTOutput = new WindowsVTOutputHelper (); @@ -111,7 +111,7 @@ public AnsiOutput (AppModel appModel = AppModel.FullScreen) } _platform = AnsiPlatform.WindowsVT; } - else if (PlatformDetection.IsUnixLike ()) + else { // duplicate the controlling terminal output fd so we don't mess with it. // When stdout is redirected this is /dev/tty rather than STDOUT_FILENO, diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs index d96cfaf8c2..391208566f 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs @@ -4,22 +4,22 @@ namespace Terminal.Gui.Drivers; /// -/// Size monitor that uses ANSI escape sequences to query terminal size. -/// This demonstrates proper use of for detecting terminal resize events. +/// Size monitor for that uses either ANSI escape sequences or a native query +/// to detect terminal resize events. /// /// /// -/// Unlike platform-specific size monitors that use native APIs (e.g., SIGWINCH on Unix or -/// console buffer events on Windows), uses pure ANSI escape -/// sequences to query the terminal size, making it portable across all ANSI-compatible terminals. +/// In ANSI-query mode, sends CSI 18t. In native-query mode, +/// obtains the size from the operating system. Both modes retain +/// ANSI cursor-position discovery for inline applications. /// /// /// How it works: /// /// -/// sends periodically -/// Terminal responds with: ESC [ 8 ; height ; width t -/// parses the response and updates cached size +/// obtains the current size through the configured mode +/// ANSI-query mode parses the terminal's ESC [ 8 ; height ; width t response +/// Native-query mode reads the size through /// If size changed, event is raised /// /// @@ -29,6 +29,7 @@ internal class AnsiSizeMonitor : ISizeMonitor private static readonly TimeSpan StartupCursorPositionQueryTimeout = TimeSpan.FromMilliseconds (500); private readonly AnsiOutput _output; + private readonly bool _queryTerminalSize; private Action? _queueAnsiRequest; private IAnsiStartupGate? _startupGate; private IDisposable? _startupSizeQueryCompletionHandle; @@ -42,7 +43,7 @@ internal class AnsiSizeMonitor : ISizeMonitor /// public bool InitialSizeReceived => - _sizeResponseReceived + (!_queryTerminalSize || _sizeResponseReceived) && (_output.AppModel != AppModel.Inline || _cursorPositionReceived); /// @@ -53,10 +54,15 @@ internal class AnsiSizeMonitor : ISizeMonitor /// /// The ANSIOutput instance to query for size /// Callback to queue ANSI requests (provided by driver/scheduler) - public AnsiSizeMonitor (AnsiOutput output, Action? queueAnsiRequest = null) + /// + /// to query terminal size with CSI 18t; to use + /// the native query configured on . + /// + public AnsiSizeMonitor (AnsiOutput output, Action? queueAnsiRequest = null, bool queryTerminalSize = true) { _output = output; _queueAnsiRequest = queueAnsiRequest; + _queryTerminalSize = queryTerminalSize; // Get initial size from console or fallback _lastSize = _output.GetSize (); @@ -75,12 +81,15 @@ public void Initialize (IDriver? driver) _queueAnsiRequest = driver.QueueAnsiRequest; _startupGate = driver.AnsiStartupGate; - Trace.Lifecycle (nameof (AnsiSizeMonitor), "Initialize", "Driver wired up; sending initial size query"); - - _startupSizeQueryCompletionHandle = _startupGate?.RegisterQuery (AnsiStartupQuery.TerminalSize, StartupSizeQueryTimeout); _startupCursorPositionQueryCompletionHandle = _startupGate?.RegisterQuery (AnsiStartupQuery.CursorPosition, StartupCursorPositionQueryTimeout); - SendSizeQuery (); + if (_queryTerminalSize) + { + Trace.Lifecycle (nameof (AnsiSizeMonitor), "Initialize", "Driver wired up; sending initial size query"); + _startupSizeQueryCompletionHandle = _startupGate?.RegisterQuery (AnsiStartupQuery.TerminalSize, StartupSizeQueryTimeout); + SendSizeQuery (); + } + SendCursorPositionQuery (); } @@ -121,6 +130,11 @@ private void SendSizeQuery () /// public bool Poll () { + if (!_queryTerminalSize) + { + return CheckSizeChanged (); + } + // Throttle queries to avoid spamming the terminal if (DateTime.Now - _lastQuery < _queryThrottle) { diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs index 7fe33d852d..36e3f3f756 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs @@ -683,6 +683,11 @@ private void RaiseMouseEvent (string? cur) /// public event EventHandler? Keyboard; + /// + /// Raised with the parser pattern that recognized a keyboard escape sequence. + /// + internal event Action? KeyboardPatternMatched; + /// /// Gets or sets whether to explicitly handle keyboard escape sequences by raising the event. /// @@ -702,6 +707,7 @@ protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string? cu } else { + KeyboardPatternMatched?.Invoke (pattern, k); Keyboard?.Invoke (this, k); } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index dbfe0e055e..cb7c41882c 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -181,7 +181,7 @@ public void Dispose () { } /// public void Suspend () { - if (PlatformDetection.IsWindows () && !IsAttachedToTerminal) + if (OperatingSystem.IsWindows () && !IsAttachedToTerminal) { return; } diff --git a/Terminal.Gui/Drivers/Driver.cs b/Terminal.Gui/Drivers/Driver.cs index 27f960479e..61f5faf98b 100644 --- a/Terminal.Gui/Drivers/Driver.cs +++ b/Terminal.Gui/Drivers/Driver.cs @@ -1,4 +1,4 @@ -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// Holds global driver settings and cross-driver utility methods. @@ -12,7 +12,6 @@ public sealed class Driver /// Determines if driver instances should use 16 colors instead of the default TrueColors. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { get => DriverSettings.Defaults.Force16Colors; @@ -31,15 +30,12 @@ public static bool Force16Colors // It controls which strategy the ANSI driver uses to determine the terminal's size. /// /// Controls how the ANSI driver detects the terminal's window size. - /// Defaults to , which sends a - /// CSI 18t ANSI escape-sequence query to obtain the terminal size. - /// Set to to use a synchronous - /// native syscall (ioctl on Unix, Console API on Windows) instead — - /// useful when the ANSI query response does not reflect the remote terminal - /// size (e.g., over an SSH tunnel). + /// Defaults to , which uses a synchronous + /// native query (ioctl on Unix, Console API on Windows). + /// Set to for transports that require + /// an ANSI CSI 18t query instead. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static SizeDetectionMode SizeDetection { get => DriverSettings.Defaults.SizeDetection; diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 35e19af39c..09cf7a1d78 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -205,9 +205,9 @@ private static IClipboard CreateClipboard () PlatformID p = Environment.OSVersion.Platform; return CreateClipboard (() => p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows, - () => RuntimeInformation.IsOSPlatform (OSPlatform.OSX), + OperatingSystem.IsMacOS, PlatformDetection.IsWSL, - PlatformDetection.IsLinux, + OperatingSystem.IsLinux, () => new WindowsClipboard (), () => new MacOSXClipboard (), () => new WSLClipboard (), diff --git a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs index 8564339a3c..77059c23a4 100644 --- a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs @@ -33,6 +33,9 @@ public abstract class InputProcessorImpl : IInputProcessor, IDispo private readonly ITimeProvider _timeProvider; private readonly MouseInterpreter _mouseInterpreter; + /// Gets the current pipeline time. + protected DateTime CurrentTime => _timeProvider.Now; + /// /// Thread-safe input queue populated by on the input thread. /// Dequeued by on the main loop thread. @@ -78,19 +81,19 @@ protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyCon Parser.Paste += (_, text) => RaisePasteEvent (text); - Parser.Keyboard += (_, keyEvent) => - { - Key normalizedKeyEvent = OnKeyboardEventParsed (keyEvent); + Parser.KeyboardPatternMatched += (pattern, keyEvent) => + { + Key normalizedKeyEvent = OnKeyboardEventParsed (pattern, keyEvent); - if (normalizedKeyEvent.EventType == KeyEventType.Release) - { - RaiseKeyUpEvent (normalizedKeyEvent); - } - else - { - RaiseKeyDownEvent (normalizedKeyEvent); - } - }; + if (normalizedKeyEvent.EventType == KeyEventType.Release) + { + RaiseKeyUpEvent (normalizedKeyEvent); + } + else + { + RaiseKeyDownEvent (normalizedKeyEvent); + } + }; // Configure unexpected response handler Parser.UnexpectedResponseHandler = str => @@ -222,6 +225,15 @@ protected virtual void ProcessAfterParsing (TInputRecord input) /// The key event to forward. protected virtual Key OnKeyboardEventParsed (Key keyEvent) => keyEvent; + /// + /// Called with the parser pattern that recognized a keyboard event before the event is forwarded to + /// or subscribers. + /// + /// The parser pattern that recognized the input. + /// The parsed key event. + /// The key event to forward. + private protected virtual Key OnKeyboardEventParsed (AnsiKeyboardParserPattern pattern, Key keyEvent) => OnKeyboardEventParsed (keyEvent); + /// /// Gives derived processors a chance to suppress keydown events that come from fallback stream processing /// after ANSI parsing. diff --git a/Terminal.Gui/Drivers/Output/IOutputBuffer.cs b/Terminal.Gui/Drivers/Output/IOutputBuffer.cs index 4926a9df9a..a627ba91f1 100644 --- a/Terminal.Gui/Drivers/Output/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/Output/IOutputBuffer.cs @@ -44,6 +44,11 @@ public interface IOutputBuffer /// Gets a value for each row indicating whether that row contains any dirty cells and should be rendered. /// Used by to skip rows with no pending changes, avoiding unnecessary cursor moves. /// + /// + /// Output implementations clear each row's flag after emitting its dirty cells. Buffer implementations must set + /// the corresponding row flag whenever a cell is marked dirty. Code that modifies directly + /// is responsible for updating the corresponding row flag. + /// bool [] DirtyLines { get; } /// diff --git a/Terminal.Gui/Drivers/Output/OutputBase.cs b/Terminal.Gui/Drivers/Output/OutputBase.cs index 8eee8ec94e..0d6931f5a5 100644 --- a/Terminal.Gui/Drivers/Output/OutputBase.cs +++ b/Terminal.Gui/Drivers/Output/OutputBase.cs @@ -173,92 +173,85 @@ public virtual void Write (IOutputBuffer buffer) outputStringBuilder.Append (EscSeqUtils.OSC_EndHyperlink ()); } + lastCol = 0; + var outputWidth = 0; + // Process columns in row for (int col = left; col < cols; col++) { - lastCol = -1; - var outputWidth = 0; + // Skip clean cells, plus blank cells owned by raster images. Raster-owned + // blanks must not be emitted because normal text output would erase Sixel + // images and is unnecessary over Kitty's transparent-cleared placement. + bool skipRasterBlank = IsRasterCoveredBlankCell (buffer, row, col, rasterCellRectangles); - // Batch consecutive dirty cells - for (; col < cols; col++) + if (!buffer.Contents! [row, col].IsDirty || skipRasterBlank) { - // Skip clean cells, plus blank cells owned by raster images. Raster-owned - // blanks must not be emitted because normal text output would erase Sixel - // images and is unnecessary over Kitty's transparent-cleared placement. - bool skipRasterBlank = IsRasterCoveredBlankCell (buffer, row, col, rasterCellRectangles); - - if (!buffer.Contents! [row, col].IsDirty || skipRasterBlank) + if (skipRasterBlank) { - if (skipRasterBlank) - { - buffer.Contents [row, col].IsDirty = false; - } - - if (outputStringBuilder.Length > 0) - { - // This clears outputStringBuilder - WriteToConsole (outputStringBuilder, ref lastCol, ref outputWidth); - } - else if (lastCol == -1) - { - lastCol = col; - } - - if (lastCol + 1 < cols) - { - lastCol++; - } - - SetCursorPositionImpl (lastCol, row); + buffer.Contents [row, col].IsDirty = false; + } - continue; + if (outputWidth > 0) + { + // This clears outputStringBuilder + WriteToConsole (outputStringBuilder, ref lastCol, ref outputWidth); } - if (lastCol == -1) + continue; + } + + // Position the cursor once at the beginning of each dirty run. Clean gaps + // are skipped without emitting one cursor-position sequence per cell. + if (outputWidth == 0 && col != lastCol) + { + if (!SetCursorPositionImpl (col, row)) { - lastCol = col; + return; } - // Handle URL hyperlink state changes - if (!IsLegacyConsole) + lastCol = col; + } + + // Handle URL hyperlink state changes + if (!IsLegacyConsole) + { + string? cellUrl = buffer.GetCellUrl (col, row); + + if (cellUrl != _lastUrl) { - string? cellUrl = buffer.GetCellUrl (col, row); + // If we were in a hyperlink, end it + if (_lastUrl is { }) + { + outputStringBuilder.Append (EscSeqUtils.OSC_EndHyperlink ()); + } - if (cellUrl != _lastUrl) + // If starting a new hyperlink, begin it + if (!string.IsNullOrEmpty (cellUrl)) { - // If we were in a hyperlink, end it - if (_lastUrl is { }) - { - outputStringBuilder.Append (EscSeqUtils.OSC_EndHyperlink ()); - } - - // If starting a new hyperlink, begin it - if (!string.IsNullOrEmpty (cellUrl)) - { - outputStringBuilder.Append (EscSeqUtils.OSC_StartHyperlink (cellUrl)); - } - - _lastUrl = cellUrl; + outputStringBuilder.Append (EscSeqUtils.OSC_StartHyperlink (cellUrl)); } + + _lastUrl = cellUrl; } + } - // Append dirty cell as ANSI and mark clean - Cell cell = buffer.Contents [row, col]; - buffer.Contents [row, col].IsDirty = false; - AppendCellAnsi (cell, outputStringBuilder, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth); + // Append dirty cell as ANSI and mark clean + int cellCol = col; + Cell cell = buffer.Contents [row, col]; + buffer.Contents [row, col].IsDirty = false; + AppendCellAnsi (cell, outputStringBuilder, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth); - if (col != lastCol) - { - // Was a wide grapheme so mark clean next cell - // See https://github.com/tui-cs/Terminal.Gui/issues/4466 - buffer.Contents [row, col].IsDirty = false; - } + if (col != cellCol) + { + // Was a wide grapheme so mark clean next cell + // See https://github.com/tui-cs/Terminal.Gui/issues/4466 + buffer.Contents [row, col].IsDirty = false; } } // Track row's URL status BEFORE the early-exit so _rowsWithUrls stays consistent // with the buffer state — even for rows whose cells were all flushed via WriteToConsole - // during the inner loop (leaving outputStringBuilder empty at this point). + // during the row scan (leaving outputStringBuilder empty at this point). if (!IsLegacyConsole) { if (rowHasUrlsNow) @@ -271,6 +264,10 @@ public virtual void Write (IOutputBuffer buffer) } } + // Every dirty cell in this row has now been consumed. Drawing operations will + // set this flag again when they modify the row in a later frame. + buffer.DirtyLines [row] = false; + // Flush buffered output for row. Even when nothing remains buffered, an OSC 8 hyperlink // may still be open in the terminal because it was started in a prior batch flushed by // WriteToConsole and the row ended (or only clean cells followed) before any cell with @@ -296,8 +293,6 @@ public virtual void Write (IOutputBuffer buffer) _lastUrl = null; } - SetCursorPositionImpl (lastCol, row); - Write (outputStringBuilder); } diff --git a/Terminal.Gui/Drivers/Output/OutputBufferImpl.cs b/Terminal.Gui/Drivers/Output/OutputBufferImpl.cs index f8b6509f8c..5849a8a847 100644 --- a/Terminal.Gui/Drivers/Output/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/Output/OutputBufferImpl.cs @@ -458,8 +458,14 @@ private void SetRasterImageCellsDirtyState (RasterImageCommand command, bool isD { for (int col = visible.X; col < visible.Right; col++) { - Contents [row, col].IsDirty = isDirty; - DirtyLines [row] |= isDirty; + if (isDirty) + { + MarkDirty (col, row); + + continue; + } + + Contents [row, col].IsDirty = false; } } } @@ -551,8 +557,6 @@ private void AddGrapheme (string grapheme) string printableGrapheme = grapheme.MakePrintable (); printableGraphemeWidth = printableGrapheme.GetColumns (); WriteGraphemeByWidth (Col, Row, printableGrapheme, printableGraphemeWidth, clipRect); - - DirtyLines [Row] = true; } // Always advance cursor (even if location was invalid) @@ -575,7 +579,7 @@ private void AddGrapheme (string grapheme) if (Contents [Row, Col].Attribute != CurrentAttribute) { Contents [Row, Col].Attribute = CurrentAttribute; - Contents [Row, Col].IsDirty = true; + MarkDirty (Col, Row); } } @@ -597,7 +601,18 @@ private void InvalidateOverlappedWideGlyph (int col, int row) return; } Contents [row, col - 1].Grapheme = _column1ReplacementChar.ToString (); - Contents [row, col - 1].IsDirty = true; + MarkDirty (col - 1, row); + } + + /// + /// INTERNAL: Marks a cell and its row as dirty. + /// + /// The column. + /// The row. + private void MarkDirty (int col, int row) + { + Contents! [row, col].IsDirty = true; + DirtyLines [row] = true; } /// @@ -608,7 +623,7 @@ private void InvalidateOverlappedWideGlyph (int col, int row) private void SetAttributeAndDirty (int col, int row) { Contents! [row, col].Attribute = CurrentAttribute; - Contents [row, col].IsDirty = true; + MarkDirty (col, row); // Update the URL map: store CurrentUrl, or clear any stale entry so cells // overdrawn by non-link content are not wrapped in OSC 8 sequences. @@ -737,7 +752,7 @@ private void WriteGrapheme (int col, int row, string grapheme, Rectangle clipRec // Mark the next cell as dirty to ensure proper rendering of adjacent content if (col < clipRect.Right - 1 && col + 1 < Cols) { - Contents [row, col + 1].IsDirty = true; + MarkDirty (col + 1, row); } } diff --git a/Terminal.Gui/Drivers/PlatformDetection.cs b/Terminal.Gui/Drivers/PlatformDetection.cs index 4df9468464..d2c2c75c4e 100644 --- a/Terminal.Gui/Drivers/PlatformDetection.cs +++ b/Terminal.Gui/Drivers/PlatformDetection.cs @@ -7,66 +7,6 @@ namespace Terminal.Gui.Drivers; /// public static class PlatformDetection { - /// - /// Determines whether the current operating system is Linux. - /// - /// - /// This method returns only when running on a Linux - /// distribution. Other Unix-like platforms such as macOS and FreeBSD - /// return . - /// - /// - /// if the operating system is Linux; - /// otherwise, . - /// - public static bool IsLinux () => RuntimeInformation.IsOSPlatform (OSPlatform.Linux); - - /// - /// Determines whether the current operating system is macOS. - /// - /// true if the current operating system is macOS; otherwise, false. - public static bool IsMac () => RuntimeInformation.IsOSPlatform (OSPlatform.OSX); - - /// - /// Determines whether the current operating system is a Unix-like platform. - /// - /// - /// Unix-like platforms include operating systems that derive from or - /// closely follow traditional UNIX and POSIX design principles. - /// On .NET, this currently includes Linux, macOS (Darwin), and FreeBSD. - /// - /// - /// if the operating system is Linux, macOS, or FreeBSD; - /// otherwise, . - /// - public static bool IsUnixLike () => - RuntimeInformation.IsOSPlatform (OSPlatform.Linux) - || RuntimeInformation.IsOSPlatform (OSPlatform.OSX) - || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD); - - - /// Returns the for the current operating system. - public static TuiPlatform GetCurrentPlatform () - { - if (IsWindows ()) - { - return TuiPlatform.Windows; - } - - if (IsMac ()) - { - return TuiPlatform.Macos; - } - - return TuiPlatform.Linux; - } - - /// - /// Determines if the current platform is Windows. - /// - /// - public static bool IsWindows () => RuntimeInformation.IsOSPlatform (OSPlatform.Windows); - /// /// Determines if the current platform is WSL (Windows Subsystem for Linux). /// diff --git a/Terminal.Gui/Drivers/SizeDetectionMode.cs b/Terminal.Gui/Drivers/SizeDetectionMode.cs index a003e8d4e4..708db0287b 100644 --- a/Terminal.Gui/Drivers/SizeDetectionMode.cs +++ b/Terminal.Gui/Drivers/SizeDetectionMode.cs @@ -8,14 +8,15 @@ public enum SizeDetectionMode /// /// Sends a CSI 18t ANSI escape-sequence query and parses the /// ESC [ 8 ; height ; width t response. Works over SSH or any - /// ANSI-compatible terminal. This is the default. + /// ANSI-compatible terminal. Use this as an explicit fallback when a + /// native size query is unavailable or does not report the transport's size. /// AnsiQuery, /// /// Uses ioctl(TIOCGWINSZ) on Unix/macOS or the Console API on Windows. - /// Synchronous, immediate, and reliable. Useful when the ANSI query response - /// does not reflect the remote terminal size (e.g., over an SSH tunnel). + /// This is the default on supported local platforms because it is synchronous, + /// immediate, and does not generate terminal query traffic. /// Polling } diff --git a/Terminal.Gui/Drivers/TerminalDevice.cs b/Terminal.Gui/Drivers/TerminalDevice.cs index 839384a2f9..f2d0158d8d 100644 --- a/Terminal.Gui/Drivers/TerminalDevice.cs +++ b/Terminal.Gui/Drivers/TerminalDevice.cs @@ -186,7 +186,7 @@ private static void EnsureInitialized () try { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (OperatingSystem.IsWindows ()) { InitializeWindows (); } diff --git a/Terminal.Gui/Drivers/UnixHelpers/SuspendHelper.cs b/Terminal.Gui/Drivers/UnixHelpers/SuspendHelper.cs index 79aae05726..084fdae858 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/SuspendHelper.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/SuspendHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; @@ -10,6 +11,7 @@ internal static class SuspendHelper /// True if the suspension was successful. public static bool Suspend () { + // TODO: Use (int)PosixSignal.SIGTSTP once baseline moves to .NET 11.0 int signal = GetSuspendSignal (); Logging.Information ($"SuspendHelper.Suspend: signal={signal}"); @@ -36,58 +38,39 @@ private static int GetSuspendSignal () return _suspendSignal; } - nint buf = Marshal.AllocHGlobal (8192); - - if (uname (buf) != 0) + if (OperatingSystem.IsMacOS () || + OperatingSystem.IsMacCatalyst () || + OperatingSystem.IsIOS () || + OperatingSystem.IsTvOS () || + OperatingSystem.IsWatchOS () || + OperatingSystem.IsFreeBSD () || + RuntimeInformation.IsOSPlatform (OSPlatform.Create ("NETBSD")) || + RuntimeInformation.IsOSPlatform (OSPlatform.Create ("OPENBSD"))) { - Marshal.FreeHGlobal (buf); - _suspendSignal = -1; - - return _suspendSignal; + _suspendSignal = 18; } - - try + else if (OperatingSystem.IsLinux () || + OperatingSystem.IsAndroid ()) { - switch (Marshal.PtrToStringAnsi (buf)) - { - case "Darwin": - case "DragonFly": - case "FreeBSD": - case "NetBSD": - case "OpenBSD": - _suspendSignal = 18; - - break; - - case "Linux": - // TODO: should fetch the machine name and - // if it is MIPS return 24 - _suspendSignal = 20; - - break; - - case "Solaris": - _suspendSignal = 24; - - break; - - default: - _suspendSignal = -1; - - break; - } - - return _suspendSignal; + _suspendSignal = 20; + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Create ("SOLARIS")) || + RuntimeInformation.IsOSPlatform (OSPlatform.Create ("ILLUMOS"))) + { + _suspendSignal = 24; } - finally + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Create ("HAIKU"))) { - Marshal.FreeHGlobal (buf); + _suspendSignal = 13; } + else + { + _suspendSignal = -1; + } + + return _suspendSignal; } [DllImport ("libc", SetLastError = true)] private static extern int killpg (int pgrp, int sig); - - [DllImport ("libc")] - private static extern int uname (nint buf); } diff --git a/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs b/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs index e223eb9da8..0d5bc69900 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs @@ -179,11 +179,12 @@ public struct WinSize /// /// Get window/terminal size using ioctl. - /// Platform-specific constant (different on Darwin/BSD vs Linux). + /// Platform-specific constant. /// - public static readonly uint TIOCGWINSZ = RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD) - ? 0x40087468u // Darwin/BSD - : 0x5413u; // Linux + public static readonly uint TIOCGWINSZ = + (OperatingSystem.IsLinux () || OperatingSystem.IsAndroid ()) && RuntimeInformation.ProcessArchitecture != Architecture.Ppc64le ? 0x5413u : + RuntimeInformation.IsOSPlatform (OSPlatform.Create ("SOLARIS")) || RuntimeInformation.IsOSPlatform (OSPlatform.Create ("ILLUMOS")) ? 0x5468u : + RuntimeInformation.IsOSPlatform (OSPlatform.Create ("HAIKU")) ? 0x800Cu : 0x40087468u; /// /// I/O control operations on file descriptors. @@ -196,7 +197,7 @@ public struct WinSize public static extern int ioctl (int fd, uint request, out WinSize ws); /// - /// ioctl definition for Darwin/FreeBSD on ARM64. + /// ioctl definition for Darwin on ARM64. /// See https://github.com/dotnet/runtime/issues/48796#issuecomment-3695794860. /// /// File descriptor @@ -435,7 +436,7 @@ public static bool TryGetTerminalSize (out Size size) WinSize ws; if (RuntimeInformation.OSArchitecture == Architecture.Arm64 - && (RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD))) + && OperatingSystem.IsMacOS ()) { ioctlResult = ioctl_arm64 (fd, TIOCGWINSZ, diff --git a/Terminal.Gui/Drivers/UnixHelpers/UnixRawModeHelper.cs b/Terminal.Gui/Drivers/UnixHelpers/UnixRawModeHelper.cs index e24994c301..25018f3f3f 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/UnixRawModeHelper.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/UnixRawModeHelper.cs @@ -58,7 +58,7 @@ public bool TryEnable () } // Only attempt on Unix-like platforms - if (!PlatformDetection.IsUnixLike ()) + if (OperatingSystem.IsWindows ()) { return false; } diff --git a/Terminal.Gui/Drivers/UnixHelpers/UnixTerminalHelper.cs b/Terminal.Gui/Drivers/UnixHelpers/UnixTerminalHelper.cs index b45aecee19..e41afcbfe6 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/UnixTerminalHelper.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/UnixTerminalHelper.cs @@ -100,7 +100,7 @@ private static void RunSttySane () public static void Suspend (IOutput output) { - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { return; } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 87ff0f0b69..4cc3f3120c 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -101,7 +101,7 @@ public WindowsOutput () return; } - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -261,7 +261,7 @@ public void Write (ReadOnlySpan str) return; } - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -290,7 +290,7 @@ public Size ResizeBuffer (Size size) internal Size SetConsoleWindow (short cols, short rows) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows) || !IsAttachedToTerminal) + if (!OperatingSystem.IsWindows () || !IsAttachedToTerminal) { return new Size (cols, rows); } @@ -392,7 +392,7 @@ public override void Write (IOutputBuffer outputBuffer) { Logging.Error ($"Error: {e.Message} in {nameof (WindowsOutput)}"); - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (OperatingSystem.IsWindows ()) { throw; } @@ -455,7 +455,7 @@ protected override void Write (StringBuilder output) { Logging.Error ($"Error: {e.Message} in {nameof (WindowsOutput)}"); - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (OperatingSystem.IsWindows ()) { throw; } diff --git a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs index c17a440c99..0830573dab 100644 --- a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs +++ b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs @@ -124,7 +124,7 @@ public bool TryEnable () } // Only attempt on Windows - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return false; } @@ -290,7 +290,7 @@ public void Dispose () /// public static void WakePendingRead () { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } diff --git a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTOutputHelper.cs b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTOutputHelper.cs index de89005b42..0717a976f8 100644 --- a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTOutputHelper.cs +++ b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTOutputHelper.cs @@ -70,7 +70,7 @@ public bool TryEnable () } // Only attempt on Windows - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return false; } diff --git a/Terminal.Gui/FileServices/FileSystemIconProvider.cs b/Terminal.Gui/FileServices/FileSystemIconProvider.cs index 27a70edc28..355ee4247d 100644 --- a/Terminal.Gui/FileServices/FileSystemIconProvider.cs +++ b/Terminal.Gui/FileServices/FileSystemIconProvider.cs @@ -49,7 +49,7 @@ public string GetIconWithOptionalSpace (IFileSystemInfo? fileSystemInfo) /// /// Gets or sets a flag indicating whether to use Nerd Font icons. Defaults to /// which can be configured by end users from their ./.tui/config.json via - /// . + /// . /// /// Enabling implicitly disables . /// diff --git a/Terminal.Gui/Input/CommandBindingsBase.cs b/Terminal.Gui/Input/CommandBindingsBase.cs index 62dc2804c1..89f730b094 100644 --- a/Terminal.Gui/Input/CommandBindingsBase.cs +++ b/Terminal.Gui/Input/CommandBindingsBase.cs @@ -38,10 +38,9 @@ public void Add (TEvent eventArgs, TBinding binding) throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); } - // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy - // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus - // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + // IMPORTANT: Add a COPY of the eventArgs. Dictionary keys are cached; if a later overlay mutated + // IMPORTANT: the same instance in place, lookup would see a different key than the one stored. + // IMPORTANT: Copying keeps the stored key stable after theme/config re-apply. if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); diff --git a/Terminal.Gui/Input/IAcceptTarget.cs b/Terminal.Gui/Input/IAcceptTarget.cs index f1dbf75c5a..f334219b6e 100644 --- a/Terminal.Gui/Input/IAcceptTarget.cs +++ b/Terminal.Gui/Input/IAcceptTarget.cs @@ -1,4 +1,4 @@ -namespace Terminal.Gui; +namespace Terminal.Gui.Input; /// /// Interface for views that handle as terminal destinations. diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index 21fcdfca31..f24de2be82 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -1332,7 +1332,6 @@ public static bool TryParse (string text, out Key key) #endregion /// Gets or sets the separator character used when parsing and printing Keys. E.g. Ctrl+A. The default is '+'. - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Rune Separator { get => KeySettings.Defaults.Separator; diff --git a/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs b/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs index 0e764db7d8..8415b86431 100644 --- a/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs @@ -66,13 +66,8 @@ public IEnumerable GetCurrentPlatformKeys () } } - Key []? platKeys = PlatformDetection.GetCurrentPlatform () switch - { - TuiPlatform.Windows => Windows, - TuiPlatform.Linux => Linux, - TuiPlatform.Macos => Macos, - _ => null - }; + Key []? platKeys = OperatingSystem.IsWindows () ? Windows : + OperatingSystem.IsMacOS () ? Macos : Linux; if (platKeys is null) { diff --git a/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs b/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs index 5513e3b331..a3cda375fc 100644 --- a/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs @@ -66,13 +66,8 @@ public IEnumerable GetCurrentPlatformMouseFlags () } } - MouseFlags []? platformFlags = PlatformDetection.GetCurrentPlatform () switch - { - TuiPlatform.Windows => Windows, - TuiPlatform.Linux => Linux, - TuiPlatform.Macos => Macos, - _ => null - }; + MouseFlags []? platformFlags = OperatingSystem.IsWindows () ? Windows : + OperatingSystem.IsMacOS () ? Macos : Linux; if (platformFlags is null) { diff --git a/Terminal.Gui/ModuleInitializers.cs b/Terminal.Gui/ModuleInitializers.cs index d7e4c86077..bd8b87994f 100644 --- a/Terminal.Gui/ModuleInitializers.cs +++ b/Terminal.Gui/ModuleInitializers.cs @@ -1,8 +1,6 @@ using System.Runtime.CompilerServices; using Terminal.Gui.Configuration; -#pragma warning disable CS0618 // Obsolete - ConfigurationManager still used internally during transition - namespace Terminal.Gui; /// @@ -11,28 +9,25 @@ namespace Terminal.Gui; internal static class ModuleInitializers { /// - /// Initializes the ConfigurationManager when the Terminal.Gui assembly is loaded. - /// Ensures configuration properties are loaded deterministically before any part - /// of the library is used. + /// Applies MEC configuration to static settings facades when the Terminal.Gui assembly is loaded. /// + /// + /// Must never throw: an exception here kills the app (and any test host) with an opaque + /// module-initializer error before Main runs. Bad configuration is collected in + /// and printed at shutdown; the app runs on defaults. + /// #pragma warning disable CA2255 [ModuleInitializer] #pragma warning restore CA2255 - internal static void InitializeConfigurationManager () + internal static void InitializeConfiguration () { - // Initialize ConfigurationManager to ensure all configuration properties - // are loaded deterministically - ConfigurationManager.Initialize (); - - // After CM initialization, also apply MEC-based settings. - // MEC values (from the same config files) take precedence over CM defaults. - TuiConfigurationBuilder mecBuilder = new (); - mecBuilder.ApplyToStaticFacades (); - - // Note: We're only initializing the config properties structure here, - // not loading settings from files. That still happens during Application.Init() - // via InitializeConfigurationManagement() + try + { + TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); + } + catch (Exception ex) + { + TuiJsonErrors.Add ($"Applying configuration failed: {ex.Message}. Running with default settings."); + } } } - -#pragma warning restore CS0618 diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 283ec00a4a..ec0fca8eec 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -14,10 +14,12 @@ The [GitVersion.MsBuild](https://www.nuget.org/packages/GitVersion.MsBuild) NuGe |--------|----------------|-----------|-------| | `main` (stable) | `2.0.0` | Patch | Label set in `GitVersion.yml` (`label: ''`) | | `main` (pre-release) | `2.0.1-rc.1` | Patch | Label set in `GitVersion.yml` (e.g., `label: rc`) | -| `develop` | `2.1.0-develop.42` | Minor | Always carries `-develop` pre-release label | -| `feature/*`, `fix/*`, etc. | `2.1.0-my-feature.1` | Inherit | Inherits from `develop`; branch name becomes label | +| `develop` | `2.5.0-develop.55` | Patch | Always carries `-develop` pre-release label | +| `feature/*`, `fix/*`, etc. | `2.5.0-my-feature.1` | Inherit | Inherits from `develop`; branch name becomes label | | `pull-request/*` | `2.0.0-pr.123.1` | Inherit | PR number in label | +`develop` increments **Patch** (`GitVersion.yml` `increment: Patch`). To bump Minor, include `+semver: minor` in a commit message. Do not pin `next-version`. + ### Checking Versions Locally ```powershell @@ -135,7 +137,7 @@ These branches are **not** configured in `GitVersion.yml` (the config was remove - **Package**: [nuget.org/packages/Terminal.Gui](https://www.nuget.org/packages/Terminal.Gui) - **Auto-published** on every push to `develop` (pre-release versions) and on `v*` tag pushes (stable/pre-release versions tagged on `main`) -- Pre-release versions (e.g., `2.1.0-develop.42`) are marked as pre-release on NuGet +- Pre-release versions (e.g., `2.5.0-develop.55`) are marked as pre-release on NuGet ### Local Package Development diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index bb006e2819..393f8c5577 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -1,1501 +1,1765 @@ { - // Specifies the "source of truth" for default values for all Terminal.Gui settings managed by - // ConfigurationManager. It is automatically loaded, and applied, each time Application.Init - // is run (via the ConfigurationManager.Reset method). - // - // In other words, initial values set in the the codebase are always overwritten by the contents of this - // resource embedded in the Terminal.Gui.dll assembly. - // - // The Unit Test method "ConfigurationManagerTests.SaveDefaults" can be used to re-create the base of this file, but - // note that not all values here will be recreated (e.g. the Light and Dark themes and any property initialized - // null). - // + // Nested MEC-native Terminal.Gui configuration (v2.5). + // Convert pre-2.5 flat-key files with: dotnet run --project Tools/MigrateConfig -- "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", - - // Set this to true in a .config file to be loaded to cause JSON parsing errors - // to throw exceptions. - "ConfigurationManager.ThrowOnJsonErrors": false, - - // --------------- Tracing ---------------------------- - //"Trace.EnabledCategories": "Lifecycle", - - // --------------- Application Settings --------------- - "Key.Separator": "+", - - "Driver.Force16Colors": false, - //"Application.ForceDriver": "", // TODO: ForceDriver should be nullable - "Application.IsMouseDisabled": false, - - // --------------- Colors --------------- - - - // --------------- View Specific Settings --------------- - "PopoverMenu.DefaultKey": "Shift+F10", - - // --------------- Themes ----------------- - "Themes": [ - // We do not override any hard-coded properties for Default here; - // Default is by definition the same as hard-coded. - //{ - // "Default": { - // } - //}, - { - "TurboPascal 5": { - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "White", - "Background": "Blue" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "Yellow", - "Background": "Blue" - }, - "Code": { "Foreground": "Yellow", "Background": "Blue", "Style": "None" }, - "CodeComment": { "Foreground": "LightGray", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "LightCyan", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "LightGreen", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "Magenta", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "BrightRed", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "Yellow", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "LightGreen", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "LightYellow", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "LightCyan", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Black", - "Background": "LightGray" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "Cyan", - "Style": "None" - }, - "Focus": { - "Foreground": "Black", - "Background": "LightGray", - "Style": "None" - }, - "HotNormal": { - "Foreground": "BrightRed", - "Background": "Cyan", - "Style": "None" - }, - "HotFocus": { - "Foreground": "BrightRed", - "Background": "LightGray", - "Style": "None" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Cyan", - "Style": "None" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "BrightRed", - "Background": "LightGray", - "Style": "None" - } - } + "Key": { + "Separator": "+" + }, + "Driver": { + "Force16Colors": false + }, + "Application": { + "IsMouseDisabled": false + }, + "PopoverMenu": { + "DefaultKey": "Shift+F10" + }, + "Themes": { + "TurboPascal 5": { + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "White", + "Background": "Blue" } - ], - "Glyphs.CheckStateChecked": "☑", - "Glyphs.CheckStateNone": "⬛", - "Glyphs.CheckStateUnChecked": "☐", - "Glyphs.LeftBracket": "[", - "Glyphs.RightBracket": "]" + }, + "Base": { + "Normal": { + "Foreground": "Yellow", + "Background": "Blue" + }, + "Code": { + "Foreground": "Yellow", + "Background": "Blue", + "Style": "None" + }, + "CodeComment": { + "Foreground": "LightGray", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "LightCyan", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "LightGreen", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "Magenta", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "BrightRed", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "Yellow", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "LightGreen", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "LightYellow", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "LightCyan", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Black", + "Background": "LightGray" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "Cyan", + "Style": "None" + }, + "Focus": { + "Foreground": "Black", + "Background": "LightGray", + "Style": "None" + }, + "HotNormal": { + "Foreground": "BrightRed", + "Background": "Cyan", + "Style": "None" + }, + "HotFocus": { + "Foreground": "BrightRed", + "Background": "LightGray", + "Style": "None" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "Cyan", + "Style": "None" + } + }, + "Error": { + "Normal": { + "Foreground": "BrightRed", + "Background": "LightGray", + "Style": "None" + } + } + }, + "Glyphs": { + "CheckStateChecked": "☑", + "CheckStateNone": "⬛", + "CheckStateUnChecked": "☐", + "LeftBracket": "[", + "RightBracket": "]" } }, - { - "Anders": { - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "WhiteSmoke", - "Background": "DimGray" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "White", - "Background": "DarkBlue" - }, - "Code": { "Foreground": "White", "Background": "DarkBlue", "Style": "None" }, - "CodeComment": { "Foreground": "#8fbc8f", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#87cefa", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#f0e68c", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#98fb98", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#40e0d0", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#dda0dd", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#87cefa", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "White", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#fffacd", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#add8e6", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "BrightBlue", - "Background": "LightGray" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "White", - "Background": "Blue", - "Style": "Bold" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "Red", - "Background": "WhiteSmoke", - "Style": "Italic" - } - } + "Anders": { + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "WhiteSmoke", + "Background": "DimGray" + } + }, + "Base": { + "Normal": { + "Foreground": "White", + "Background": "DarkBlue" + }, + "Code": { + "Foreground": "White", + "Background": "DarkBlue", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#8fbc8f", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#87cefa", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#f0e68c", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#98fb98", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#40e0d0", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#dda0dd", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#87cefa", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "White", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#fffacd", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#add8e6", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "BrightBlue", + "Background": "LightGray" } - ], - "Glyphs.CheckStateChecked": "☑", - "Glyphs.CheckStateNone": "⬛", - "Glyphs.CheckStateUnChecked": "☐", - "Glyphs.LeftBracket": "[", - "Glyphs.RightBracket": "]" + }, + "Menu": { + "Normal": { + "Foreground": "White", + "Background": "Blue", + "Style": "Bold" + } + }, + "Error": { + "Normal": { + "Foreground": "Red", + "Background": "WhiteSmoke", + "Style": "Italic" + } + } + }, + "Glyphs": { + "CheckStateChecked": "☑", + "CheckStateNone": "⬛", + "CheckStateUnChecked": "☐", + "LeftBracket": "[", + "RightBracket": "]" } }, - { - "Dark": { - "Dialog.DefaultButtonAlignment": "End", - "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems", - "Dialog.DefaultBorderStyle": "Heavy", - "Dialog.DefaultShadow": "Transparent", - "FrameView.DefaultBorderStyle": "Single", - "Window.DefaultBorderStyle": "Single", - "MessageBox.DefaultButtonAlignment": "Center", - "MessageBox.DefaultBorderStyle": "Heavy", - "Button.DefaultShadow": "Opaque", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "LightGray", - "Background": "Onyx", - "Style": "None" - }, - "Focus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Silver", - "Background": "Onyx", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Onyx", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "Underline" - }, - "Active": { - "Foreground": "White", - "Background": "Onyx", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "White", - "Background": "Onyx", - "Style": "Underline" - }, - "Highlight": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "None" - }, - "Editable": { - "Foreground": "LightYellow", - "Background": "Onyx", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "Onyx", - "Style": "Italic" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "LightGray", - "Background": "None", - "Style": "None" - }, - "Focus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Silver", - "Background": "None", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "None", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "Underline" - }, - "Active": { - "Foreground": "White", - "Background": "Onyx" - }, - "HotActive": { - "Foreground": "White", - "Background": "Onyx", - "Style": "Underline" - }, - "Highlight": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "None" - }, - "Editable": { - "Foreground": "Gray", - "Background": "None", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "None", - "Style": "Italic" - }, - "Code": { "Foreground": "LightGray", "Background": "Onyx", "Style": "None" }, - "CodeComment": { "Foreground": "#6a9955", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#569cd6", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#ce9178", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#b5cea8", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "#d4d4d4", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#4ec9b0", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#c586c0", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "#9cdcfe", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#569cd6", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "#d4d4d4", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#dcdcaa", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#9cdcfe", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "LightGray", - "Background": "Charcoal", - "Style": "None" - }, - "Focus": { - "Foreground": "White", - "Background": "SlateGray", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Silver", - "Background": "Charcoal", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Charcoal", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "White", - "Background": "SlateGray", - "Style": "Underline" - }, - "Active": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "White", - "Background": "Onyx", - "Style": "None" - }, - "Editable": { - "Foreground": "Gray", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "Charcoal", - "Style": "Italic" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "White", - "Background": "Onyx", - "Style": "Bold" - }, - "Focus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "Bold" - }, - "HotNormal": { - "Foreground": "Silver", - "Background": "Onyx", - "Style": "Underline,Bold" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Charcoal", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Charcoal", - "Style": "Underline,Bold" - }, - "Active": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "White", - "Background": "OuterSpace", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "White", - "Background": "SlateGray", - "Style": "None" - }, - "Editable": { - "Foreground": "Gray", - "Background": "Onyx", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "Onyx", - "Style": "Italic" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "IndianRed", - "Background": "Black", - "Style": "None" - }, - "Focus": { - "Foreground": "White", - "Background": "IndianRed", - "Style": "None" - }, - "HotNormal": { - "Foreground": "LightCoral", - "Background": "Black", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Black", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "White", - "Background": "IndianRed", - "Style": "Underline" - }, - "Active": { - "Foreground": "White", - "Background": "LightCoral", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "White", - "Background": "LightCoral", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "White", - "Background": "IndianRed", - "Style": "None" - }, - "Editable": { - "Foreground": "Gray", - "Background": "Black", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "Black", - "Style": "Italic" - } - } + "Dark": { + "Dialog": { + "DefaultButtonAlignment": "End", + "DefaultButtonAlignmentModes": "AddSpaceBetweenItems", + "DefaultBorderStyle": "Heavy", + "DefaultShadow": "Transparent" + }, + "FrameView": { + "DefaultBorderStyle": "Single" + }, + "Window": { + "DefaultBorderStyle": "Single" + }, + "MessageBox": { + "DefaultButtonAlignment": "Center", + "DefaultBorderStyle": "Heavy" + }, + "Button": { + "DefaultShadow": "Opaque" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "LightGray", + "Background": "Onyx", + "Style": "None" + }, + "Focus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Silver", + "Background": "Onyx", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "Onyx", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "Underline" + }, + "Active": { + "Foreground": "White", + "Background": "Onyx", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "White", + "Background": "Onyx", + "Style": "Underline" + }, + "Highlight": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "None" + }, + "Editable": { + "Foreground": "LightYellow", + "Background": "Onyx", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "Onyx", + "Style": "Italic" + } + }, + "Base": { + "Normal": { + "Foreground": "LightGray", + "Background": "None", + "Style": "None" + }, + "Focus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Silver", + "Background": "None", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "None", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "Underline" + }, + "Active": { + "Foreground": "White", + "Background": "Onyx" + }, + "HotActive": { + "Foreground": "White", + "Background": "Onyx", + "Style": "Underline" + }, + "Highlight": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "None" + }, + "Editable": { + "Foreground": "Gray", + "Background": "None", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "None", + "Style": "Italic" + }, + "Code": { + "Foreground": "LightGray", + "Background": "Onyx", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#6a9955", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#569cd6", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#ce9178", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#b5cea8", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "#d4d4d4", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#4ec9b0", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#c586c0", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "#9cdcfe", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#569cd6", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "#d4d4d4", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#dcdcaa", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#9cdcfe", + "Background": "None", + "Style": "None" } - ] + }, + "Dialog": { + "Normal": { + "Foreground": "LightGray", + "Background": "Charcoal", + "Style": "None" + }, + "Focus": { + "Foreground": "White", + "Background": "SlateGray", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Silver", + "Background": "Charcoal", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "Charcoal", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "White", + "Background": "SlateGray", + "Style": "Underline" + }, + "Active": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "White", + "Background": "Onyx", + "Style": "None" + }, + "Editable": { + "Foreground": "Gray", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "Charcoal", + "Style": "Italic" + } + }, + "Menu": { + "Normal": { + "Foreground": "White", + "Background": "Onyx", + "Style": "Bold" + }, + "Focus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "Bold" + }, + "HotNormal": { + "Foreground": "Silver", + "Background": "Onyx", + "Style": "Underline,Bold" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "Charcoal", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Charcoal", + "Style": "Underline,Bold" + }, + "Active": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "White", + "Background": "OuterSpace", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "White", + "Background": "SlateGray", + "Style": "None" + }, + "Editable": { + "Foreground": "Gray", + "Background": "Onyx", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "Onyx", + "Style": "Italic" + } + }, + "Error": { + "Normal": { + "Foreground": "IndianRed", + "Background": "Black", + "Style": "None" + }, + "Focus": { + "Foreground": "White", + "Background": "IndianRed", + "Style": "None" + }, + "HotNormal": { + "Foreground": "LightCoral", + "Background": "Black", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "DarkGray", + "Background": "Black", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "White", + "Background": "IndianRed", + "Style": "Underline" + }, + "Active": { + "Foreground": "White", + "Background": "LightCoral", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "White", + "Background": "LightCoral", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "White", + "Background": "IndianRed", + "Style": "None" + }, + "Editable": { + "Foreground": "Gray", + "Background": "Black", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "Black", + "Style": "Italic" + } + } } }, - { - "Light": { - "Dialog.DefaultButtonAlignment": "End", - "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems", - "Dialog.DefaultBorderStyle": "Heavy", - "Dialog.DefaultShadow": "Transparent", - "FrameView.DefaultBorderStyle": "Single", - "Window.DefaultBorderStyle": "Single", - "MessageBox.DefaultButtonAlignment": "Center", - "MessageBox.DefaultBorderStyle": "Heavy", - "Button.DefaultShadow": "Opaque", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "DimGray", - "Background": "WhiteSmoke", - "Style": "None" - }, - "Focus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Gray", - "Background": "WhiteSmoke", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "LightGray", - "Background": "WhiteSmoke", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "Underline" - }, - "Active": { - "Foreground": "Black", - "Background": "White", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "Black", - "Background": "White", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "LightGray", - "Style": "None" - }, - "Editable": { - "Foreground": "Black", - "Background": "LemonChiffon", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "WhiteSmoke", - "Style": "Italic" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "Gray", - "Background": "None", - "Style": "None" - }, - "Focus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Silver", - "Background": "None", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "LightGray", - "Background": "None", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "Underline" - }, - "Active": { - "Foreground": "Black", - "Background": "White", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "Black", - "Background": "White", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "LightGray", - "Style": "None" - }, - "Editable": { - "Foreground": "Black", - "Background": "LemonChiffon", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "None", - "Style": "Italic" - }, - "Code": { "Foreground": "Black", "Background": "WhiteSmoke", "Style": "None" }, - "CodeComment": { "Foreground": "#008000", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#0000ff", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#a31515", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#098658", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "#000000", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#267f99", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#0000ff", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "#001080", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#0000ff", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "#000000", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#795e26", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#ff0000", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "DimGray", - "Background": "WhiteSmoke", - "Style": "None" - }, - "Focus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "None" - }, - "HotNormal": { - "Foreground": "Gray", - "Background": "WhiteSmoke", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "LightGray", - "Background": "WhiteSmoke", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "Underline" - }, - "Active": { - "Foreground": "Black", - "Background": "White", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "Black", - "Background": "White", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "LightGray", - "Style": "None" - }, - "Editable": { - "Foreground": "Black", - "Background": "LemonChiffon", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "WhiteSmoke", - "Style": "Italic" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "DimGray", - "Background": "White", - "Style": "Bold" - }, - "Focus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "Bold" - }, - "HotNormal": { - "Foreground": "Gray", - "Background": "White", - "Style": "Underline,Bold" - }, - "Disabled": { - "Foreground": "LightGray", - "Background": "WhiteSmoke", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "Gainsboro", - "Style": "Underline,Bold" - }, - "Active": { - "Foreground": "Black", - "Background": "WhiteSmoke", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "Black", - "Background": "WhiteSmoke", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "LemonChiffon", - "Style": "None" - }, - "Editable": { - "Foreground": "DimGray", - "Background": "White", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "White", - "Style": "Italic" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "FireBrick", - "Background": "WhiteSmoke", - "Style": "None" - }, - "Focus": { - "Foreground": "WhiteSmoke", - "Background": "FireBrick", - "Style": "None" - }, - "HotNormal": { - "Foreground": "LightCoral", - "Background": "WhiteSmoke", - "Style": "Underline" - }, - "Disabled": { - "Foreground": "LightGray", - "Background": "WhiteSmoke", - "Style": "Faint" - }, - "HotFocus": { - "Foreground": "WhiteSmoke", - "Background": "FireBrick", - "Style": "Underline" - }, - "Active": { - "Foreground": "WhiteSmoke", - "Background": "LightCoral", - "Style": "Bold" - }, - "HotActive": { - "Foreground": "WhiteSmoke", - "Background": "LightCoral", - "Style": "Underline,Bold" - }, - "Highlight": { - "Foreground": "WhiteSmoke", - "Background": "FireBrick", - "Style": "None" - }, - "Editable": { - "Foreground": "Black", - "Background": "WhiteSmoke", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "WhiteSmoke", - "Style": "Italic" - } - } + "Light": { + "Dialog": { + "DefaultButtonAlignment": "End", + "DefaultButtonAlignmentModes": "AddSpaceBetweenItems", + "DefaultBorderStyle": "Heavy", + "DefaultShadow": "Transparent" + }, + "FrameView": { + "DefaultBorderStyle": "Single" + }, + "Window": { + "DefaultBorderStyle": "Single" + }, + "MessageBox": { + "DefaultButtonAlignment": "Center", + "DefaultBorderStyle": "Heavy" + }, + "Button": { + "DefaultShadow": "Opaque" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "DimGray", + "Background": "WhiteSmoke", + "Style": "None" + }, + "Focus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Gray", + "Background": "WhiteSmoke", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "LightGray", + "Background": "WhiteSmoke", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "Underline" + }, + "Active": { + "Foreground": "Black", + "Background": "White", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "Black", + "Background": "White", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "LightGray", + "Style": "None" + }, + "Editable": { + "Foreground": "Black", + "Background": "LemonChiffon", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "WhiteSmoke", + "Style": "Italic" } - ] + }, + "Base": { + "Normal": { + "Foreground": "Gray", + "Background": "None", + "Style": "None" + }, + "Focus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Silver", + "Background": "None", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "LightGray", + "Background": "None", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "Underline" + }, + "Active": { + "Foreground": "Black", + "Background": "White", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "Black", + "Background": "White", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "LightGray", + "Style": "None" + }, + "Editable": { + "Foreground": "Black", + "Background": "LemonChiffon", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "None", + "Style": "Italic" + }, + "Code": { + "Foreground": "Black", + "Background": "WhiteSmoke", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#008000", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#0000ff", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#a31515", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#098658", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "#000000", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#267f99", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#0000ff", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "#001080", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#0000ff", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "#000000", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#795e26", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#ff0000", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "DimGray", + "Background": "WhiteSmoke", + "Style": "None" + }, + "Focus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "None" + }, + "HotNormal": { + "Foreground": "Gray", + "Background": "WhiteSmoke", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "LightGray", + "Background": "WhiteSmoke", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "Underline" + }, + "Active": { + "Foreground": "Black", + "Background": "White", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "Black", + "Background": "White", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "LightGray", + "Style": "None" + }, + "Editable": { + "Foreground": "Black", + "Background": "LemonChiffon", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "WhiteSmoke", + "Style": "Italic" + } + }, + "Menu": { + "Normal": { + "Foreground": "DimGray", + "Background": "White", + "Style": "Bold" + }, + "Focus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "Bold" + }, + "HotNormal": { + "Foreground": "Gray", + "Background": "White", + "Style": "Underline,Bold" + }, + "Disabled": { + "Foreground": "LightGray", + "Background": "WhiteSmoke", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "Gainsboro", + "Style": "Underline,Bold" + }, + "Active": { + "Foreground": "Black", + "Background": "WhiteSmoke", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "Black", + "Background": "WhiteSmoke", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "LemonChiffon", + "Style": "None" + }, + "Editable": { + "Foreground": "DimGray", + "Background": "White", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "White", + "Style": "Italic" + } + }, + "Error": { + "Normal": { + "Foreground": "FireBrick", + "Background": "WhiteSmoke", + "Style": "None" + }, + "Focus": { + "Foreground": "WhiteSmoke", + "Background": "FireBrick", + "Style": "None" + }, + "HotNormal": { + "Foreground": "LightCoral", + "Background": "WhiteSmoke", + "Style": "Underline" + }, + "Disabled": { + "Foreground": "LightGray", + "Background": "WhiteSmoke", + "Style": "Faint" + }, + "HotFocus": { + "Foreground": "WhiteSmoke", + "Background": "FireBrick", + "Style": "Underline" + }, + "Active": { + "Foreground": "WhiteSmoke", + "Background": "LightCoral", + "Style": "Bold" + }, + "HotActive": { + "Foreground": "WhiteSmoke", + "Background": "LightCoral", + "Style": "Underline,Bold" + }, + "Highlight": { + "Foreground": "WhiteSmoke", + "Background": "FireBrick", + "Style": "None" + }, + "Editable": { + "Foreground": "Black", + "Background": "WhiteSmoke", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "Gray", + "Background": "WhiteSmoke", + "Style": "Italic" + } + } } }, - { - "Green Phosphor": { - "Dialog.DefaultShadow": "None", - "FrameView.DefaultBorderStyle": "Single", - "Window.DefaultBorderStyle": "Single", - "MessageBox.DefaultBorderStyle": "Single", - "Button.DefaultShadow": "None", - "Menu.DefaultBorderStyle": "Single", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "Active": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Italic" - }, - "Editable": { - "Foreground": "GreenPhosphor", - "Background": "Onyx", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "GreenPhosphor", - "Background": "Onyx", - "Style": "Faint" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "GreenPhosphor", - "Background": "None", - "Style": "None" - }, - "Active": { - "Foreground": "GreenPhosphor", - "Background": "None", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "GreenPhosphor", - "Background": "None", - "Style": "Italic" - }, - "Editable": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Faint" - }, - "Code": { "Foreground": "GreenPhosphor", "Background": "Charcoal", "Style": "None" }, - "CodeComment": { "Foreground": "#7cff7c", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#b6ffb6", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#9cff9c", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#ccffcc", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "GreenPhosphor", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#80ffd4", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#d0ffd0", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "GreenPhosphor", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#b6ffb6", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "GreenPhosphor", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#e0ffe0", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#80ffd4", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "Bold" - }, - "Active": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "None" - }, - "Editable": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Faint" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "Bold" - }, - "Active": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "GreenPhosphor", - "Style": "None" - }, - "Editable": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Faint" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "GreenPhosphor", - "Background": "Black", - "Style": "Italic" - }, - "Active": { - "Foreground": "GreenPhosphor", - "Background": "Black", - "Style": "Bold,Italic" - }, - "Highlight": { - "Foreground": "GreenPhosphor", - "Background": "Black", - "Style": "Italic" - }, - "Editable": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Italic" - }, - "ReadOnly": { - "Foreground": "GreenPhosphor", - "Background": "Charcoal", - "Style": "Faint,Italic" - } - } + "Green Phosphor": { + "Dialog": { + "DefaultShadow": "None" + }, + "FrameView": { + "DefaultBorderStyle": "Single" + }, + "Window": { + "DefaultBorderStyle": "Single" + }, + "MessageBox": { + "DefaultBorderStyle": "Single" + }, + "Button": { + "DefaultShadow": "None" + }, + "Menu": { + "DefaultBorderStyle": "Single" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "Active": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Italic" + }, + "Editable": { + "Foreground": "GreenPhosphor", + "Background": "Onyx", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "GreenPhosphor", + "Background": "Onyx", + "Style": "Faint" } - ] + }, + "Base": { + "Normal": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "None" + }, + "Active": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "Italic" + }, + "Editable": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Faint" + }, + "Code": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#7cff7c", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#b6ffb6", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#9cff9c", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#ccffcc", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#80ffd4", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#d0ffd0", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#b6ffb6", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "GreenPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#e0ffe0", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#80ffd4", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "Bold" + }, + "Active": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "None" + }, + "Editable": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Faint" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "Bold" + }, + "Active": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "GreenPhosphor", + "Style": "None" + }, + "Editable": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Faint" + } + }, + "Error": { + "Normal": { + "Foreground": "GreenPhosphor", + "Background": "Black", + "Style": "Italic" + }, + "Active": { + "Foreground": "GreenPhosphor", + "Background": "Black", + "Style": "Bold,Italic" + }, + "Highlight": { + "Foreground": "GreenPhosphor", + "Background": "Black", + "Style": "Italic" + }, + "Editable": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Italic" + }, + "ReadOnly": { + "Foreground": "GreenPhosphor", + "Background": "Charcoal", + "Style": "Faint,Italic" + } + } } }, - { - "Amber Phosphor": { - "Dialog.DefaultShadow": "None", - "FrameView.DefaultBorderStyle": "Single", - "Window.DefaultBorderStyle": "Single", - "MessageBox.DefaultBorderStyle": "Single", - "Button.DefaultShadow": "None", - "Menu.DefaultBorderStyle": "Single", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "Active": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Italic" - }, - "Editable": { - "Foreground": "AmberPhosphor", - "Background": "Onyx", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "AmberPhosphor", - "Background": "Onyx", - "Style": "Faint" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "AmberPhosphor", - "Background": "None", - "Style": "None" - }, - "Active": { - "Foreground": "AmberPhosphor", - "Background": "None", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "AmberPhosphor", - "Background": "None", - "Style": "Italic" - }, - "Editable": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Faint" - }, - "Code": { "Foreground": "AmberPhosphor", "Background": "Charcoal", "Style": "None" }, - "CodeComment": { "Foreground": "#d08a00", "Background": "None", "Style": "None" }, - "CodeKeyword": { "Foreground": "#ffd166", "Background": "None", "Style": "None" }, - "CodeString": { "Foreground": "#ffb000", "Background": "None", "Style": "None" }, - "CodeNumber": { "Foreground": "#ffe08a", "Background": "None", "Style": "None" }, - "CodeOperator": { "Foreground": "AmberPhosphor", "Background": "None", "Style": "None" }, - "CodeType": { "Foreground": "#ffc04d", "Background": "None", "Style": "None" }, - "CodePreprocessor": { "Foreground": "#ff9f1c", "Background": "None", "Style": "None" }, - "CodeIdentifier": { "Foreground": "AmberPhosphor", "Background": "None", "Style": "None" }, - "CodeConstant": { "Foreground": "#ffd166", "Background": "None", "Style": "None" }, - "CodePunctuation": { "Foreground": "AmberPhosphor", "Background": "None", "Style": "None" }, - "CodeFunctionName": { "Foreground": "#fff0a6", "Background": "None", "Style": "None" }, - "CodeAttribute": { "Foreground": "#ffc04d", "Background": "None", "Style": "None" } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "Bold" - }, - "Active": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "None" - }, - "Editable": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Faint" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "Bold" - }, - "Active": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "Bold" - }, - "Highlight": { - "Foreground": "Black", - "Background": "AmberPhosphor", - "Style": "None" - }, - "Editable": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "None" - }, - "ReadOnly": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Faint" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "AmberPhosphor", - "Background": "Black", - "Style": "Italic" - }, - "Active": { - "Foreground": "AmberPhosphor", - "Background": "Black", - "Style": "Bold,Italic" - }, - "Highlight": { - "Foreground": "AmberPhosphor", - "Background": "Black", - "Style": "Italic" - }, - "Editable": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Italic" - }, - "ReadOnly": { - "Foreground": "AmberPhosphor", - "Background": "Charcoal", - "Style": "Faint,Italic" - } - } + "Amber Phosphor": { + "Dialog": { + "DefaultShadow": "None" + }, + "FrameView": { + "DefaultBorderStyle": "Single" + }, + "Window": { + "DefaultBorderStyle": "Single" + }, + "MessageBox": { + "DefaultBorderStyle": "Single" + }, + "Button": { + "DefaultShadow": "None" + }, + "Menu": { + "DefaultBorderStyle": "Single" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "Active": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Italic" + }, + "Editable": { + "Foreground": "AmberPhosphor", + "Background": "Onyx", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "AmberPhosphor", + "Background": "Onyx", + "Style": "Faint" + } + }, + "Base": { + "Normal": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "None" + }, + "Active": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "Italic" + }, + "Editable": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Faint" + }, + "Code": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "CodeComment": { + "Foreground": "#d08a00", + "Background": "None", + "Style": "None" + }, + "CodeKeyword": { + "Foreground": "#ffd166", + "Background": "None", + "Style": "None" + }, + "CodeString": { + "Foreground": "#ffb000", + "Background": "None", + "Style": "None" + }, + "CodeNumber": { + "Foreground": "#ffe08a", + "Background": "None", + "Style": "None" + }, + "CodeOperator": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeType": { + "Foreground": "#ffc04d", + "Background": "None", + "Style": "None" + }, + "CodePreprocessor": { + "Foreground": "#ff9f1c", + "Background": "None", + "Style": "None" + }, + "CodeIdentifier": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeConstant": { + "Foreground": "#ffd166", + "Background": "None", + "Style": "None" + }, + "CodePunctuation": { + "Foreground": "AmberPhosphor", + "Background": "None", + "Style": "None" + }, + "CodeFunctionName": { + "Foreground": "#fff0a6", + "Background": "None", + "Style": "None" + }, + "CodeAttribute": { + "Foreground": "#ffc04d", + "Background": "None", + "Style": "None" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "Bold" + }, + "Active": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "None" + }, + "Editable": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Faint" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "Bold" + }, + "Active": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "Bold" + }, + "Highlight": { + "Foreground": "Black", + "Background": "AmberPhosphor", + "Style": "None" + }, + "Editable": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "None" + }, + "ReadOnly": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Faint" + } + }, + "Error": { + "Normal": { + "Foreground": "AmberPhosphor", + "Background": "Black", + "Style": "Italic" + }, + "Active": { + "Foreground": "AmberPhosphor", + "Background": "Black", + "Style": "Bold,Italic" + }, + "Highlight": { + "Foreground": "AmberPhosphor", + "Background": "Black", + "Style": "Italic" + }, + "Editable": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Italic" + }, + "ReadOnly": { + "Foreground": "AmberPhosphor", + "Background": "Charcoal", + "Style": "Faint,Italic" } - ] + } } }, - { - "8-Bit": { - "Dialog.DefaultShadow": "None", - "FrameView.DefaultBorderStyle": "Single", - "Window.DefaultBorderStyle": "Single", - "MessageBox.DefaultBorderStyle": "Single", - "Button.DefaultShadow": "None", - "Menu.DefaultBorderStyle": "None", - "Glyphs.LeftBracket": "[", - "Glyphs.RightBracket": "]", - "Glyphs.CheckStateChecked": "X", - "Glyphs.CheckStateUnChecked": "O", - "Glyphs.CheckStateNone": " ", - "Glyphs.Selected": "*", - "Glyphs.UnSelected": " ", - "Glyphs.RightArrow": ">", - "Glyphs.LeftArrow": "<", - "Glyphs.DownArrow": "v", - "Glyphs.UpArrow": "^", - "Glyphs.LeftDefaultIndicator": ">", - "Glyphs.RightDefaultIndicator": "<", - "Glyphs.BlocksMeterSegment": "#", - "Glyphs.ContinuousMeterSegment": " ", - "Glyphs.Stipple": ".", - "Glyphs.Diamond": "+", - "Glyphs.Close": "X", - "Glyphs.Minimize": "_", - "Glyphs.Maximize": "^", - "Glyphs.Dot": ".", - "Glyphs.DottedSquare": "#", - "Glyphs.BlackCircle": "O", - "Glyphs.Expand": "+", - "Glyphs.Collapse": "-", - "Glyphs.IdenticalTo": "=", - "Glyphs.Move": "+", - "Glyphs.SizeHorizontal": "-", - "Glyphs.SizeVertical": "|", - "Glyphs.SizeTopLeft": "/", - "Glyphs.SizeTopRight": "\\", - "Glyphs.SizeBottomRight": "/", - "Glyphs.SizeBottomLeft": "\\", - "Glyphs.Apple": "@", - "Glyphs.AppleBMP": "@", - "Glyphs.HLine": "-", - "Glyphs.VLine": "|", - "Glyphs.HLineDbl": "=", - "Glyphs.VLineDbl": "|", - "Glyphs.HLineHvDa2": "-", - "Glyphs.VLineHvDa2": "|", - "Glyphs.HLineHvDa3": "-", - "Glyphs.VLineHvDa3": "|", - "Glyphs.HLineHvDa4": "-", - "Glyphs.VLineHvDa4": "|", - "Glyphs.HLineDa2": "-", - "Glyphs.VLineDa2": "|", - "Glyphs.HLineDa3": "-", - "Glyphs.VLineDa3": "|", - "Glyphs.HLineDa4": "-", - "Glyphs.VLineDa4": "|", - "Glyphs.HLineHv": "=", - "Glyphs.VLineHv": "|", - "Glyphs.HalfLeftLine": "-", - "Glyphs.HalfTopLine": "|", - "Glyphs.HalfRightLine": "-", - "Glyphs.HalfBottomLine": "|", - "Glyphs.HalfLeftLineHv": "-", - "Glyphs.HalfTopLineHv": "|", - "Glyphs.HalfRightLineHv": "-", - "Glyphs.HalfBottomLineLt": "|", - "Glyphs.RightSideLineLtHv": "-", - "Glyphs.BottomSideLineLtHv": "|", - "Glyphs.LeftSideLineHvLt": "-", - "Glyphs.TopSideLineHvLt": "|", - "Glyphs.ULCorner": "+", - "Glyphs.ULCornerDbl": "+", - "Glyphs.ULCornerR": "+", - "Glyphs.ULCornerHv": "+", - "Glyphs.ULCornerHvLt": "+", - "Glyphs.ULCornerLtHv": "+", - "Glyphs.ULCornerDblSingle": "+", - "Glyphs.ULCornerSingleDbl": "+", - "Glyphs.LLCorner": "+", - "Glyphs.LLCornerHv": "+", - "Glyphs.LLCornerHvLt": "+", - "Glyphs.LLCornerLtHv": "+", - "Glyphs.LLCornerDbl": "+", - "Glyphs.LLCornerSingleDbl": "+", - "Glyphs.LLCornerDblSingle": "+", - "Glyphs.LLCornerR": "+", - "Glyphs.URCorner": "+", - "Glyphs.URCornerDbl": "+", - "Glyphs.URCornerR": "+", - "Glyphs.URCornerHv": "+", - "Glyphs.URCornerHvLt": "+", - "Glyphs.URCornerLtHv": "+", - "Glyphs.URCornerDblSingle": "+", - "Glyphs.URCornerSingleDbl": "+", - "Glyphs.LRCorner": "+", - "Glyphs.LRCornerDbl": "+", - "Glyphs.LRCornerR": "+", - "Glyphs.LRCornerHv": "+", - "Glyphs.LRCornerDblSingle": "+", - "Glyphs.LRCornerSingleDbl": "+", - "Glyphs.LRCornerLtHv": "+", - "Glyphs.LRCornerHvLt": "+", - "Glyphs.LeftTee": "+", - "Glyphs.LeftTeeDblH": "+", - "Glyphs.LeftTeeDblV": "+", - "Glyphs.LeftTeeDbl": "+", - "Glyphs.LeftTeeHvH": "+", - "Glyphs.LeftTeeHvV": "+", - "Glyphs.LeftTeeHvDblH": "+", - "Glyphs.RightTee": "+", - "Glyphs.RightTeeDblH": "+", - "Glyphs.RightTeeDblV": "+", - "Glyphs.RightTeeDbl": "+", - "Glyphs.RightTeeHvH": "+", - "Glyphs.RightTeeHvV": "+", - "Glyphs.RightTeeHvDblH": "+", - "Glyphs.TopTee": "+", - "Glyphs.TopTeeDblH": "+", - "Glyphs.TopTeeDblV": "+", - "Glyphs.TopTeeDbl": "+", - "Glyphs.TopTeeHvH": "+", - "Glyphs.TopTeeHvV": "+", - "Glyphs.TopTeeHvDblH": "+", - "Glyphs.BottomTee": "+", - "Glyphs.BottomTeeDblH": "+", - "Glyphs.BottomTeeDblV": "+", - "Glyphs.BottomTeeDbl": "+", - "Glyphs.BottomTeeHvH": "+", - "Glyphs.BottomTeeHvV": "+", - "Glyphs.BottomTeeHvDblH": "+", - "Glyphs.Cross": "+", - "Glyphs.CrossDblH": "+", - "Glyphs.CrossDblV": "+", - "Glyphs.CrossDbl": "+", - "Glyphs.CrossHvH": "+", - "Glyphs.CrossHvV": "+", - "Glyphs.CrossHv": "+", - "Glyphs.ShadowVerticalStart": "|", - "Glyphs.ShadowVertical": "|", - "Glyphs.ShadowHorizontalStart": "-", - "Glyphs.ShadowHorizontal": "-", - "Glyphs.ShadowHorizontalEnd": "-", - "Schemes": [ - { - "Accent": { - "Normal": { - "Foreground": "White", - "Background": "Black" - }, - "Focus": { - "Foreground": "Black", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Black", - "Background": "White" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Black" - }, - "Disabled": { - "Foreground": "Black", - "Background": "Black" - }, - "Active": { - "Foreground": "Black", - "Background": "White" - }, - "HotActive": { - "Foreground": "White", - "Background": "Black" - }, - "Highlight": { - "Foreground": "Black", - "Background": "White" - }, - "Editable": { - "Foreground": "White", - "Background": "Black" - }, - "ReadOnly": { - "Foreground": "White", - "Background": "Black" - } - } - }, - { - "Base": { - "Normal": { - "Foreground": "White", - "Background": "Black" - }, - "Focus": { - "Foreground": "Black", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Black", - "Background": "White" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Black" - }, - "Disabled": { - "Foreground": "Black", - "Background": "Black" - }, - "Active": { - "Foreground": "Black", - "Background": "White" - }, - "HotActive": { - "Foreground": "White", - "Background": "Black" - }, - "Highlight": { - "Foreground": "Black", - "Background": "White" - }, - "Editable": { - "Foreground": "White", - "Background": "Black" - }, - "ReadOnly": { - "Foreground": "White", - "Background": "Black" - } - } - }, - { - "Dialog": { - "Normal": { - "Foreground": "Black", - "Background": "White" - }, - "Focus": { - "Foreground": "White", - "Background": "Black" - }, - "HotNormal": { - "Foreground": "White", - "Background": "Black" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "White" - }, - "Disabled": { - "Foreground": "White", - "Background": "White" - }, - "Active": { - "Foreground": "White", - "Background": "Black" - }, - "HotActive": { - "Foreground": "Black", - "Background": "White" - }, - "Highlight": { - "Foreground": "White", - "Background": "Black" - }, - "Editable": { - "Foreground": "Black", - "Background": "White" - }, - "ReadOnly": { - "Foreground": "Black", - "Background": "White" - } - } - }, - { - "Menu": { - "Normal": { - "Foreground": "Black", - "Background": "White" - }, - "Focus": { - "Foreground": "White", - "Background": "Black" - }, - "HotNormal": { - "Foreground": "White", - "Background": "Black" - }, - "HotFocus": { - "Foreground": "Black", - "Background": "White" - }, - "Disabled": { - "Foreground": "White", - "Background": "White" - }, - "Active": { - "Foreground": "White", - "Background": "Black" - }, - "HotActive": { - "Foreground": "Black", - "Background": "White" - }, - "Highlight": { - "Foreground": "White", - "Background": "Black" - }, - "Editable": { - "Foreground": "Black", - "Background": "White" - }, - "ReadOnly": { - "Foreground": "Black", - "Background": "White" - } - } - }, - { - "Error": { - "Normal": { - "Foreground": "White", - "Background": "Black" - }, - "Focus": { - "Foreground": "Black", - "Background": "White" - }, - "HotNormal": { - "Foreground": "Black", - "Background": "White" - }, - "HotFocus": { - "Foreground": "White", - "Background": "Black" - }, - "Disabled": { - "Foreground": "Black", - "Background": "Black" - }, - "Active": { - "Foreground": "Black", - "Background": "White" - }, - "HotActive": { - "Foreground": "White", - "Background": "Black" - }, - "Highlight": { - "Foreground": "Black", - "Background": "White" - }, - "Editable": { - "Foreground": "White", - "Background": "Black" - }, - "ReadOnly": { - "Foreground": "White", - "Background": "Black" - } - } + "8-Bit": { + "Dialog": { + "DefaultShadow": "None" + }, + "FrameView": { + "DefaultBorderStyle": "Single" + }, + "Window": { + "DefaultBorderStyle": "Single" + }, + "MessageBox": { + "DefaultBorderStyle": "Single" + }, + "Button": { + "DefaultShadow": "None" + }, + "Menu": { + "DefaultBorderStyle": "None" + }, + "Glyphs": { + "LeftBracket": "[", + "RightBracket": "]", + "CheckStateChecked": "X", + "CheckStateUnChecked": "O", + "CheckStateNone": " ", + "Selected": "*", + "UnSelected": " ", + "RightArrow": ">", + "LeftArrow": "<", + "DownArrow": "v", + "UpArrow": "^", + "LeftDefaultIndicator": ">", + "RightDefaultIndicator": "<", + "BlocksMeterSegment": "#", + "ContinuousMeterSegment": " ", + "Stipple": ".", + "Diamond": "+", + "Close": "X", + "Minimize": "_", + "Maximize": "^", + "Dot": ".", + "DottedSquare": "#", + "BlackCircle": "O", + "Expand": "+", + "Collapse": "-", + "IdenticalTo": "=", + "Move": "+", + "SizeHorizontal": "-", + "SizeVertical": "|", + "SizeTopLeft": "/", + "SizeTopRight": "\\", + "SizeBottomRight": "/", + "SizeBottomLeft": "\\", + "Apple": "@", + "AppleBMP": "@", + "HLine": "-", + "VLine": "|", + "HLineDbl": "=", + "VLineDbl": "|", + "HLineHvDa2": "-", + "VLineHvDa2": "|", + "HLineHvDa3": "-", + "VLineHvDa3": "|", + "HLineHvDa4": "-", + "VLineHvDa4": "|", + "HLineDa2": "-", + "VLineDa2": "|", + "HLineDa3": "-", + "VLineDa3": "|", + "HLineDa4": "-", + "VLineDa4": "|", + "HLineHv": "=", + "VLineHv": "|", + "HalfLeftLine": "-", + "HalfTopLine": "|", + "HalfRightLine": "-", + "HalfBottomLine": "|", + "HalfLeftLineHv": "-", + "HalfTopLineHv": "|", + "HalfRightLineHv": "-", + "HalfBottomLineLt": "|", + "RightSideLineLtHv": "-", + "BottomSideLineLtHv": "|", + "LeftSideLineHvLt": "-", + "TopSideLineHvLt": "|", + "ULCorner": "+", + "ULCornerDbl": "+", + "ULCornerR": "+", + "ULCornerHv": "+", + "ULCornerHvLt": "+", + "ULCornerLtHv": "+", + "ULCornerDblSingle": "+", + "ULCornerSingleDbl": "+", + "LLCorner": "+", + "LLCornerHv": "+", + "LLCornerHvLt": "+", + "LLCornerLtHv": "+", + "LLCornerDbl": "+", + "LLCornerSingleDbl": "+", + "LLCornerDblSingle": "+", + "LLCornerR": "+", + "URCorner": "+", + "URCornerDbl": "+", + "URCornerR": "+", + "URCornerHv": "+", + "URCornerHvLt": "+", + "URCornerLtHv": "+", + "URCornerDblSingle": "+", + "URCornerSingleDbl": "+", + "LRCorner": "+", + "LRCornerDbl": "+", + "LRCornerR": "+", + "LRCornerHv": "+", + "LRCornerDblSingle": "+", + "LRCornerSingleDbl": "+", + "LRCornerLtHv": "+", + "LRCornerHvLt": "+", + "LeftTee": "+", + "LeftTeeDblH": "+", + "LeftTeeDblV": "+", + "LeftTeeDbl": "+", + "LeftTeeHvH": "+", + "LeftTeeHvV": "+", + "LeftTeeHvDblH": "+", + "RightTee": "+", + "RightTeeDblH": "+", + "RightTeeDblV": "+", + "RightTeeDbl": "+", + "RightTeeHvH": "+", + "RightTeeHvV": "+", + "RightTeeHvDblH": "+", + "TopTee": "+", + "TopTeeDblH": "+", + "TopTeeDblV": "+", + "TopTeeDbl": "+", + "TopTeeHvH": "+", + "TopTeeHvV": "+", + "TopTeeHvDblH": "+", + "BottomTee": "+", + "BottomTeeDblH": "+", + "BottomTeeDblV": "+", + "BottomTeeDbl": "+", + "BottomTeeHvH": "+", + "BottomTeeHvV": "+", + "BottomTeeHvDblH": "+", + "Cross": "+", + "CrossDblH": "+", + "CrossDblV": "+", + "CrossDbl": "+", + "CrossHvH": "+", + "CrossHvV": "+", + "CrossHv": "+", + "ShadowVerticalStart": "|", + "ShadowVertical": "|", + "ShadowHorizontalStart": "-", + "ShadowHorizontal": "-", + "ShadowHorizontalEnd": "-" + }, + "Schemes": { + "Accent": { + "Normal": { + "Foreground": "White", + "Background": "Black" + }, + "Focus": { + "Foreground": "Black", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Black", + "Background": "White" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Black" + }, + "Disabled": { + "Foreground": "Black", + "Background": "Black" + }, + "Active": { + "Foreground": "Black", + "Background": "White" + }, + "HotActive": { + "Foreground": "White", + "Background": "Black" + }, + "Highlight": { + "Foreground": "Black", + "Background": "White" + }, + "Editable": { + "Foreground": "White", + "Background": "Black" + }, + "ReadOnly": { + "Foreground": "White", + "Background": "Black" + } + }, + "Base": { + "Normal": { + "Foreground": "White", + "Background": "Black" + }, + "Focus": { + "Foreground": "Black", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Black", + "Background": "White" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Black" + }, + "Disabled": { + "Foreground": "Black", + "Background": "Black" + }, + "Active": { + "Foreground": "Black", + "Background": "White" + }, + "HotActive": { + "Foreground": "White", + "Background": "Black" + }, + "Highlight": { + "Foreground": "Black", + "Background": "White" + }, + "Editable": { + "Foreground": "White", + "Background": "Black" + }, + "ReadOnly": { + "Foreground": "White", + "Background": "Black" + } + }, + "Dialog": { + "Normal": { + "Foreground": "Black", + "Background": "White" + }, + "Focus": { + "Foreground": "White", + "Background": "Black" + }, + "HotNormal": { + "Foreground": "White", + "Background": "Black" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "White" + }, + "Disabled": { + "Foreground": "White", + "Background": "White" + }, + "Active": { + "Foreground": "White", + "Background": "Black" + }, + "HotActive": { + "Foreground": "Black", + "Background": "White" + }, + "Highlight": { + "Foreground": "White", + "Background": "Black" + }, + "Editable": { + "Foreground": "Black", + "Background": "White" + }, + "ReadOnly": { + "Foreground": "Black", + "Background": "White" + } + }, + "Menu": { + "Normal": { + "Foreground": "Black", + "Background": "White" + }, + "Focus": { + "Foreground": "White", + "Background": "Black" + }, + "HotNormal": { + "Foreground": "White", + "Background": "Black" + }, + "HotFocus": { + "Foreground": "Black", + "Background": "White" + }, + "Disabled": { + "Foreground": "White", + "Background": "White" + }, + "Active": { + "Foreground": "White", + "Background": "Black" + }, + "HotActive": { + "Foreground": "Black", + "Background": "White" + }, + "Highlight": { + "Foreground": "White", + "Background": "Black" + }, + "Editable": { + "Foreground": "Black", + "Background": "White" + }, + "ReadOnly": { + "Foreground": "Black", + "Background": "White" + } + }, + "Error": { + "Normal": { + "Foreground": "White", + "Background": "Black" + }, + "Focus": { + "Foreground": "Black", + "Background": "White" + }, + "HotNormal": { + "Foreground": "Black", + "Background": "White" + }, + "HotFocus": { + "Foreground": "White", + "Background": "Black" + }, + "Disabled": { + "Foreground": "Black", + "Background": "Black" + }, + "Active": { + "Foreground": "Black", + "Background": "White" + }, + "HotActive": { + "Foreground": "White", + "Background": "Black" + }, + "Highlight": { + "Foreground": "Black", + "Background": "White" + }, + "Editable": { + "Foreground": "White", + "Background": "Black" + }, + "ReadOnly": { + "Foreground": "White", + "Background": "Black" } - ] - + } } } - ] + } } diff --git a/Terminal.Gui/Text/NerdFonts.cs b/Terminal.Gui/Text/NerdFonts.cs index 2253a3ecef..dd1d05a1c3 100644 --- a/Terminal.Gui/Text/NerdFonts.cs +++ b/Terminal.Gui/Text/NerdFonts.cs @@ -15,12 +15,7 @@ internal class NerdFonts /// If , enables the use of Nerd unicode symbols. This requires specific font(s) to be /// installed on the users machine to work correctly. Defaults to . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static bool Enable - { - get => NerdFontsSettings.Defaults.Enable; - set => NerdFontsSettings.Defaults.Enable = value; - } + public static bool Enable => NerdFontsSettings.Current.Enable; /// Mapping of file extension to name. public Dictionary ExtensionToIcon { get; set; } = new () diff --git a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs index 58c5b108ab..0da318c9be 100644 --- a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs +++ b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs @@ -108,6 +108,7 @@ public ArrangeButtons ButtonType } _buttonType = value; + SetTextDirect (GetButtonGlyph ()); ApplyOrientationAndDirection (); SetupKeyBindings (); } @@ -123,19 +124,15 @@ public ArrangeButtons ButtonType public NavigationDirection Direction { get; set; } /// - public override string Text - { - get => - ButtonType switch - { - ArrangeButtons.Move => $"{Glyphs.Move}", - ArrangeButtons.AllSize => $"{Glyphs.SizeBottomRight}", - ArrangeButtons.LeftSize or ArrangeButtons.RightSize => $"{Glyphs.SizeHorizontal}", - ArrangeButtons.TopSize or ArrangeButtons.BottomSize => $"{Glyphs.SizeVertical}", - _ => base.Text - }; - set => base.Text = value; - } + private string GetButtonGlyph () => + ButtonType switch + { + ArrangeButtons.Move => $"{Glyphs.Move}", + ArrangeButtons.AllSize => $"{Glyphs.SizeBottomRight}", + ArrangeButtons.LeftSize or ArrangeButtons.RightSize => $"{Glyphs.SizeHorizontal}", + ArrangeButtons.TopSize or ArrangeButtons.BottomSize => $"{Glyphs.SizeVertical}", + _ => Text + }; /// /// Sets and based on . diff --git a/Terminal.Gui/ViewBase/Adornment/BorderView.Arrangement.cs b/Terminal.Gui/ViewBase/Adornment/BorderView.Arrangement.cs index c52fb2beef..fa96668bf1 100644 --- a/Terminal.Gui/ViewBase/Adornment/BorderView.Arrangement.cs +++ b/Terminal.Gui/ViewBase/Adornment/BorderView.Arrangement.cs @@ -8,7 +8,6 @@ public partial class BorderView /// Gets or sets mouse bindings unique to . /// Shared bindings come from . /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public new static Dictionary? DefaultMouseBindings { get; set; } = new () { [Command.Arrange] = BindMouse.All (MouseFlags.LeftButtonPressed) diff --git a/Terminal.Gui/ViewBase/Adornment/TitleView.cs b/Terminal.Gui/ViewBase/Adornment/TitleView.cs index 0274ce5bb2..49c2cd5d1f 100644 --- a/Terminal.Gui/ViewBase/Adornment/TitleView.cs +++ b/Terminal.Gui/ViewBase/Adornment/TitleView.cs @@ -112,7 +112,12 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri public NavigationDirection Direction { get; set; } /// - public override string Text { get => base.Text; set => base.Text = Title = value; } + protected override void OnTextChanged () + { + Title = Text; + + base.OnTextChanged (); + } /// /// Binds the appropriate arrow keys to their directional based on diff --git a/Terminal.Gui/ViewBase/Mouse/View.Mouse.cs b/Terminal.Gui/ViewBase/Mouse/View.Mouse.cs index 72564e230c..4f124c3c5c 100644 --- a/Terminal.Gui/ViewBase/Mouse/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/Mouse/View.Mouse.cs @@ -19,7 +19,6 @@ public partial class View // Mouse APIs /// Do not set in parallelizable unit tests. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Dictionary? DefaultMouseBindings { get; @@ -48,7 +47,6 @@ public static Dictionary? DefaultMouseBindings /// Do not set in parallelizable unit tests. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Dictionary>? ViewMouseBindings { get; set; } private void SetupMouse () diff --git a/Terminal.Gui/ViewBase/View.Keyboard.cs b/Terminal.Gui/ViewBase/View.Keyboard.cs index cc68dd04a4..6c5a28074a 100644 --- a/Terminal.Gui/ViewBase/View.Keyboard.cs +++ b/Terminal.Gui/ViewBase/View.Keyboard.cs @@ -1,7 +1,5 @@ using Terminal.Gui.Tracing; -#pragma warning disable CS0618 // Obsolete - View.Keyboard still uses ConfigurationPropertyAttribute during transition - namespace Terminal.Gui.ViewBase; public partial class View // Keyboard APIs @@ -651,11 +649,14 @@ public bool NewKeyUpEvent (Key key) /// Gets or sets the default key bindings shared across all views. Only commands that a view supports /// (via ) are actually bound when is called. /// + /// Nested JSON View.DefaultKeyBindings overlays this dictionary by command when + /// runs. Unmentioned commands keep these defaults. + /// + /// /// IMPORTANT: This is a process-wide static property. Change with care. /// Do not set in parallelizable unit tests. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Dictionary? DefaultKeyBindings { get; @@ -706,12 +707,13 @@ public static Dictionary? DefaultKeyBindings /// /// Gets or sets per-view key binding overrides from configuration. The outer key is the view type name /// (e.g., "TextField"), the inner dictionary maps commands to platform key bindings. + /// Nested JSON View.ViewKeyBindings overlays this dictionary when + /// runs. /// /// IMPORTANT: This is a process-wide static property. Change with care. /// Do not set in parallelizable unit tests. /// /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Dictionary>? ViewKeyBindings { get; set; } /// @@ -732,7 +734,7 @@ protected void ApplyKeyBindings (params Dictionary? ApplyLayer (layer, supported); } - // Apply user overrides from View.ViewKeyBindings (CM/MEC bridge) + // Apply user overrides from View.ViewKeyBindings (config JSON and/or code). // Strip generic arity suffix (e.g., "TreeView`1" → "TreeView") so config keys match string typeName = GetType ().Name; int backtick = typeName.IndexOf ('`'); diff --git a/Terminal.Gui/ViewBase/View.Text.cs b/Terminal.Gui/ViewBase/View.Text.cs index 820f0689ff..de582df631 100644 --- a/Terminal.Gui/ViewBase/View.Text.cs +++ b/Terminal.Gui/ViewBase/View.Text.cs @@ -1,14 +1,11 @@ +using System.ComponentModel; + namespace Terminal.Gui.ViewBase; public partial class View // Text Property APIs { private string _text = string.Empty; - /// - /// Called when the has changed. Fires the event. - /// - public void OnTextChanged () => TextChanged?.Invoke (this, EventArgs.Empty); - /// /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved /// or not when is enabled. @@ -50,9 +47,19 @@ public bool PreserveTrailingSpaces /// If or are using , /// the will be adjusted to fit the text. /// - /// When the text changes, the is fired. + /// + /// Setting to the same value as the current value is a no-op; neither + /// nor will be raised. + /// + /// + /// Before the text is changed, the CWP hook is invoked. If cancelled, + /// the text remains unchanged and is not raised. + /// + /// + /// After the text is changed, the event is raised. + /// /// - public virtual string Text + public string Text { get => _text; set @@ -62,22 +69,116 @@ public virtual string Text return; } - _text = value; + if (OnTextChanging (value)) + { + return; + } + _text = value; UpdateTextFormatterText (); if (TryRedrawWithoutLayout ()) { - OnTextChanged (); + RaiseTextChanged (); return; } SetNeedsLayout (); - OnTextChanged (); + RaiseTextChanged (); } } + /// + /// Sets the backing field directly without raising + /// or events and without invoking the + /// or virtual methods. + /// + /// + /// + /// Use this method in derived views that maintain an internal text model (e.g., an editor + /// buffer) and need to keep in sync after internal edits without + /// re-entering the CWP flow. + /// + /// + /// The new text value to store. + protected void SetTextDirect (string value) + { + _text = value; + UpdateTextFormatterText (); + SetNeedsLayout (); + } + + /// + /// Called before the changes. Invokes the event, which can + /// be cancelled. + /// + /// + /// + /// The base implementation raises the event. Override in derived views + /// to perform validation or fire control-specific pre-change events. + /// + /// + /// The proposed new text value. + /// if the text change should be cancelled; otherwise . + protected virtual bool OnTextChanging (string newText) + { + CancelEventArgs args = new (); + TextChanging?.Invoke (this, args); + + return args.Cancel; + } + + /// + /// Raised when the is about to change. Set to + /// to prevent the change. + /// + /// + /// + /// This is a signal-only notification at the level. It does not carry old or new + /// text values. Derived controls that need richer text-edit semantics may expose their own specific events. + /// + /// + public event EventHandler? TextChanging; + + /// + /// Called after the has been changed. + /// + /// + /// + /// Override in derived views to react to text changes. The base implementation is empty. + /// The event is raised by the caller after this method returns. + /// + /// + protected virtual void OnTextChanged () { } + + /// + /// Invokes the CWP post-change workflow for : calls + /// then raises . + /// + /// + /// + /// Derived views that bypass the base setter (e.g., using new) should + /// call this method after mutating text to participate in the CWP workflow. + /// + /// + protected internal void RaiseTextChanged () + { + OnTextChanged (); + TextChanged?.Invoke (this, EventArgs.Empty); + } + + /// + /// Raised after the has been changed. + /// + /// + /// + /// This is a signal-only notification at the level. It does not carry old or new + /// text values. Derived controls that need richer text-edit semantics may expose their own specific events. + /// + /// + public event EventHandler? TextChanged; + /// /// If the new resolves to the same (position and size) the view has now, /// handles the change with a redraw only — skipping and the ancestor layout @@ -164,11 +265,6 @@ public Alignment TextAlignment } } - /// - /// Text changed event, raised when the text has changed. - /// - public event EventHandler? TextChanged; - /// /// Gets or sets the direction of the View's . Changing this property will redisplay the /// . diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs index c104c8a86f..2867509746 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteFilepathContext.cs @@ -42,7 +42,7 @@ public IEnumerable GenerateSuggestions (AutocompleteContext context) return []; } - bool isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows); + bool isWindows = OperatingSystem.IsWindows (); string? [] suggestions = _state?.Children.Where (d => !d.IsParent) .Select (e => e.FileSystemInfo is IDirectoryInfo d ? d.Name + Path.DirectorySeparatorChar : e.FileSystemInfo?.Name) diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 3e05362cc2..74dcbe5686 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -53,22 +53,12 @@ public class Button : View, IDesignable, IAcceptTarget /// /// Gets or sets whether s are shown with a shadow effect by default. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => ButtonSettings.Defaults.DefaultShadow; - set => ButtonSettings.Defaults.DefaultShadow = value; - } + public static ShadowStyles DefaultShadow => ButtonSettings.Current.DefaultShadow; /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => ButtonSettings.Defaults.DefaultMouseHighlightStates; - set => ButtonSettings.Defaults.DefaultMouseHighlightStates = value; - } + public static MouseState DefaultMouseHighlightStates => ButtonSettings.Current.DefaultMouseHighlightStates; /// Initializes a new instance of . public Button () @@ -108,7 +98,7 @@ public Button () /// /// Called before the Button's initial is applied during construction. - /// Override to change or suppress the default shadow — set + /// Override to change or suppress the default shadow — set /// to the desired style, or set to /// to skip applying any shadow. /// @@ -131,10 +121,10 @@ private void RaiseInitializingShadowStyle () { ValueChangingEventArgs args = new (null, DefaultShadow); - // 1. Virtual method — subclasses override to change/suppress the default shadow. + // 1. Virtual method — subclasses override to change/suppress the default shadow. OnInitializingShadowStyle (args); - // 2. Event — external subscribers get a chance to customize. + // 2. Event — external subscribers get a chance to customize. InitializingShadowStyle?.Invoke (this, args); // 3. Apply the (potentially modified) shadow style unless already handled. @@ -179,13 +169,18 @@ private void SetMouseBindings (MouseFlags? mouseHoldRepeat) private void Button_TitleChanged (object? sender, EventArgs e) { - base.Text = e.Value; + SetTextDirect (e.Value); TextFormatter.HotKeySpecifier = HotKeySpecifier; _interiorTextFormatter.HotKeySpecifier = HotKeySpecifier; } /// - public override string Text { get => Title; set => base.Text = Title = value; } + protected override void OnTextChanged () + { + Title = Text; + + base.OnTextChanged (); + } /// public override Rune HotKeySpecifier diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 86c3f8cab3..394dd31346 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -54,12 +54,7 @@ public class CharMap : View, IDesignable, IValue /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => CharMapSettings.Defaults.DefaultCursorStyle; - set => CharMapSettings.Defaults.DefaultCursorStyle = value; - } + public static CursorStyle DefaultCursorStyle => CharMapSettings.Current.DefaultCursorStyle; private const int COLUMN_WIDTH = 3; // Width of each column of glyphs private const int HEADER_HEIGHT = 1; // Height of the header diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index df991b4cc5..28d9763dbd 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -24,12 +24,7 @@ public class CheckBox : View, IValue /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => CheckBoxSettings.Defaults.DefaultMouseHighlightStates; - set => CheckBoxSettings.Defaults.DefaultMouseHighlightStates = value; - } + public static MouseState DefaultMouseHighlightStates => CheckBoxSettings.Current.DefaultMouseHighlightStates; /// /// Initializes a new instance of . @@ -68,12 +63,17 @@ protected override void OnActivated (ICommandContext? commandContext) private void Checkbox_TitleChanged (object? sender, EventArgs e) { - base.Text = e.Value; + SetTextDirect (e.Value); TextFormatter.HotKeySpecifier = HotKeySpecifier; } /// - public override string Text { get => Title; set => base.Text = Title = value; } + protected override void OnTextChanged () + { + Title = Text; + + base.OnTextChanged (); + } /// public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; } diff --git a/Terminal.Gui/Views/Code.cs b/Terminal.Gui/Views/Code.cs index 9bbb6b6e78..56be0aa804 100644 --- a/Terminal.Gui/Views/Code.cs +++ b/Terminal.Gui/Views/Code.cs @@ -16,19 +16,11 @@ public Code () } /// Gets or sets the source text to render. - public override string Text + protected override void OnTextChanged () { - get => base.Text; - set - { - if (base.Text == value) - { - return; - } + UpdateStyledLines (); - base.Text = value; - UpdateStyledLines (); - } + base.OnTextChanged (); } /// Gets or sets the language hint used for syntax highlighting. @@ -105,6 +97,9 @@ protected override bool OnClearingViewport () return true; } + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnDrawingContent (DrawContext? context) { @@ -176,4 +171,4 @@ public sealed class CodeRoleSample /// string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:800"; -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs index 8be7577efa..3a97c0329d 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.cs @@ -105,6 +105,9 @@ public void ApplyStyleChanges () SetNeedsLayout (); } + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnDrawingContent (DrawContext? context) { @@ -154,16 +157,26 @@ protected override bool OnDrawingContent (DrawContext? context) protected virtual void OnValueChanged (ValueChangedEventArgs args) { } /// - public override string Text + protected override bool OnTextChanging (string newText) { - get => SelectedColor.ToString (); - set + // Reject text that cannot be parsed as a valid color + if (!_colorNameResolver.TryParseColor (newText, out _)) { - if (_colorNameResolver.TryParseColor (value, out Color newColor)) - { - SelectedColor = newColor; - } + return true; + } + + return base.OnTextChanging (newText); + } + + /// + protected override void OnTextChanged () + { + if (_colorNameResolver.TryParseColor (Text, out Color newColor)) + { + SelectedColor = newColor; } + + base.OnTextChanged (); } /// @@ -289,6 +302,9 @@ private void SetSelectedColor (Color value, bool syncBars) // Do the work _selectedColor = value; + // Keep Text in sync with the new color + SetTextDirect (_selectedColor.ToString ()); + // CWP: Fire ValueChanged ValueChangedEventArgs changedArgs = new (oldValue, value); OnValueChanged (changedArgs); diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index 23a3fdaddc..cf67b4204e 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -50,16 +50,26 @@ public CultureInfo? Culture } /// - public override string Text + protected override bool OnTextChanging (string newText) { - get => Value.ToString (Format); - set + // Reject text that cannot be parsed as a valid DateTime + if (!DateTime.TryParse (newText, out _)) { - if (DateTime.TryParse (value, out DateTime result)) - { - Value = result; - } + return true; + } + + return base.OnTextChanging (newText); + } + + /// + protected override void OnTextChanged () + { + if (DateTime.TryParse (Text, out DateTime result)) + { + Value = result; } + + base.OnTextChanged (); } #region IValue Implementation @@ -93,6 +103,9 @@ public DateTime Value _date = value; + // Keep Text in sync with the new date value + SetTextDirect (_date.ToString (Format)); + // Propagate value to embedded editor _dateEditor?.Value = value; diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 5da9e0e0c9..6e5a65bbbb 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -62,44 +62,24 @@ public class Dialog : Dialog { /// /// The default border style for new instances. Can be configured via - /// and theme files. + /// and theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => DialogSettings.Defaults.DefaultBorderStyle; - set => DialogSettings.Defaults.DefaultBorderStyle = value; - } + public static LineStyle DefaultBorderStyle => DialogSettings.Current.DefaultBorderStyle; /// /// The default button alignment for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment - { - get => DialogSettings.Defaults.DefaultButtonAlignment; - set => DialogSettings.Defaults.DefaultButtonAlignment = value; - } + public static Alignment DefaultButtonAlignment => DialogSettings.Current.DefaultButtonAlignment; /// /// The default button alignment modes for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static AlignmentModes DefaultButtonAlignmentModes - { - get => DialogSettings.Defaults.DefaultButtonAlignmentModes; - set => DialogSettings.Defaults.DefaultButtonAlignmentModes = value; - } + public static AlignmentModes DefaultButtonAlignmentModes => DialogSettings.Current.DefaultButtonAlignmentModes; /// /// The default shadow style for new instances. Can be configured via theme files. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => DialogSettings.Defaults.DefaultShadow; - set => DialogSettings.Defaults.DefaultShadow = value; - } + public static ShadowStyles DefaultShadow => DialogSettings.Current.DefaultShadow; /// /// Helper property that gets whether the dialog was canceled (Result is or 1). diff --git a/Terminal.Gui/Views/DropDownList.cs b/Terminal.Gui/Views/DropDownList.cs index aa6a5ec4c8..4e35f18913 100644 --- a/Terminal.Gui/Views/DropDownList.cs +++ b/Terminal.Gui/Views/DropDownList.cs @@ -234,6 +234,10 @@ protected override bool OnHasFocusChanging (bool currentHasFocus, bool newHasFoc return false; } + // Hide while the popover is still active so Hide can invalidate the covered region. + // DeRegister after that also hides if still active; doing Hide here covers the + // keyboard Accepting+Handled path that moves focus before OnAccepted runs (#5635). + App?.Popovers?.Hide (_listPopover); App?.Popovers?.DeRegister (_listPopover); return false; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 0529ac71d6..3820beb999 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -372,7 +372,6 @@ public override void EndInit () /// The maximum number of results that will be collected when searching before stopping. /// This prevents performance issues e.g. when searching root of file system for a common letter (e.g. 'e'). - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static int MaxSearchResults { get => FileDialogSettings.Defaults.MaxSearchResults; diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs b/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs index d66e0fa3c4..1be64c6b61 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialogStyle.cs @@ -42,9 +42,8 @@ public FileDialogStyle (IFileSystem? fileSystem) /// /// Gets or sets the default value to use for . This can be populated from .tui config - /// files via + /// files via /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool DefaultUseColors { get => FileDialogStyleSettings.Defaults.DefaultUseColors; @@ -53,9 +52,8 @@ public static bool DefaultUseColors /// /// Gets or sets the default value to use for . This can be populated from .tui - /// config files via + /// config files via /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static bool DefaultUseUnicodeCharacters { get => FileDialogStyleSettings.Defaults.DefaultUseUnicodeCharacters; diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index 68ad2940fd..209c98f10b 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -34,16 +34,11 @@ public FrameView () /// /// Defines the default border styling for . Can be configured via - /// . + /// . /// /// /// This property can be set in a Theme to change the default for all /// s. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => FrameViewSettings.Defaults.DefaultBorderStyle; - set => FrameViewSettings.Defaults.DefaultBorderStyle = value; - } + public static LineStyle DefaultBorderStyle => FrameViewSettings.Current.DefaultBorderStyle; } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index e772bf72f0..47a43211cb 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -80,12 +80,7 @@ public class HexView : View, IDesignable /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => HexViewSettings.Defaults.DefaultCursorStyle; - set => HexViewSettings.Defaults.DefaultCursorStyle = value; - } + public static CursorStyle DefaultCursorStyle => HexViewSettings.Current.DefaultCursorStyle; /// /// Gets or sets the view-specific default key bindings for . Contains only bindings diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 7615112cc7..7eb864248c 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -29,12 +29,17 @@ public Label () private void Label_TitleChanged (object? sender, EventArgs e) { - base.Text = e.Value; + SetTextDirect (e.Value); TextFormatter.HotKeySpecifier = HotKeySpecifier; } /// - public override string Text { get => Title; set => base.Text = Title = value; } + protected override void OnTextChanged () + { + Title = Text; + + base.OnTextChanged (); + } /// public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; } diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs index 04f5b7e145..11bf70e814 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeDefaults.cs @@ -8,10 +8,5 @@ namespace Terminal.Gui.Views; public static class LinearRangeDefaults { /// Gets or sets the default cursor style applied to a new linear range view. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => LinearRangeSettings.Defaults.DefaultCursorStyle; - set => LinearRangeSettings.Defaults.DefaultCursorStyle = value; - } + public static CursorStyle DefaultCursorStyle => LinearRangeSettings.Current.DefaultCursorStyle; } diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs b/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs index 522b8eac67..48c938373e 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeViewBase.cs @@ -67,7 +67,7 @@ public abstract class LinearRangeViewBase : View, IOrientation, /// /// /// - /// No is applied because this is a generic + /// This property is not configuration-bound because this is a generic /// type. Use with key "LinearRange" to override bindings via /// configuration. /// @@ -111,7 +111,7 @@ private void SetInitialProperties (List> options, Ori SubViewLayout += (_, _) => { SetContentSize (); }; } - // TODO: Make configurable via ConfigurationManager + // TODO: Make configurable via TuiConfigurationBuilder private void SetDefaultStyle () { _config._showLegends = true; @@ -189,22 +189,21 @@ protected LinearRangeViewBase (List? options, Orientation orientation, /// /// Setting the Text of a linear range is a shortcut to setting options. The text is a CSV string of the options. /// - public override string Text + protected override void OnTextChanged () { - // Return labels as a CSV string - get => _options is null or { Count: 0 } ? string.Empty : string.Join (",", _options); - set + string value = Text; + + if (string.IsNullOrEmpty (value)) { - if (string.IsNullOrEmpty (value)) - { - Options = []; - } - else - { - IEnumerable list = value.Split (',').Select (x => x.Trim ()); - Options = list.Select (x => new LinearRangeOption { Legend = x }).ToList (); - } + Options = []; } + else + { + IEnumerable list = value.Split (',').Select (x => x.Trim ()); + Options = list.Select (x => new LinearRangeOption { Legend = x }).ToList (); + } + + base.OnTextChanged (); } /// Allow no selection. @@ -370,6 +369,10 @@ public List> Options // Drop any selected indices that are no longer valid _setOptions.RemoveAll (i => i < 0 || i >= _options.Count); + // Keep Text in sync with Options + string csv = _options.Count == 0 ? string.Empty : string.Join (",", _options); + SetTextDirect (csv); + if (_options.Count == 0) { return; @@ -1004,6 +1007,9 @@ private void UpdateCursor () #region Drawing + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnDrawingContent (DrawContext? context) { diff --git a/Terminal.Gui/Views/Link.cs b/Terminal.Gui/Views/Link.cs index d24d682ea0..6f86f3a4ce 100644 --- a/Terminal.Gui/Views/Link.cs +++ b/Terminal.Gui/Views/Link.cs @@ -185,15 +185,15 @@ public static void OpenUrl (string url) try { - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); } - else if (PlatformDetection.IsMac ()) + else if (OperatingSystem.IsMacOS ()) { Process.Start ("open", url); } - else if (PlatformDetection.IsLinux ()) + else if (OperatingSystem.IsLinux ()) { using Process process = new (); diff --git a/Terminal.Gui/Views/Markdown/Markdown.cs b/Terminal.Gui/Views/Markdown/Markdown.cs index 57a7303c2b..1f2ed440dc 100644 --- a/Terminal.Gui/Views/Markdown/Markdown.cs +++ b/Terminal.Gui/Views/Markdown/Markdown.cs @@ -124,7 +124,23 @@ protected override bool OnActivating (CommandEventArgs args) /// Gets or sets the Markdown-formatted text displayed by this view. /// The raw Markdown string. Setting this property triggers reparsing, re-layout, and a redraw. - public override string Text { get => _markdown; set => SetMarkdown (value); } + protected override void OnTextChanged () + { + SetMarkdown (Text); + + base.OnTextChanged (); + } + + /// + /// + /// does not use for rendering. + /// It uses its own styled-line pipeline. Clearing the formatter text prevents the base + /// from drawing raw markdown as plain text. + /// + protected override void UpdateTextFormatterText () + { + TextFormatter.Text = string.Empty; + } /// /// diff --git a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs index 1ee078382f..a1b046dfc3 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs @@ -43,15 +43,17 @@ public MarkdownCodeBlock () /// (```lang\ncode\n```) and extracts the language automatically. Plain text /// (without fences) is also accepted and treated as language-less code. /// - public override string Text + protected override void OnTextChanged () { - get - { - string body = string.Join ("\n", CodeLines); + ParseFencedText (Text); - return !string.IsNullOrEmpty (Language) ? $"```{Language}\n{body}\n```" : body; - } - set => ParseFencedText (value); + base.OnTextChanged (); + } + + /// + protected override void UpdateTextFormatterText () + { + TextFormatter.Text = string.Empty; } /// diff --git a/Terminal.Gui/Views/Markdown/MarkdownTable.cs b/Terminal.Gui/Views/Markdown/MarkdownTable.cs index 04275e5181..0f22da723e 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownTable.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownTable.cs @@ -86,55 +86,61 @@ public MarkdownTable () /// The setter parses the text via and updates . /// Invalid or empty text clears the table. /// - public override string Text + protected override void OnTextChanged () { - get + string value = Text; + + if (string.IsNullOrWhiteSpace (value)) { - if (_data.ColumnCount == 0) - { - return string.Empty; - } + TableData = _emptyData; + } + else + { + string [] lines = value.Split ('\n', StringSplitOptions.RemoveEmptyEntries); + TableData? parsed = TableData.TryParse (lines); + TableData = parsed ?? _emptyData; + } - // Reconstruct pipe-delimited table text - List lines = [$"| {string.Join (" | ", _data.Headers)} |"]; + base.OnTextChanged (); + } - // Separator row - var seps = new string [_data.ColumnCount]; + /// + protected override void UpdateTextFormatterText () + { + TextFormatter.Text = string.Empty; + } - for (var i = 0; i < _data.ColumnCount; i++) - { - seps [i] = _data.ColumnAlignments [i] switch - { - Alignment.Center => ":---:", - Alignment.End => "---:", - _ => "---" - }; - } + private string BuildTableText () + { + if (_data.ColumnCount == 0) + { + return string.Empty; + } - lines.Add ($"| {string.Join (" | ", seps)} |"); + // Reconstruct pipe-delimited table text + List lines = [$"| {string.Join (" | ", _data.Headers)} |"]; - foreach (string [] row in _data.Rows) - { - lines.Add ($"| {string.Join (" | ", row)} |"); - } + // Separator row + string [] seps = new string [_data.ColumnCount]; - return string.Join ("\n", lines); - } - set + for (var i = 0; i < _data.ColumnCount; i++) { - // Guard: View base constructor calls Text setter before MarkdownTable() initializes fields. - - if (string.IsNullOrWhiteSpace (value)) - { - TableData = _emptyData; + seps [i] = _data.ColumnAlignments [i] switch + { + Alignment.Center => ":---:", + Alignment.End => "---:", + _ => "---" + }; + } - return; - } + lines.Add ($"| {string.Join (" | ", seps)} |"); - string [] lines = value.Split ('\n', StringSplitOptions.RemoveEmptyEntries); - TableData? parsed = TableData.TryParse (lines); - TableData = parsed ?? _emptyData; + foreach (string [] row in _data.Rows) + { + lines.Add ($"| {string.Join (" | ", row)} |"); } + + return string.Join ("\n", lines); } /// @@ -155,6 +161,9 @@ public TableData TableData _rowSegments [r] = ParseCellSegments (value.Rows [r], MarkdownStyleRole.Normal); } + // Keep Text in sync with TableData + SetTextDirect (BuildTableText ()); + // Compute initial layout using current Frame width (or a default for standalone use) int initialWidth = Frame.Width > 0 ? Frame.Width : 80; _lastComputedWidth = -1; diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 8c10d3e5f9..29e4366ce8 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -1,7 +1,5 @@ using Terminal.Gui.Tracing; -#pragma warning disable CS0618 // Obsolete - Menu uses [ConfigurationProperty] for DefaultBorderStyle during transition - namespace Terminal.Gui.Views; /// @@ -52,12 +50,7 @@ public class Menu : Bar, IValue /// /// Gets or sets the default Border Style for Menus. The default is . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => MenuSettings.Defaults.DefaultBorderStyle; - set => MenuSettings.Defaults.DefaultBorderStyle = value; - } + public static LineStyle DefaultBorderStyle => MenuSettings.Current.DefaultBorderStyle; /// public Menu () : this ([]) { } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 905f0d5262..4aa7474319 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1,8 +1,6 @@ using System.ComponentModel; using Terminal.Gui.Tracing; -#pragma warning disable CS0618 // Obsolete - MenuBar uses [ConfigurationProperty] for DefaultBorderStyle/DefaultKey during transition - namespace Terminal.Gui.Views; /// @@ -440,21 +438,25 @@ internal set /// /// Gets or sets the default Border Style for the MenuBar. The default is . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static LineStyle DefaultBorderStyle - { - get => MenuBarSettings.Defaults.DefaultBorderStyle; - set => MenuBarSettings.Defaults.DefaultBorderStyle = value; - } + public new static LineStyle DefaultBorderStyle => MenuBarSettings.Current.DefaultBorderStyle; + + private static Key? _defaultKeyOverride; /// The default key for activating menu bars. - [ConfigurationProperty (Scope = typeof (SettingsScope))] + /// + /// Setting this overrides the configured value; the override survives theme switches and + /// configuration re-application ( is wholesale-replaced + /// on each apply, so a value written there would silently revert). + /// public static Key DefaultKey { - get => MenuBarSettings.Defaults.DefaultKey; - set => MenuBarSettings.Defaults.DefaultKey = value; + get => _defaultKeyOverride ?? MenuBarSettings.Current.DefaultKey; + set => _defaultKeyOverride = value; } + /// INTERNAL: Clears the app-set override (for tests). + internal static void ResetDefaultKeyOverride () => _defaultKeyOverride = null; + /// public override void EndInit () { @@ -786,7 +788,7 @@ protected override void OnSuperViewChanged (ValueChangedEventArgs e) { if (SuperView is null) { - // BUGBUG: This is a hack for avoiding a race condition in ConfigurationManager.Apply + // BUGBUG: This is a hack for avoiding a race condition when theme overlays re-apply. // BUGBUG: For some reason in some unit tests, when Top is disposed, MenuBar.Dispose does not get called. // BUGBUG: Yet, the MenuBar does get Removed from Top (and it's SuperView set to null). // BUGBUG: Related: https://github.com/tui-cs/Terminal.Gui/issues/4021 diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index c8b5b0da45..3746652d5a 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -286,19 +286,26 @@ public Key Key /// public event EventHandler? KeyChanged; + private static Key? _defaultKeyOverride; + /// /// Gets or sets the default key for activating popover menus. The default value is with Shift. /// /// /// This is a configuration property that affects all new instances. + /// Setting it overrides the configured value; the override survives theme switches and + /// configuration re-application ( is wholesale-replaced + /// on each apply, so a value written there would silently revert). /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key DefaultKey { - get => PopoverMenuSettings.Defaults.DefaultKey; - set => PopoverMenuSettings.Defaults.DefaultKey = value; + get => _defaultKeyOverride ?? PopoverMenuSettings.Current.DefaultKey; + set => _defaultKeyOverride = value; } + /// INTERNAL: Clears the app-set override (for tests). + internal static void ResetDefaultKeyOverride () => _defaultKeyOverride = null; + /// /// The mouse flags that will cause the popover menu to be visible. The default is /// which is typically the right mouse button. diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 56fb3f873c..a198de1055 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -55,23 +55,13 @@ public static class MessageBox { /// /// Defines the default border styling for . Can be configured via - /// . + /// . /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => MessageBoxSettings.Defaults.DefaultBorderStyle; - set => MessageBoxSettings.Defaults.DefaultBorderStyle = value; - } + public static LineStyle DefaultBorderStyle => MessageBoxSettings.Current.DefaultBorderStyle; /// The default for . /// This property can be set in a Theme. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment - { - get => MessageBoxSettings.Defaults.DefaultButtonAlignment; - set => MessageBoxSettings.Defaults.DefaultButtonAlignment = value; - } + public static Alignment DefaultButtonAlignment => MessageBoxSettings.Current.DefaultButtonAlignment; /// /// Displays an auto-sized error . diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index 8b6151fdbf..30ab935a42 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Numerics; namespace Terminal.Gui.Views; @@ -44,11 +45,14 @@ public class NumericUpDown : View, IValue where T : notnull public new static Dictionary? DefaultKeyBindings { get; set; } = new (); private readonly ScrollButton _down; - - // TODO: Use a TextField instead of a Label - private readonly View _number; private readonly ScrollButton _up; + private View _number; + private TextField? _editor; + private bool _updatingText; + private bool _editingFromUser; + private bool _canEdit; + /// /// Initializes a new instance of the class. /// @@ -64,16 +68,6 @@ public NumericUpDown () CanFocus = true; - // `object` is supported only for AllViewsTester - if (type != typeof (object)) - { - if (NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper)) - { - Increment = (T)helper!.One; - Value = (T)helper.Zero; - } - } - Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); @@ -83,16 +77,7 @@ public NumericUpDown () Direction = NavigationDirection.Forward }; - _number = new View - { - Text = Value?.ToString () ?? "Err", - X = Pos.Right (_down), - Y = Pos.Top (_down), - Width = Dim.Auto (minimumContentDim: Dim.Func (_ => string.Format (Format, Value).GetColumns ())), - Height = 1, - TextAlignment = Alignment.Center, - CanFocus = true - }; + _number = CreateNumberView (); _up = new ScrollButton { @@ -107,6 +92,16 @@ public NumericUpDown () Add (_down, _number, _up); + // `object` is supported only for AllViewsTester + if (type != typeof (object)) + { + if (NumericHelper.TryGetHelper (typeof (T), out INumericHelper? helper)) + { + Increment = (T)helper!.One; + Value = (T)helper.Zero; + } + } + AddCommand (Command.Up, _ => { @@ -163,6 +158,24 @@ void OnUpButtonOnAccept (object? s, CommandEventArgs e) } } + /// + /// Gets or sets whether the value area uses an editable text field instead of the compact display. + /// + public bool CanEdit + { + get => _canEdit; + set + { + if (_canEdit == value) + { + return; + } + + _canEdit = value; + UpdateNumberView (); + } + } + private T? _value; /// @@ -253,10 +266,169 @@ public string Format /// public event EventHandler>? FormatChanged; + private View CreateNumberView () + { + View view = new () + { + Text = GetDisplayText (), + X = Pos.Right (_down), + Y = Pos.Top (_down), + Width = Dim.Auto (minimumContentDim: Dim.Func (_ => GetNumberWidth ())), + Height = 1, + TextAlignment = Alignment.Center, + CanFocus = true + }; + + return view; + } + + private TextField CreateEditorView () + { + TextField editor = new () + { + Text = GetDisplayText (), + X = Pos.Right (_down), + Y = Pos.Top (_down), + Width = Dim.Auto (minimumContentDim: Dim.Func (_ => GetNumberWidth ())), + Height = 1, + TextAlignment = Alignment.Center, + CanFocus = true + }; + + editor.ValueChanged += EditorValueChanged; + + return editor; + } + + private void UpdateNumberView () + { + View oldNumber = _number; + View newNumber = CanEdit ? CreateEditorView () : CreateNumberView (); + + if (oldNumber != newNumber) + { + Remove (oldNumber); + oldNumber.Dispose (); + } + + _editor = newNumber as TextField; + _number = newNumber; + _number.X = Pos.Right (_down); + _number.Y = Pos.Top (_down); + _number.Width = Dim.Auto (minimumContentDim: Dim.Func (_ => GetNumberWidth ())); + _number.Height = 1; + _number.TextAlignment = Alignment.Center; + _number.CanFocus = true; + + _up.X = Pos.Right (_number); + _up.Y = Pos.Top (_number); + + // Insert the value view between the down and up buttons so Tab/focus order matches the + // visual left-to-right layout (down, value, up). Add () appends after _up, so move it back one. + Add (_number); + MoveSubViewTowardsStart (_number); + + SetNeedsLayout (); + SetText (); + } + + private void EditorValueChanged (object? sender, ValueChangedEventArgs e) + { + if (_updatingText || !CanEdit || _editor is null) + { + return; + } + + Text = _editor.Text; + + if (!TryParse (_editor.Text, out T? parsedValue)) + { + return; + } + + // Apply the parsed value but leave the user's in-progress text untouched (see SetText). Reformatting + // on every keystroke would wipe out partial input such as a trailing decimal point. + _editingFromUser = true; + + try + { + Value = parsedValue; + } + finally + { + _editingFromUser = false; + } + } + + private bool TryParse (string? text, out T? result) + { + if (string.IsNullOrWhiteSpace (text)) + { + result = default; + + return false; + } + + try + { + object? convertedValue = Convert.ChangeType (text, typeof (T), CultureInfo.CurrentCulture); + result = (T?)convertedValue; + + return true; + } + catch + { + result = default; + + return false; + } + } + + private int GetNumberWidth () + { + int width = GetDisplayText ().GetColumns (); + + if (CanEdit) + { + width++; + } + + return Math.Max (width, 1); + } + + private string GetDisplayText () => string.Format (Format, _value); + private void SetText () { - _number.Text = string.Format (Format, _value); - Text = _number.Text; + string displayText = GetDisplayText (); + + // When the value change was initiated by the user typing, don't overwrite the editor text; doing so + // would reformat and reset the caret mid-entry. Text already reflects the user's raw input (set in + // EditorValueChanged). The editor resyncs on Up/Down or external Value changes. + if (_editingFromUser) + { + return; + } + + if (_number is TextField textField) + { + _updatingText = true; + + try + { + textField.Text = displayText; + } + finally + { + _updatingText = false; + } + } + else + { + _number.Text = displayText; + } + + Text = displayText; } /// diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs index 570b269abe..62463e392e 100644 --- a/Terminal.Gui/Views/ProgressBar.cs +++ b/Terminal.Gui/Views/ProgressBar.cs @@ -108,6 +108,7 @@ public ProgressBar () Height = Dim.Auto (DimAutoStyle.Content, 1); CanFocus = false; _fraction = 0; + SetTextDirect ("0%"); } /// @@ -125,6 +126,7 @@ public float Fraction { _fraction = Math.Min (value, 1); _isActivity = false; + SetTextDirect ($"{_fraction * 100:F0}%"); UpdateTerminalProgress (); SetNeedsDraw (); } @@ -216,19 +218,21 @@ public ProgressBarStyle ProgressBarStyle /// is the percentage will be /// displayed. If is a marquee style, the text will be displayed. /// - public override string Text + protected override bool OnTextChanging (string newText) { - get => string.IsNullOrEmpty (base.Text) ? $"{_fraction * 100:F0}%" : base.Text; - set + // Only allow text to be set externally in marquee mode + if (ProgressBarStyle is not (ProgressBarStyle.MarqueeBlocks or ProgressBarStyle.MarqueeContinuous or ProgressBarStyle.Fire)) { - if (ProgressBarStyle is ProgressBarStyle.MarqueeBlocks or ProgressBarStyle.MarqueeContinuous or ProgressBarStyle.Fire) - { - base.Text = value; - } + return true; } + + return base.OnTextChanging (newText); } - /// + /// + protected override bool OnDrawingText (DrawContext? context) => true; + + /// protected override bool OnDrawingContent (DrawContext? context) { SetAttribute (GetAttributeForRole (VisualRole.Normal)); diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index e962e9a38e..3a8a0ebeeb 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -25,12 +25,7 @@ public abstract class SelectorBase : View, IOrientation, IValue /// /// Gets or sets the default Highlight Style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static MouseState DefaultMouseHighlightStates - { - get => SelectorBaseSettings.Defaults.DefaultMouseHighlightStates; - set => SelectorBaseSettings.Defaults.DefaultMouseHighlightStates = value; - } + public static MouseState DefaultMouseHighlightStates => SelectorBaseSettings.Current.DefaultMouseHighlightStates; /// /// Initializes a new instance of the class. diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 3846cc7051..3812756be4 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -140,6 +140,7 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText HelpView.Id = "_helpView"; #endif HelpView.Text = helpText ?? string.Empty; + SetTextDirect (helpText ?? string.Empty); HelpView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; #if DEBUG @@ -676,18 +677,19 @@ private void SetHelpViewDefaultLayout () HelpView.MouseHighlightStates = MouseState.None; } + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// /// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to /// . /// - public override string Text + protected override void OnTextChanged () { - get => HelpView.Text; - set - { - HelpView.Text = value; - ShowHide (); - } + HelpView.Text = Text; + ShowHide (); + + base.OnTextChanged (); } /// @@ -699,6 +701,7 @@ public string HelpText set { HelpView.Text = value; + SetTextDirect (value); ShowHide (); } } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 110a0b56c0..a480e52adf 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS0618 // Obsolete - StatusBar uses [ConfigurationProperty] for DefaultSeparatorLineStyle during transition - namespace Terminal.Gui.Views; /// @@ -41,7 +39,7 @@ protected override void OnSuperViewChanged (ValueChangedEventArgs e) { if (SuperView is null) { - // BUGBUG: This is a hack for avoiding a race condition in ConfigurationManager.Apply + // BUGBUG: This is a hack for avoiding a race condition when theme overlays re-apply. // BUGBUG: For some reason in some unit tests, when Top is disposed, MenuBar.Dispose does not get called. // BUGBUG: Yet, the MenuBar does get Removed from Top (and it's SuperView set to null). // BUGBUG: Related: https://github.com/tui-cs/Terminal.Gui/issues/4021 @@ -59,12 +57,7 @@ private void OnThemeChanged (object? sender, App.EventArgs e) /// /// Gets or sets the default Line Style for the separators between the shortcuts of the StatusBar. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultSeparatorLineStyle - { - get => StatusBarSettings.Defaults.DefaultSeparatorLineStyle; - set => StatusBarSettings.Defaults.DefaultSeparatorLineStyle = value; - } + public static LineStyle DefaultSeparatorLineStyle => StatusBarSettings.Current.DefaultSeparatorLineStyle; /// protected override void OnSubViewLayout (LayoutEventArgs args) diff --git a/Terminal.Gui/Views/TableView/TableView.Content.cs b/Terminal.Gui/Views/TableView/TableView.Content.cs index dec5623ea6..9ecf4c9736 100644 --- a/Terminal.Gui/Views/TableView/TableView.Content.cs +++ b/Terminal.Gui/Views/TableView/TableView.Content.cs @@ -183,25 +183,6 @@ public void EnsureValidScrollOffsets () int lastColIdx = nonHiddenColumns.Any () ? nonHiddenColumns.Last ().colIdx : -1; - // Precompute per-column minimum widths and a suffix sum so that "space reserved for remaining - // columns" is O(1) per column during reservation/min-width bookkeeping. Later width calculations - // may still inspect row data. - int columnCount = nonHiddenColumns.Count; - int [] minWidths = new int [columnCount]; - int [] reservedFromIndex = new int [columnCount + 1]; - - for (var i = 0; i < columnCount; i++) - { - (int colIdx, ColumnStyle? colStyle) = nonHiddenColumns [i]; - minWidths [i] = MinimumWidthFor (colIdx, colStyle); - } - - for (int i = columnCount - 1; i >= 0; i--) - { - int separator = i < columnCount - 1 ? 1 : 0; - reservedFromIndex [i] = minWidths [i] + separator + reservedFromIndex [i + 1]; - } - contentSize.Width += leftOuterBorderWidth; var startRow = 0; @@ -215,8 +196,6 @@ public void EnsureValidScrollOffsets () } // Calculate the content size based on the table's data - var columnIndex = 0; - foreach ((int colIdx, ColumnStyle? colStyle) in nonHiddenColumns) { int maxContentSize = CalculateMaxCellWidth (colIdx, colStyle, startRow, rowsToRender) + padding; @@ -238,7 +217,8 @@ public void EnsureValidScrollOffsets () if (isVeryLast) { - //remaining space for last column + // Remaining space for the last column is only used when the table would otherwise be smaller + // than the viewport; otherwise content size should remain based on the actual column widths. int remainingSpace = Viewport.Width - contentSize.Width - rightOuterBorderWidth; if (Style.ExpandLastColumn && colWidth < remainingSpace) @@ -246,21 +226,6 @@ public void EnsureValidScrollOffsets () colWidth = remainingSpace; } } - else if (Viewport.Width > 0) - { - // Reserve at least the header width for each subsequent visible column so that a wide - // column does not consume all viewport space and push later columns off-screen. - int reservedForRemaining = reservedFromIndex [columnIndex + 1]; - int availableForThisCol = Viewport.Width - contentSize.Width - reservedForRemaining - rightOuterBorderWidth - 1; // -1 for this column's separator - - // Don't shrink below this column's own minimum (header width or configured minimum) - int thisColMin = minWidths [columnIndex]; - - if (colWidth > availableForThisCol && availableForThisCol >= thisColMin) - { - colWidth = availableForThisCol; - } - } columnsToRender.Add (new ColumnToRender (colIdx, contentSize.Width, colWidth + 1, lastColIdx == colIdx)); @@ -271,8 +236,6 @@ public void EnsureValidScrollOffsets () // for separator symbols between columns contentSize.Width += 1; } - - columnIndex++; } contentSize.Width += rightOuterBorderWidth; diff --git a/Terminal.Gui/Views/TextInput/DateEditor.cs b/Terminal.Gui/Views/TextInput/DateEditor.cs index 4391de81c2..6e339d1122 100644 --- a/Terminal.Gui/Views/TextInput/DateEditor.cs +++ b/Terminal.Gui/Views/TextInput/DateEditor.cs @@ -161,7 +161,7 @@ bool IValue.TrySetValueFromString (string input) /// /// Synchronizes the backing field when the base class - /// property changes programmatically. + /// property changes programmatically. /// protected override void OnValueChanged (ValueChangedEventArgs args) => _value = DateProvider.DateValue; diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Drawing.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Drawing.cs index bc0dc88e6d..6757622ffc 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.Drawing.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Drawing.cs @@ -2,6 +2,9 @@ namespace Terminal.Gui.Views; public partial class TextField { + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute) { diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs index b6fce8850d..fa22cf78be 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.Text.cs @@ -9,7 +9,7 @@ public partial class TextField private string? _lastPastedText; /// Raised before changes. The change can be canceled the text adjusted. - public event EventHandler>? TextChanging; + public new event EventHandler>? TextChanging; /// /// Tracks whether the text field should be considered "used", that is, that the user has moved in the entry, so @@ -296,18 +296,6 @@ private static bool IsWithinPasteRange (PasteEditOperation operation, int propos return proposedIndex >= pasteStart && proposedIndex < pasteEnd; } - /// Raises the event, enabling canceling the change or adjusting the text. - /// The event arguments. - /// if the event was cancelled or the text was adjusted by the event. - public bool RaiseTextChanging (ResultEventArgs args) - { - // TODO: CWP: Add an OnTextChanging protected virtual method that can be overridden to handle text changing events. - - TextChanging?.Invoke (this, args); - - return args.Handled; - } - private List DeleteSelectedText () { SetSelectedStartSelectedLength (); @@ -390,77 +378,120 @@ private void InsertText (Key a, bool usePreTextChangedCursorPos) /// private List _text; + /// + /// Stashes the final text value determined during (after sanitization + /// and possible subscriber modification) for use in . + /// + private string? _pendingText; + private void SetText (List newText) => Text = StringExtensions.ToString (newText); private void SetText (IEnumerable newText) => SetText (newText.ToList ()); - /// Sets or gets the text held by the view. - public override string Text + /// + /// + /// + /// TextField overrides this to sanitize text (strip tabs and newlines), fire + /// , and raise the TextField-specific + /// event (which allows subscribers to modify the text). + /// + /// + protected override bool OnTextChanging (string newText) { - get => StringExtensions.ToString (_text); - set + // Guard against base constructor calling before _text is initialized + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (_text is null) { - // Guard against base constructor calling before _text is initialized - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_text is null) - { - return; - } + return true; + } - var oldText = StringExtensions.ToString (_text); + // Sanitize: single-line, no tabs + string sanitized = newText.Replace ("\t", "").Split ("\n") [0]; - if (oldText == value) - { - return; - } + var oldText = StringExtensions.ToString (_text); - string newText = value.Replace ("\t", "").Split ("\n") [0]; + // After sanitization, check if there is an effective change + if (oldText == sanitized) + { + return true; + } - // Raise IValue.ValueChanging - if (RaiseValueChanging (oldText, newText)) + // Raise IValue.ValueChanging + if (RaiseValueChanging (oldText, sanitized)) + { + return true; + } + + ResultEventArgs args = new (sanitized); + TextChanging?.Invoke (this, args); + + if (args.Handled) + { + if (InsertionPoint > _text.Count) { - return; + InsertionPoint = _text.Count; } - ResultEventArgs args = new (newText); - RaiseTextChanging (args); + return true; + } - if (args.Handled) - { - if (InsertionPoint > _text.Count) - { - InsertionPoint = _text.Count; - } + // Stash the (possibly subscriber-modified) result for OnTextChanged + _pendingText = args.Result; - return; - } + if (base.OnTextChanging (newText)) + { + _pendingText = null; - ClearAllSelection (); + return true; + } - // Note we use NewValue here; TextChanging subscribers may have changed it - _text = args.Result!.ToStringList (); + return false; + } - if (!Secret && !_historyText.IsFromHistory) - { - _historyText.Add ([Cell.ToCellList (oldText)], new Point (InsertionPoint, 0)); + /// + /// + /// + /// Syncs the internal grapheme-list editing model from the base value, + /// applying any sanitization or subscriber modifications stashed during . + /// + /// + protected override void OnTextChanged () + { + // Use stashed text from OnTextChanging if available; otherwise sanitize base.Text + string finalText = _pendingText ?? Text.Replace ("\t", "").Split ("\n") [0]; + _pendingText = null; - _historyText.Add ([Cell.ToCells (_text)], new Point (InsertionPoint, 0), TextEditingLineStatus.Replaced); - } + var oldText = StringExtensions.ToString (_text); + + ClearAllSelection (); + _text = finalText.ToStringList (); - OnTextChanged (); + // Ensure base View._text holds the sanitized/modified value + if (Text != finalText) + { + SetTextDirect (finalText); + } - // Raise IValue.ValueChanged - RaiseValueChanged (oldText, StringExtensions.ToString (_text)); + if (!Secret && !_historyText.IsFromHistory) + { + _historyText.Add ([Cell.ToCellList (oldText)], new Point (InsertionPoint, 0)); - ProcessAutocomplete (); + _historyText.Add ([Cell.ToCells (_text)], new Point (InsertionPoint, 0), TextEditingLineStatus.Replaced); + } - if (InsertionPoint > _text.Count) - { - InsertionPoint = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0); - } + base.OnTextChanged (); - Adjust (); - SetNeedsDraw (); + // Raise IValue.ValueChanged + RaiseValueChanged (oldText, StringExtensions.ToString (_text)); + + ProcessAutocomplete (); + + if (InsertionPoint > _text.Count) + { + InsertionPoint = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0); } + + Adjust (); + SetNeedsDraw (); } /// diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.cs index a4212617e7..7203e92603 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.cs @@ -79,12 +79,7 @@ public partial class TextField : View, IDesignable, IValue /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => TextFieldSettings.Defaults.DefaultCursorStyle; - set => TextFieldSettings.Defaults.DefaultCursorStyle = value; - } + public static CursorStyle DefaultCursorStyle => TextFieldSettings.Current.DefaultCursorStyle; /// /// Initializes a new instance of the class. @@ -98,7 +93,7 @@ public TextField () ReadOnly = false; Autocomplete = new TextFieldAutocomplete (); - Height = Dim.Auto (DimAutoStyle.Text, 1); + Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1, maximumContentDim: 1); CanFocus = true; Used = true; diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs index 4ea9f9f723..e02bfc1429 100644 --- a/Terminal.Gui/Views/TextInput/TextValidateField.cs +++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs @@ -57,7 +57,7 @@ public class TextValidateField : View, IDesignable, IValue /// public TextValidateField () { - Height = Dim.Auto (minimumContentDim: 1); + Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1, maximumContentDim: 1); CanFocus = true; // Things this view knows how to do @@ -153,6 +153,7 @@ public ITextValidateProvider? Provider { _provider.TextChanged += ProviderOnTextChanged; _lastKnownText = _provider.Text; + SetTextDirect (_provider.Text); } if (_provider!.Fixed) @@ -203,57 +204,93 @@ protected virtual void OnValueChanged (ValueChangedEventArgs args) { } #endregion - /// Text - public override string Text + /// + /// Stashes the final text value determined during (after possible + /// subscriber modification) for use in . + /// + private string? _pendingValidatedText; + + /// + protected override bool OnTextChanging (string newText) { - get => _provider is null ? string.Empty : _provider.Text; - set + if (_provider is null || SuppressValueEvents) { - if (_provider is null) - { - return; - } + return base.OnTextChanging (newText); + } - string oldValue = _provider.Text; + string oldValue = _provider.Text; - if (oldValue == value) - { - return; - } + if (oldValue == newText) + { + return true; + } - if (!SuppressValueEvents) - { - ValueChangingEventArgs args = new (oldValue, value); + ValueChangingEventArgs args = new (oldValue, newText); - if (OnValueChanging (args) || args.Handled) - { - return; - } + if (OnValueChanging (args) || args.Handled) + { + return true; + } - ValueChanging?.Invoke (this, args); + ValueChanging?.Invoke (this, args); - if (args.Handled) - { - return; - } + if (args.Handled) + { + return true; + } - // Allow subscribers to modify the new value - value = args.NewValue ?? string.Empty; - } + // Allow subscribers to modify the new value + _pendingValidatedText = args.NewValue ?? string.Empty; - _lastKnownText = value; - _provider.Text = value; + return base.OnTextChanging (newText); + } - if (!SuppressValueEvents) - { - ValueChangedEventArgs changedArgs = new (oldValue, value); - OnValueChanged (changedArgs); - ValueChanged?.Invoke (this, changedArgs); - ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (oldValue, value)); - } + /// Text + protected override void OnTextChanged () + { + string value = _pendingValidatedText ?? Text; + _pendingValidatedText = null; + + if (_provider is null) + { + base.OnTextChanged (); + + return; + } + + string oldValue = _provider.Text; + + if (oldValue == value) + { + base.OnTextChanged (); + + return; + } + + // Apply subscriber-modified value if different from what the base stored + if (value != Text) + { + SetTextDirect (value); + } + + _lastKnownText = value; + _provider.Text = value; + + // The provider may transform the text (e.g., mask formatting). + // Keep base _text in sync with the provider's actual text. + SetTextDirect (_provider.Text); - SetNeedsDraw (); + if (!SuppressValueEvents) + { + ValueChangedEventArgs changedArgs = new (oldValue, _provider.Text); + OnValueChanged (changedArgs); + ValueChanged?.Invoke (this, changedArgs); + ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (oldValue, _provider.Text)); } + + SetNeedsDraw (); + + base.OnTextChanged (); } private int _insertionPoint; @@ -325,6 +362,9 @@ private void ProviderOnTextChanged (object? sender, EventArgs e) // Sync _lastKnownText with actual provider state (may have been reverted by handler) _lastKnownText = _provider.Text; + + // Keep base Text in sync with provider + SetTextDirect (_provider.Text); } private void RevertProviderText (string oldText) @@ -357,6 +397,9 @@ protected override bool OnMouseEvent (Mouse mouse) return true; } + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnDrawingContent (DrawContext? context) { @@ -368,7 +411,7 @@ protected override bool OnDrawingContent (DrawContext? context) return true; } - var role = VisualRole.Editable; + VisualRole role = VisualRole.Editable; Attribute textColor = IsValid ? GetAttributeForRole (role) : SchemeManager.GetScheme (Schemes.Error).GetAttributeForRole (role); (int marginLeft, int marginRight) = GetMargins (Viewport.Width); @@ -405,7 +448,7 @@ protected override bool OnDrawingContent (DrawContext? context) return true; } - SetAttributeForRole (VisualRole.Focus); + SetAttribute (textColor with { Style = textColor.Style | TextStyle.Underline }); Move (InsertionPoint + marginLeft, 0); AddRune ((Rune)_provider.DisplayText [InsertionPoint]); diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs index a9fbc687b5..3b5db1266c 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs @@ -11,6 +11,9 @@ public partial class TextView private bool _isDrawing; + /// + protected override bool OnDrawingText (DrawContext? context) => true; + /// protected override bool OnDrawingContent (DrawContext? context) { @@ -494,4 +497,3 @@ internal void ApplyCellsAttribute (Attribute attribute) return GetAttributeForRole (VisualRole.Active); } } - diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs index d27e6b4b0a..2f8f6d70ef 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs @@ -165,7 +165,7 @@ public int TabWidth /// The event is fired whenever this property is set. Note, however, that Text is not /// set by as the user types. /// - public override string Text + public new string Text { get { @@ -178,6 +178,12 @@ public override string Text } set { + // Raise View.TextChanging so subscribers holding a View reference can cancel. + if (OnTextChanging (value)) + { + return; + } + ResetPosition (); _model.LoadString (value); @@ -188,13 +194,63 @@ public override string Text _lastWrapWidth = Viewport.Width; } - OnTextChanged (); + // Keep base View._text in sync + SetTextDirect (value); SetNeedsDraw (); - _historyText.Clear (_model.GetAllLines ()); + + _skipNextTextChangedSync = true; + + try + { + RaiseTextChanged (); + } + finally + { + _skipNextTextChangedSync = false; + } } } + /// Tracks whether the next call should skip redundant model synchronization. + private bool _skipNextTextChangedSync; + + /// + /// + /// Syncs the internal when is set through a + /// polymorphic () reference, ensuring the TextView's editing model stays consistent. + /// + protected override void OnTextChanged () + { + // Skip sync when called from our own `new Text` setter — it already updated the model. + if (_skipNextTextChangedSync) + { + // Consume before TextChanged subscribers run so reentrant polymorphic sets synchronize normally. + _skipNextTextChangedSync = false; + base.OnTextChanged (); + + return; + } + + string baseText = base.Text; + + // Sync when the internal model diverges (polymorphic setter case). + ResetPosition (); + _model.LoadString (baseText); + + if (_wordWrap) + { + _wrapManager = new WordWrapManager (_model); + _model = _wrapManager.WrapModel (Viewport.Width, out _, out _, out _, out _); + _lastWrapWidth = Viewport.Width; + } + + _historyText.Clear (_model.GetAllLines ()); + SetNeedsDraw (); + + base.OnTextChanged (); + } + /// /// Tracks whether the text view should be considered "used", that is, that the user has moved in the entry, so /// new input should be appended at the cursor position, rather than clearing the entry diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs index e3b94cab8d..f351a90c59 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs @@ -104,12 +104,7 @@ public partial class TextView : View, IDesignable /// /// Gets or sets the default cursor style. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static CursorStyle DefaultCursorStyle - { - get => TextViewSettings.Defaults.DefaultCursorStyle; - set => TextViewSettings.Defaults.DefaultCursorStyle = value; - } + public static CursorStyle DefaultCursorStyle => TextViewSettings.Current.DefaultCursorStyle; private CultureInfo? _currentCulture; @@ -132,6 +127,10 @@ public TextView () _model.LinesLoaded += Model_LinesLoaded!; _historyText.ChangeText += HistoryText_ChangeText; + // Initialize the model with at least one empty line + _model.LoadString (string.Empty); + _historyText.Clear (_model.GetAllLines ()); + CreateCommandsAndBindings (); _currentCulture = Thread.CurrentThread.CurrentUICulture; diff --git a/Terminal.Gui/Views/TextInput/TimeEditor.cs b/Terminal.Gui/Views/TextInput/TimeEditor.cs index b06c8a6271..512d3c5622 100644 --- a/Terminal.Gui/Views/TextInput/TimeEditor.cs +++ b/Terminal.Gui/Views/TextInput/TimeEditor.cs @@ -177,7 +177,7 @@ bool IValue.TrySetValueFromString (string input) /// /// Synchronizes the backing field when the base class - /// property changes programmatically. + /// property changes programmatically. /// protected override void OnValueChanged (ValueChangedEventArgs args) => _value = TimeProvider.TimeValue; diff --git a/Terminal.Gui/Views/TreeView/TreeViewT.cs b/Terminal.Gui/Views/TreeView/TreeViewT.cs index c9ed5fcd48..bca93de5a8 100644 --- a/Terminal.Gui/Views/TreeView/TreeViewT.cs +++ b/Terminal.Gui/Views/TreeView/TreeViewT.cs @@ -266,7 +266,7 @@ public TreeView () /// /// /// - /// This property is not decorated with because + /// This property is not a configuration-bound property because /// is a generic type. Use to /// override key bindings for TreeView via configuration. /// diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index a93b1a3301..2b7ff61bf3 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -33,12 +33,7 @@ public Window () /// /// Gets or sets whether all s are shown with a shadow effect by default. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static ShadowStyles DefaultShadow - { - get => WindowSettings.Defaults.DefaultShadow; - set => WindowSettings.Defaults.DefaultShadow = value; - } + public static ShadowStyles DefaultShadow => WindowSettings.Current.DefaultShadow; // TODO: enable this ///// @@ -47,8 +42,7 @@ public static ShadowStyles DefaultShadow ///// ///// This property can be set in a Theme to change the default for all s. ///// - /////[ConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))] - ////public static Scheme DefaultScheme { get; set; } = Colors.Schemes ["Base"]; + /// /// The default for 's border. The default is @@ -58,10 +52,5 @@ public static ShadowStyles DefaultShadow /// This property can be set in a Theme to change the default for all /// s. /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static LineStyle DefaultBorderStyle - { - get => WindowSettings.Defaults.DefaultBorderStyle; - set => WindowSettings.Defaults.DefaultBorderStyle = value; - } + public static LineStyle DefaultBorderStyle => WindowSettings.Current.DefaultBorderStyle; } diff --git a/Tests/AppTestHelpers/AppTestHelper.cs b/Tests/AppTestHelpers/AppTestHelper.cs index d68aaf8268..51ac16e784 100644 --- a/Tests/AppTestHelpers/AppTestHelper.cs +++ b/Tests/AppTestHelpers/AppTestHelper.cs @@ -181,7 +181,7 @@ internal AppTestHelper (Func runnableBuilder, int width, int height, catch (Exception ex) { _backgroundException = ex; - _ansiInput.ExternalCancellationTokenSource!.Cancel (); + _ansiInput.ExternalCancellationTokenSource?.Cancel (); } finally { @@ -314,8 +314,15 @@ public AppTestHelper Then (Action doAction) /// public AppTestHelper WaitIteration (Action? action = null) { - // If application has already exited don't wait! - if (Finished || _runCancellationTokenSource.Token.IsCancellationRequested || _ansiInput.ExternalCancellationTokenSource!.Token.IsCancellationRequested) + CancellationTokenSource? externalCts = _ansiInput.ExternalCancellationTokenSource; + + // If application has already exited don't wait. + // ExternalCancellationTokenSource can already be null during shutdown: + // CleanupApplication nulls it, and Stop/Dispose still call WaitIteration. + if (Finished + || _runCancellationTokenSource.Token.IsCancellationRequested + || externalCts is null + || externalCts.Token.IsCancellationRequested) { Logging.Warning ("WaitIteration called after context was stopped"); @@ -359,7 +366,7 @@ public AppTestHelper WaitIteration (Action? action = null) [ _runCancellationTokenSource.Token.WaitHandle, ctsActionCompleted.Token.WaitHandle, - _ansiInput.ExternalCancellationTokenSource!.Token.WaitHandle + externalCts.Token.WaitHandle ]; WaitHandle.WaitAny (waitHandles); @@ -565,15 +572,27 @@ internal void Fail (string reason) throw new Exception (reason); } + /// + /// Test hook: nulls the external cancellation source without marking . + /// Matches the shutdown race where clears the source + /// while is still running. + /// + internal void ClearExternalCancellationTokenSourceForTests () + { + _ansiInput.ExternalCancellationTokenSource = null; + } + private void CleanupApplication () { Logging.Trace ("CleanupApplication"); + + // Mark finished first so WaitIteration can return without touching the CTS. + Finished = true; _ansiInput.ExternalCancellationTokenSource = null; App?.ResetState (true); _loggerScope?.Dispose (); _loggerScope = null; - Finished = true; Application.MaximumIterationsPerSecond = Application.DefaultMaximumIterationsPerSecond; } diff --git a/Tests/AppTestHelpers/AppTestHelpers.csproj b/Tests/AppTestHelpers/AppTestHelpers.csproj index 6fdfdbee42..be39a6d83a 100644 --- a/Tests/AppTestHelpers/AppTestHelpers.csproj +++ b/Tests/AppTestHelpers/AppTestHelpers.csproj @@ -15,4 +15,8 @@ - \ No newline at end of file + + + + + diff --git a/Tests/AppTestHelpers/AssemblyInfo.cs b/Tests/AppTestHelpers/AssemblyInfo.cs index 4e9b2b2db2..b710715e0c 100644 --- a/Tests/AppTestHelpers/AssemblyInfo.cs +++ b/Tests/AppTestHelpers/AssemblyInfo.cs @@ -1,6 +1,5 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Drivers; global using Terminal.Gui.Input; diff --git a/Tests/Benchmarks/Benchmarks.csproj b/Tests/Benchmarks/Benchmarks.csproj index 7cc060bef1..90c297aa25 100644 --- a/Tests/Benchmarks/Benchmarks.csproj +++ b/Tests/Benchmarks/Benchmarks.csproj @@ -9,6 +9,8 @@ + + diff --git a/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs b/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs deleted file mode 100644 index c64f9a7d25..0000000000 --- a/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs +++ /dev/null @@ -1,41 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Terminal.Gui.Configuration; - -namespace Terminal.Gui.Benchmarks.Configuration; - -/// -/// Measures the cold-start cost of loading the embedded library configuration: -/// ConfigurationManager.Disable (true) → Enable (ConfigLocations.LibraryResources) → Apply (). -/// This is the app-startup hot path. -/// -/// -/// -/// Run: -/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*ConfigurationManagerLoad*' -/// -/// -[MemoryDiagnoser] -[BenchmarkCategory ("Configuration")] -public class ConfigurationManagerLoadBenchmark -{ - /// - /// Loads the embedded library configuration from scratch and applies it. - /// Captures the full deserialize + merge + apply path. - /// Calls first so every invocation - /// is a true cold start ( short-circuits when already enabled). - /// - [Benchmark] - public void LoadAndApply () - { - ConfigurationManager.Disable (true); - ConfigurationManager.Enable (ConfigLocations.LibraryResources); - ConfigurationManager.Apply (); - } - - /// Ensures ConfigurationManager is disabled after all iterations. - [GlobalCleanup] - public void Cleanup () - { - ConfigurationManager.Disable (true); - } -} diff --git a/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs b/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs index 6e97a10e7f..ff5290a5fc 100644 --- a/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs +++ b/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs @@ -11,7 +11,7 @@ namespace Terminal.Gui.Benchmarks.Configuration; /// — derived from /// — deepest derivation (Code → Editable → Normal) /// -/// No required; operates on a standalone +/// No configuration pipeline required; operates on a standalone /// instance. /// /// diff --git a/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs b/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs index ff3ee631b9..9b0eb808ca 100644 --- a/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs +++ b/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.Benchmarks.Configuration; /// /// Measures the cost of switching the active theme via -/// ThemeManager.Theme = "X"; ConfigurationManager.Apply (). +/// ThemeManager.Theme = "X". /// Parametric over every built-in theme name shipped in the embedded config.json. /// /// @@ -27,13 +27,9 @@ public static IEnumerable ThemeNames { get { - ConfigurationManager.Disable (true); - ConfigurationManager.Enable (ConfigLocations.LibraryResources); IEnumerable names = ThemeManager.GetThemeNames (); - ConfigurationManager.Disable (true); - return names; } } @@ -42,8 +38,6 @@ public static IEnumerable ThemeNames [GlobalSetup] public void Setup () { - ConfigurationManager.Disable (true); - ConfigurationManager.Enable (ConfigLocations.LibraryResources); } /// @@ -56,16 +50,13 @@ public void Setup () public void SwitchTheme () { ThemeManager.Theme = ThemeManager.DEFAULT_THEME_NAME; - ConfigurationManager.Apply (); ThemeManager.Theme = ThemeName; - ConfigurationManager.Apply (); } - /// Ensures ConfigurationManager is disabled after all iterations. + /// No cleanup required; facade state is process-wide and restored by the next setup. [GlobalCleanup] public void Cleanup () { - ConfigurationManager.Disable (true); } } diff --git a/Tests/Benchmarks/Configuration/TuiConfigurationBuilderBuildBenchmark.cs b/Tests/Benchmarks/Configuration/TuiConfigurationBuilderBuildBenchmark.cs new file mode 100644 index 0000000000..21f8e8e253 --- /dev/null +++ b/Tests/Benchmarks/Configuration/TuiConfigurationBuilderBuildBenchmark.cs @@ -0,0 +1,32 @@ +using BenchmarkDotNet.Attributes; +using Terminal.Gui.Configuration; + +namespace Terminal.Gui.Benchmarks.Configuration; + +/// +/// Measures the cold-start cost of building the MEC source chain and applying +/// settings facades from the embedded library config.json. +/// +/// +/// +/// Run: +/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*TuiConfigurationBuilderBuild*' +/// +/// +[MemoryDiagnoser] +[BenchmarkCategory ("Configuration", "Startup")] +public class TuiConfigurationBuilderBuildBenchmark +{ + /// + /// Builds a new and applies library defaults + /// plus any remaining sources to the static settings facades. + /// + [Benchmark] + public TuiConfigurationBuilder BuildAndApply () + { + TuiConfigurationBuilder builder = new (); + builder.ApplyToStaticFacades (); + + return builder; + } +} diff --git a/Tests/Benchmarks/ConsoleDrivers/OutputBuffer/OutputWriteBenchmark.cs b/Tests/Benchmarks/ConsoleDrivers/OutputBuffer/OutputWriteBenchmark.cs new file mode 100644 index 0000000000..3665f9ab3b --- /dev/null +++ b/Tests/Benchmarks/ConsoleDrivers/OutputBuffer/OutputWriteBenchmark.cs @@ -0,0 +1,232 @@ +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using BenchmarkDotNet.Attributes; +using Terminal.Gui.Drivers; + +namespace Terminal.Gui.Benchmarks.ConsoleDrivers.OutputBuffer; + +/// +/// Measures output flushing for full and sparse 270x72 frames. +/// +/// +/// The sparse frame matches issue #5627: the first and last cells of every row are dirty, +/// with a large clean gap between them. +/// +/// Run the BenchmarkDotNet cases with: +/// dotnet run --project Tests/Benchmarks -c Release -- --filter "*OutputWriteBenchmark*" +/// +/// +[MemoryDiagnoser] +[BenchmarkCategory ("Output", "Latency")] +public class OutputWriteBenchmark +{ + private const int COLS = 270; + private const int ROWS = 72; + private const int CONPTY_WARMUP_FRAMES = 3; + private const int CONPTY_MEASURED_FRAMES = 20; + + private OutputBufferImpl _fullBuffer = null!; + private MeasuringOutput _output = null!; + private OutputBufferImpl _sparseBuffer = null!; + + /// + /// Creates and warms the output buffers before benchmarking. + /// + [GlobalSetup] + public void Setup () + { + _fullBuffer = CreateBuffer (); + _sparseBuffer = CreateBuffer (); + _output = new (); + + _output.Write (_fullBuffer); + _output.Write (_sparseBuffer); + } + + /// + /// Marks every cell dirty before measuring a full-frame flush. + /// + [IterationSetup (Target = nameof (FullFrame))] + public void PrepareFullFrame () + { + MarkFullFrame (_fullBuffer); + _output.ResetCounters (); + } + + /// + /// Marks only the first and last cells of every row dirty before measuring a sparse flush. + /// + [IterationSetup (Target = nameof (SparseBorderFrame))] + public void PrepareSparseBorderFrame () + { + MarkSparseBorderFrame (_sparseBuffer); + _output.ResetCounters (); + } + + /// + /// Flushes a frame in which every cell is dirty. + /// + [Benchmark (Baseline = true)] + public long FullFrame () + { + _output.Write (_fullBuffer); + + return _output.CharactersWritten; + } + + /// + /// Flushes a frame with dirty border cells separated by clean interior cells. + /// + [Benchmark] + public long SparseBorderFrame () + { + _output.Write (_sparseBuffer); + + return _output.CharactersWritten; + } + + /// + /// Measures sparse-frame output through under Windows Terminal or ConPTY + /// and writes the timing summary to . + /// + /// Path for the JSON timing summary. + public static void RunConPty (string resultPath) + { + if (!OperatingSystem.IsWindows ()) + { + throw new PlatformNotSupportedException ("The ConPTY output benchmark requires Windows."); + } + + if (Console.IsOutputRedirected) + { + throw new InvalidOperationException ("Run the ConPTY output benchmark from Windows Terminal or tuirec."); + } + + if (string.IsNullOrWhiteSpace (resultPath)) + { + throw new ArgumentException ("A result path is required.", nameof (resultPath)); + } + + OutputBufferImpl buffer = CreateBuffer (); + double [] samples = new double [CONPTY_MEASURED_FRAMES]; + + using (WindowsOutput output = new ()) + { + output.Write (buffer); + + for (var frame = 0; frame < CONPTY_WARMUP_FRAMES; frame++) + { + MarkSparseBorderFrame (buffer); + output.Write (buffer); + } + + for (var frame = 0; frame < CONPTY_MEASURED_FRAMES; frame++) + { + MarkSparseBorderFrame (buffer); + long started = Stopwatch.GetTimestamp (); + output.Write (buffer); + samples [frame] = Stopwatch.GetElapsedTime (started).TotalMilliseconds; + } + } + + Array.Sort (samples); + int p95Index = (int)Math.Ceiling (samples.Length * 0.95) - 1; + double median = (samples [samples.Length / 2 - 1] + samples [samples.Length / 2]) / 2; + string fullResultPath = Path.GetFullPath (resultPath); + string? resultDirectory = Path.GetDirectoryName (fullResultPath); + + if (!string.IsNullOrEmpty (resultDirectory)) + { + Directory.CreateDirectory (resultDirectory); + } + + string json = JsonSerializer.Serialize (new + { + Columns = COLS, + Rows = ROWS, + WarmupFrames = CONPTY_WARMUP_FRAMES, + MeasuredFrames = CONPTY_MEASURED_FRAMES, + MeanMilliseconds = samples.Average (), + MedianMilliseconds = median, + P95Milliseconds = samples [p95Index], + MinMilliseconds = samples [0], + MaxMilliseconds = samples [^1] + }, new JsonSerializerOptions { WriteIndented = true }); + + File.WriteAllText (fullResultPath, json); + } + + private static OutputBufferImpl CreateBuffer () + { + OutputBufferImpl buffer = new (); + buffer.SetSize (COLS, ROWS); + + for (var row = 0; row < ROWS; row++) + { + buffer.Move (0, row); + buffer.AddRune ('|'); + buffer.Move (COLS - 1, row); + buffer.AddRune ('|'); + } + + return buffer; + } + + private static void MarkFullFrame (OutputBufferImpl buffer) + { + for (var row = 0; row < ROWS; row++) + { + for (var col = 0; col < COLS; col++) + { + buffer.Contents! [row, col].IsDirty = true; + } + + buffer.DirtyLines [row] = true; + } + } + + private static void MarkSparseBorderFrame (OutputBufferImpl buffer) + { + for (var row = 0; row < ROWS; row++) + { + buffer.Contents! [row, 0].IsDirty = true; + buffer.Contents [row, COLS - 1].IsDirty = true; + buffer.DirtyLines [row] = true; + } + } + + private sealed class MeasuringOutput : OutputBase + { + public long CharactersWritten { get; private set; } + + public int CursorMoves { get; private set; } + + public int Writes { get; private set; } + + public void ResetCounters () + { + CharactersWritten = 0; + CursorMoves = 0; + Writes = 0; + } + + protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) + { + CursorMoves++; + + StringBuilder sequence = new (); + EscSeqUtils.CSI_AppendCursorPosition (sequence, screenPositionY + 1, screenPositionX + 1); + string cursorSequence = sequence.ToString (); + CharactersWritten += cursorSequence.Length; + + return true; + } + + protected override void Write (StringBuilder output) + { + Writes++; + CharactersWritten += output.Length; + } + } +} diff --git a/Tests/Benchmarks/Program.cs b/Tests/Benchmarks/Program.cs index afeb17c4b4..3be0347025 100644 --- a/Tests/Benchmarks/Program.cs +++ b/Tests/Benchmarks/Program.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; +using Terminal.Gui.Benchmarks.ConsoleDrivers.OutputBuffer; using Terminal.Gui.Benchmarks.ViewBase; namespace Terminal.Gui.Benchmarks; @@ -22,6 +23,16 @@ private static void Main (string [] args) case "scenario": ScenarioMemoryBenchmark.Run (); + return; + + case "output-conpty": + if (args.Length < 2) + { + throw new ArgumentException ("Usage: output-conpty "); + } + + OutputWriteBenchmark.RunConPty (args [1]); + return; } } diff --git a/Tests/Benchmarks/README.md b/Tests/Benchmarks/README.md index b58da7c302..478de1cd69 100644 --- a/Tests/Benchmarks/README.md +++ b/Tests/Benchmarks/README.md @@ -130,13 +130,13 @@ dotnet run --project Tests/Benchmarks -c Release -- --filter '*Scroll*' The `Configuration/` directory contains benchmarks for the configuration, theming, and scheme subsystems. -### ConfigurationManagerLoadBenchmark +### TuiConfigurationBuilderBuildBenchmark -Measures the cold-start cost of `ConfigurationManager.Disable(true)` → `Enable(ConfigLocations.LibraryResources)` → `Apply()`. This is the app-startup hot path covering embedded-config load, deserialization, and apply. +Measures the cold-start cost of `new TuiConfigurationBuilder ().ApplyToStaticFacades ()`. This is the app-startup hot path covering embedded-config load, bind, and apply. ### ThemeSwitchBenchmark -Measures `ThemeManager.Theme = "X"; ConfigurationManager.Apply()` against the embedded configuration. Parametric over all built-in theme names (`Default`, `Dark`, `Light`, `TurboPascal 5`, `Anders`, `Green Phosphor`, `Amber Phosphor`). +Measures `ThemeManager.Theme = "X"` against the embedded configuration. Parametric over all built-in theme names (`Default`, `Dark`, `Light`, `TurboPascal 5`, `Anders`, `Green Phosphor`, `Amber Phosphor`). ### SchemeAttributeBenchmark @@ -145,7 +145,7 @@ Measures `Scheme.GetAttributeForRole(VisualRole)` for roles at different depths - **GetHotFocus**: Derived from `Focus` (which itself derives from `Normal`) - **GetCode**: Deepest derivation path (`Code` → `Editable` → `Normal`) -No `ConfigurationManager` required; operates on a standalone `Scheme` instance. +No configuration pipeline required; operates on a standalone `Scheme` instance. ### SchemeSerializationBenchmark @@ -171,6 +171,25 @@ dotnet run --project Tests/Benchmarks -c Release -- --filter '*Config*' '*Scheme - Use BenchmarkDotNet for formal timing benchmarks with statistical rigor - Document what each benchmark measures +## Output Flush Benchmarks + +`OutputWriteBenchmark` compares a full 270x72 frame with a sparse frame whose first and last cells +are dirty on every row. The sparse case reproduces the shape reported in issue #5627. + +```powershell +dotnet run --project Tests/Benchmarks -c Release -- --filter "*OutputWriteBenchmark*" --job short --exporters json +``` + +To measure the real Windows output path, run the benchmark through Windows Terminal or `tuirec` and +provide a JSON result path: + +```powershell +dotnet run --project Tests/Benchmarks -c Release -- output-conpty "$env:TEMP\tg-output-conpty.json" +``` + +The ConPTY benchmark requires Windows with output attached to a terminal. It warms three sparse +frames, measures twenty frames, and records mean, median, p95, minimum, and maximum frame latency. + ## Continuous Integration ### Layer 1: Performance Smoke Tests diff --git a/Tests/IntegrationTests/AssemblyInfo.cs b/Tests/IntegrationTests/AssemblyInfo.cs index 4e9b2b2db2..b710715e0c 100644 --- a/Tests/IntegrationTests/AssemblyInfo.cs +++ b/Tests/IntegrationTests/AssemblyInfo.cs @@ -1,6 +1,5 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Drivers; global using Terminal.Gui.Input; diff --git a/Tests/IntegrationTests/FluentTests/FileDialogTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogTests.cs index 8434ea3749..76dacf36ab 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogTests.cs @@ -144,7 +144,7 @@ public void SaveFileDialog_UsingOkButton_TabEnter (string d) .AssertEqual (GetFileSystemRoot (fs!), sd!.FileName); } - private string GetFileSystemRoot (IFileSystem fs) => RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ? $@"C:{fs.Path.DirectorySeparatorChar}" : "/"; + private string GetFileSystemRoot (IFileSystem fs) => OperatingSystem.IsWindows () ? $@"C:{fs.Path.DirectorySeparatorChar}" : "/"; [Theory] [MemberData (nameof (GetAllDriverNames))] diff --git a/Tests/IntegrationTests/ProgressScenarioTests.cs b/Tests/IntegrationTests/ProgressScenarioTests.cs index 30dcbc7aed..89add53d27 100644 --- a/Tests/IntegrationTests/ProgressScenarioTests.cs +++ b/Tests/IntegrationTests/ProgressScenarioTests.cs @@ -24,7 +24,6 @@ public class ProgressScenarioTests (ITestOutputHelper output) [Fact] public void Progress_With_Running_Threaded_Timer_Quits_Without_Crashing () { - ConfigurationManager.Disable (true); Application.ResetState (true); IApplication? app = null; diff --git a/Tests/IntegrationTests/UICatalogScenarioTests.cs b/Tests/IntegrationTests/UICatalogScenarioTests.cs index 77e59b31ea..87d042d792 100644 --- a/Tests/IntegrationTests/UICatalogScenarioTests.cs +++ b/Tests/IntegrationTests/UICatalogScenarioTests.cs @@ -17,8 +17,6 @@ public void All_Scenarios_Dispose_Properly (Type scenarioType) View.EnableDebugIDisposableAsserts = true; View.Instances.Clear (); #endif - - ConfigurationManager.Disable (true); Application.ResetState (true); uint abortTime = 3000; diff --git a/Tests/NativeAotSmoke/Program.cs b/Tests/NativeAotSmoke/Program.cs index 67126b5bfd..3a6ac08cef 100644 --- a/Tests/NativeAotSmoke/Program.cs +++ b/Tests/NativeAotSmoke/Program.cs @@ -1,4 +1,4 @@ -// Native AOT smoke test application for Terminal.Gui. +// Native AOT smoke test application for Terminal.Gui. // // This app is an AOT-safe equivalent of UICatalog's AllViewsTester. It statically instantiates // every IDesignable view and calls EnableForDesign () to populate demo data. @@ -26,8 +26,6 @@ private static async Task RunAsync (string [] args) { bool smokeTest = args.Length > 0 && args [0] == "--smoke-test"; - ConfigurationManager.Enable (ConfigLocations.All); - using IApplication app = Application.Create ().Init (); #region Localization sanity check @@ -60,12 +58,12 @@ private static async Task RunAsync (string [] args) private static void ExerciseDictionaryDeepCloning () { - _ = DeepCloner.DeepClone (Color.Colors16); - _ = DeepCloner.DeepClone (Application.DefaultKeyBindings); - _ = DeepCloner.DeepClone (View.DefaultKeyBindings); - _ = DeepCloner.DeepClone (View.ViewKeyBindings); - _ = DeepCloner.DeepClone (ThemeManager.Themes); - _ = DeepCloner.DeepClone (SchemeManager.Schemes); + _ = Color.Colors16; + _ = Application.DefaultKeyBindings; + _ = View.DefaultKeyBindings; + _ = View.ViewKeyBindings; + _ = ThemeManager.GetThemeNames (); + _ = SchemeManager.Schemes; } } diff --git a/Tests/README.md b/Tests/README.md index ed235b6bd5..bcf78615a4 100644 --- a/Tests/README.md +++ b/Tests/README.md @@ -66,12 +66,12 @@ go in `UnitTestsParallelizable` — they belong in `UnitTests.NonParallelizable` | `ApplicationImpl.SetInstance(impl)` | Replaces the singleton (used in tests) | | `ApplicationImpl.ResetModelUsageTracking()` | Resets the static "which model was used" flag | -### `ConfigurationManager` (singleton — `Terminal.Gui.Configuration/`) +### Settings facades (process-wide — `Terminal.Gui.Configuration/`) | Member | Notes | |--------|-------| -| `CM.IsEnabled` | Global flag; affects all code reading configuration | -| `CM.Disable(reset)` | Disables and optionally resets to hard-coded defaults | +| `TuiConfigurationBuilder.Shared` | Process-wide builder applied at assembly load | +| `*Settings.Current` / `*Settings.Defaults` | Static facades mutated by `ApplyToStaticFacades` | ### `View.Diagnostics` (static field — `Terminal.Gui.ViewBase/View.cs`) @@ -85,7 +85,7 @@ go in `UnitTestsParallelizable` — they belong in `UnitTests.NonParallelizable` | Test characteristic | Project | |---------------------|---------| -| No static state; no `Application.Init`/`Shutdown`; no `ConfigurationManager` mutations | `UnitTestsParallelizable` | +| No static state; no `Application.Init`/`Shutdown`; no process-wide settings-facade mutations | `UnitTestsParallelizable` | | Calls `Application.Init`/`Shutdown` directly, or mutates `Application.DefaultKeyBindings`, `Application.ForceDriver`, or `ApplicationImpl.ResetModelUsageTracking()` | `UnitTests.NonParallelizable` | | Uses `[AutoInitShutdown]` or `[SetupFakeApplication]` (fake driver, does not call real `Application.Init`) | Currently in `UnitTests.Legacy`; migrate to `UnitTestsParallelizable` or `UnitTests.NonParallelizable` after review | | Not yet reviewed / ported | `UnitTests.Legacy` | diff --git a/Tests/StressTests/AssemblyInfo.cs b/Tests/StressTests/AssemblyInfo.cs index 4e9b2b2db2..b710715e0c 100644 --- a/Tests/StressTests/AssemblyInfo.cs +++ b/Tests/StressTests/AssemblyInfo.cs @@ -1,6 +1,5 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Drivers; global using Terminal.Gui.Input; diff --git a/Tests/StressTests/ScenariosStressTests.cs b/Tests/StressTests/ScenariosStressTests.cs index 5ca64300ca..2fb6480249 100644 --- a/Tests/StressTests/ScenariosStressTests.cs +++ b/Tests/StressTests/ScenariosStressTests.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using UICatalog; namespace StressTests; @@ -31,8 +31,6 @@ public void All_Scenarios_Benchmark (Type scenarioType) Assert.Null (_timeoutLock); _timeoutLock = new object (); - ConfigurationManager.Disable (true); - // If a previous test failed, this will ensure that the Application is in a clean state Application.ResetState (true); diff --git a/Tests/UnitTests.Legacy/AssemblyInfo.cs b/Tests/UnitTests.Legacy/AssemblyInfo.cs index b36b7614ee..54efd6d5ff 100644 --- a/Tests/UnitTests.Legacy/AssemblyInfo.cs +++ b/Tests/UnitTests.Legacy/AssemblyInfo.cs @@ -1,6 +1,5 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Time; global using Terminal.Gui.Testing; diff --git a/Tests/UnitTests.Legacy/AutoInitShutdownAttribute.cs b/Tests/UnitTests.Legacy/AutoInitShutdownAttribute.cs index 0862475659..a2089966b4 100644 --- a/Tests/UnitTests.Legacy/AutoInitShutdownAttribute.cs +++ b/Tests/UnitTests.Legacy/AutoInitShutdownAttribute.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using System.Reflection; using Xunit.v3; @@ -78,22 +78,13 @@ public override void After (MethodInfo methodUnderTest, IXunitTest test) } } - Debug.Assert (!CM.IsEnabled, "This test left ConfigurationManager enabled!"); - - // Force the ConfigurationManager to reset to its hardcoded defaults - CM.Disable (true); } public override void Before (MethodInfo methodUnderTest, IXunitTest test) { Debug.WriteLine ($"Before: {methodUnderTest?.Name ?? "Unknown Test"}"); - Debug.Assert (!CM.IsEnabled, "A previous test left ConfigurationManager enabled!"); - - // Disable & force the ConfigurationManager to reset to its hardcoded defaults - CM.Disable (true); - //Debug.Assert(!CM.IsEnabled, "Some other test left ConfigurationManager enabled."); if (_autoInit) { diff --git a/Tests/UnitTests.Legacy/TestRespondersDisposedAttribute.cs b/Tests/UnitTests.Legacy/TestRespondersDisposedAttribute.cs index 74fca8453d..19962ffb3d 100644 --- a/Tests/UnitTests.Legacy/TestRespondersDisposedAttribute.cs +++ b/Tests/UnitTests.Legacy/TestRespondersDisposedAttribute.cs @@ -21,8 +21,6 @@ public override void After (MethodInfo methodUnderTest, IXunitTest test) { Debug.WriteLine ($"After: {methodUnderTest.Name}"); - Debug.Assert (!CM.IsEnabled, "This test left ConfigurationManager enabled!"); - base.After (methodUnderTest, test); #if DEBUG_IDISPOSABLE diff --git a/Tests/UnitTests.NonParallelizable/Application/SynchronizationContextTests.cs b/Tests/UnitTests.NonParallelizable/Application/SynchronizationContextTests.cs index ab3641dee8..f14203a567 100644 --- a/Tests/UnitTests.NonParallelizable/Application/SynchronizationContextTests.cs +++ b/Tests/UnitTests.NonParallelizable/Application/SynchronizationContextTests.cs @@ -1,36 +1,51 @@ // Copilot +// Claude - Fable 5 #nullable enable namespace UnitTests.NonParallelizable.ApplicationTests; /// -/// Tests for the that is set during the Terminal.Gui application lifecycle. -/// Must run non-concurrently because and mutate -/// the process-wide . +/// Tests for the contract of the Terminal.Gui application lifecycle. +/// As of #5636, does NOT install the app's main-loop context as the +/// thread's ambient context — it becomes ambient only while a session is running (see Begin/Run) — +/// so an await between Init and Run/RunAsync cannot capture a context +/// whose continuations would be stranded on a not-yet-running main loop. /// public class SynchronizationContextTests { [Fact] - public void Init_SetsSynchronizationContext_Dispose_ClearsIt () + public void Init_LeavesAmbientContext_Dispose_LeavesForeignContext () { - IApplication app = Application.Create (); + SynchronizationContext? previous = SynchronizationContext.Current; + SynchronizationContext marker = new (); try { - app.Init (DriverRegistry.Names.ANSI); + SynchronizationContext.SetSynchronizationContext (marker); + + IApplication app = Application.Create (); + + try + { + app.Init (DriverRegistry.Names.ANSI); - Assert.NotNull (SynchronizationContext.Current); + Assert.Same (marker, SynchronizationContext.Current); + } + finally + { + app.Dispose (); + } + + Assert.Same (marker, SynchronizationContext.Current); } finally { - app.Dispose (); + SynchronizationContext.SetSynchronizationContext (previous); } - - Assert.Null (SynchronizationContext.Current); } [Fact] - public void Init_SynchronizationContext_CreateCopy_ReturnsDifferentInstance () + public void App_SynchronizationContext_CreateCopy_ReturnsDifferentInstance () { IApplication app = Application.Create (); @@ -38,7 +53,7 @@ public void Init_SynchronizationContext_CreateCopy_ReturnsDifferentInstance () { app.Init (DriverRegistry.Names.ANSI); - SynchronizationContext context = SynchronizationContext.Current!; + SynchronizationContext context = ((ApplicationImpl)app).SynchronizationContext!; SynchronizationContext copy = context.CreateCopy (); Assert.NotNull (copy); diff --git a/Tests/UnitTests.NonParallelizable/AssemblyInfo.cs b/Tests/UnitTests.NonParallelizable/AssemblyInfo.cs index e6cc8f6e5c..54efd6d5ff 100644 --- a/Tests/UnitTests.NonParallelizable/AssemblyInfo.cs +++ b/Tests/UnitTests.NonParallelizable/AssemblyInfo.cs @@ -1,6 +1,5 @@ global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Time; global using Terminal.Gui.Testing; diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigPropertyAssemblyScanTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigPropertyAssemblyScanTests.cs deleted file mode 100644 index d85bd147cf..0000000000 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigPropertyAssemblyScanTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Immutable; -using System.Reflection; -using System.Runtime.Loader; - -namespace UnitTests.NonParallelizable.ConfigurationTests; - -public class ConfigPropertyAssemblyScanTests -{ - [ConfigurationProperty (Scope = typeof (ConfigPropertyAssemblyScanTestsScope))] - public static bool? ScanSentinel { get; set; } - - private class ConfigPropertyAssemblyScanTestsScope : Scope - { - } - - [Fact] - public void ScanAssembliesForConfigPropertyHosts_Skips_TypeLoadException_From_Foreign_CustomAttribute_Metadata () - { - string fixtureRoot = Path.Combine (AppContext.BaseDirectory, "ConfigPropertyTypeLoadFixtures"); - string providerPath = Path.Combine (fixtureRoot, "FakeCodeAnalysis.dll"); - string consumerPath = Path.Combine (fixtureRoot, "ConsumerLib.dll"); - - Assert.True (File.Exists (providerPath)); - Assert.True (File.Exists (consumerPath)); - - AssemblyLoadContext loadContext = new (nameof (ConfigPropertyAssemblyScanTests), isCollectible: true); - loadContext.LoadFromAssemblyPath (providerPath); - Assembly consumerAssembly = loadContext.LoadFromAssemblyPath (consumerPath); - Type consumerType = consumerAssembly.GetType ("ConsumerLib.FixtureType", throwOnError: true)!; - PropertyInfo consumerProperty = consumerType.GetProperty ("HasValue")!; - - Assert.Throws ( - () => consumerProperty.GetCustomAttribute (typeof (ConfigurationPropertyAttribute)) - ); - - ImmutableSortedDictionary hosts = - ConfigProperty.ScanAssembliesForConfigPropertyHosts ([consumerAssembly, typeof (ConfigPropertyAssemblyScanTests).Assembly]); - - Assert.Contains (nameof (ConfigPropertyAssemblyScanTests), hosts.Keys); - Assert.Same (typeof (ConfigPropertyAssemblyScanTests), hosts [nameof (ConfigPropertyAssemblyScanTests)]); - - loadContext.Unload (); - } -} diff --git a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs deleted file mode 100644 index 524743c1fc..0000000000 --- a/Tests/UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs +++ /dev/null @@ -1,1842 +0,0 @@ -using System.Collections.Frozen; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using static Terminal.Gui.Configuration.ConfigurationManager; -using File = System.IO.File; -using SourcesManager = Terminal.Gui.Configuration.SourcesManager; - -#pragma warning disable IDE1006 - -namespace UnitTests.NonParallelizable.ConfigurationTests; - -public class ConfigurationMangerTests (ITestOutputHelper output) -{ - [ConfigurationProperty (Scope = typeof (AppSettingsScope))] - [JsonConverter (typeof (YesNoBooleanJsonConverter))] - public static bool? CustomConvertedBoolean { get; set; } - - [Fact] - public void ModuleInitializer_Was_Called () - { - Assert.False (IsEnabled); - Assert.True (IsInitialized ()); - } - - [Fact] - public void Initialize_Throws_If_Called_Explicitly () - { - Assert.False (IsEnabled); - - Assert.Throws (Initialize); - } - - [Fact] - public void HardCodedDefaultCache_Properties_Are_Copies () - { - Assert.False (IsEnabled); - Enable (ConfigLocations.HardCoded); - - Assert.Equal (Key.Esc, Application.GetDefaultKey (Command.Quit)); - - ConfigProperty fromSettings = Settings! ["Application.DefaultKeyBindings"]; - - FrozenDictionary initialCache = GetHardCodedConfigPropertyCache (); - Assert.NotNull (initialCache); - - ConfigProperty fromCache = initialCache ["Application.DefaultKeyBindings"]; - - // Assert - Assert.NotEqual (fromCache, fromSettings); - Disable (true); - } - - [Fact] - public void HardCodedDefaultCache_KeyBindingDictionaries_Are_Typed_Deep_Copies () - { - // Copilot - Assert.False (IsEnabled); - Application.ResetState (true); - - try - { - AssertKeyBindingDictionaryIsDeepCopy ("Application.DefaultKeyBindings", Application.DefaultKeyBindings!); - AssertKeyBindingDictionaryIsDeepCopy ("View.DefaultKeyBindings", View.DefaultKeyBindings!); - } - finally - { - Disable (true); - Application.ResetState (true); - } - - static void AssertKeyBindingDictionaryIsDeepCopy (string propertyName, Dictionary currentBindings) - { - FrozenDictionary cache = GetHardCodedConfigPropertyCache (); - Dictionary cachedBindings = Assert.IsType> (cache [propertyName].PropertyValue); - - Assert.NotSame (currentBindings, cachedBindings); - Assert.NotEmpty (cachedBindings); - - KeyValuePair cachedEntry = cachedBindings.First (kvp => HasAnyKeys (kvp.Value)); - PlatformKeyBinding currentBinding = currentBindings [cachedEntry.Key]; - - Assert.NotSame (currentBinding, cachedEntry.Value); - AssertKeyArraysAreDeepCopies (currentBinding.All, cachedEntry.Value.All); - AssertKeyArraysAreDeepCopies (currentBinding.Windows, cachedEntry.Value.Windows); - AssertKeyArraysAreDeepCopies (currentBinding.Linux, cachedEntry.Value.Linux); - AssertKeyArraysAreDeepCopies (currentBinding.Macos, cachedEntry.Value.Macos); - } - - static bool HasAnyKeys (PlatformKeyBinding binding) - { - return binding.All is { Length: > 0 } - || binding.Windows is { Length: > 0 } - || binding.Linux is { Length: > 0 } - || binding.Macos is { Length: > 0 }; - } - -#pragma warning disable CS8632 // Nullable annotations document the nullable PlatformKeyBinding properties in this non-nullable test file. - static void AssertKeyArraysAreDeepCopies (Key []? currentKeys, Key []? cachedKeys) -#pragma warning restore CS8632 - { - if (currentKeys is null) - { - Assert.Null (cachedKeys); - - return; - } - - Assert.NotNull (cachedKeys); - Assert.NotSame (currentKeys, cachedKeys); - Assert.Equal (currentKeys.Length, cachedKeys.Length); - - for (var i = 0; i < currentKeys.Length; i++) - { - Assert.NotSame (currentKeys [i], cachedKeys [i]); - Assert.Equal (currentKeys [i], cachedKeys [i]); - } - } - } - - [Fact] - public void HardCoded_Default_Theme_Uses_Fully_Populated_Cache_Values () - { - // Copilot - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - - ThemeScope defaultTheme = ThemeManager.Themes! [ThemeManager.DEFAULT_THEME_NAME]; - - Assert.Equal ( - MouseState.PressedOutside | MouseState.Pressed | MouseState.In, - (MouseState)defaultTheme ["CheckBox.DefaultMouseHighlightStates"].PropertyValue! - ); - Assert.Equal ( - CursorStyle.BlinkingBlock, - (CursorStyle)defaultTheme ["LinearRangeDefaults.DefaultCursorStyle"].PropertyValue! - ); - } - finally - { - Disable (true); - } - } - - [Fact] - public void GetHardCodedDefaultCache_Always_Returns_Same_Ref () - { - // It's important it always returns the same cache ref, so no copies are made - // Otherwise it's a big performance hit - Assert.False (IsEnabled); - - try - { - FrozenDictionary initialCache = GetHardCodedConfigPropertyCache (); - FrozenDictionary cache = GetHardCodedConfigPropertyCache (); - Assert.Equal (initialCache, cache); - } - finally - { - Disable (true); - } - } - - [Fact] - public void HardCodedDefaultCache_Properties_Are_Immutable () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - Assert.Equal (10000, FileDialog.MaxSearchResults); - - FrozenDictionary initialCache = GetHardCodedConfigPropertyCache (); - Assert.NotNull (initialCache); - Assert.Equal (10000, (int)initialCache ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Act - Settings! ["FileDialog.MaxSearchResults"].PropertyValue = 42; - Assert.Equal (42, (int)Settings ["FileDialog.MaxSearchResults"].PropertyValue!); - - Settings ["FileDialog.MaxSearchResults"].Apply (); - Assert.Equal (42, FileDialog.MaxSearchResults); - - FileDialog.MaxSearchResults = 99; - - // Assert - FrozenDictionary cache = GetHardCodedConfigPropertyCache (); - Assert.True (initialCache ["FileDialog.MaxSearchResults"].Immutable); - Assert.Equal (10000, (int)initialCache ["FileDialog.MaxSearchResults"].PropertyValue!); - } - finally - { - Disable (true); - Application.ResetState (true); - } - } - - [Fact] - public void Disable_Settings_Is_NotNull () - { - Assert.False (IsEnabled); - - Disable (); - Assert.NotNull (Settings); - } - - [Fact] - public void Disable_With_ResetToHardCodedDefaults_True_Works_When_Disabled () - { - Assert.False (IsEnabled); - Disable (true); - } - - [Fact] - public void Enable_Settings_Is_Valid () - { - Assert.False (IsEnabled); - - Enable (ConfigLocations.HardCoded); - - Assert.NotNull (Settings); - - Disable (true); - } - - [Fact] - public void Enable_HardCoded_Resets_Schemes_After_Runtime_Config () - { - Assert.False (IsEnabled); - - try - { - // Arrange: Start from hard-coded defaults and capture baseline scheme values. - Enable (ConfigLocations.HardCoded); - Dictionary schemes = SchemeManager.GetSchemes (); - Assert.NotNull (schemes); - Assert.NotEmpty (schemes); - Color baselineFg = schemes ["Base"].Normal.Foreground; - Color baselineBg = schemes ["Base"].Normal.Background; - - // Sanity: defaults should be stable - Assert.NotEqual (default (Color), baselineFg); - Assert.NotEqual (default (Color), baselineBg); - - // Act: Override the Base scheme via runtime JSON and apply - ThrowOnJsonErrors = true; - - RuntimeConfig = """ - { - "Themes": [ - { - "Default": { - "Schemes": [ - { - "Base": { - "Normal": { - "Foreground": "Black", - "Background": "Gray" - } - } - } - ] - } - } - ] - } - """; - Load (ConfigLocations.Runtime); - Apply (); - - // Verify override took effect - Dictionary overridden = SchemeManager.GetSchemes (); - Assert.Equal (Color.Black, overridden ["Base"].Normal.Foreground); - Assert.Equal (Color.Gray, overridden ["Base"].Normal.Background); - - // Now simulate "CM.Enable(true)" semantics: re-enable with HardCoded to reset - Disable (); - Enable (ConfigLocations.HardCoded); - - // Assert: schemes are reset to the original hard-coded baseline - Dictionary reset = SchemeManager.GetSchemes (); - Assert.Equal (baselineFg, reset ["Base"].Normal.Foreground); - Assert.Equal (baselineBg, reset ["Base"].Normal.Background); - } - finally - { - Disable (true); - Application.ResetState (true); - } - } - - [Fact] - public void Enable_HardCoded_Resets_Theme_Dictionary_And_Selection () - { - Assert.False (IsEnabled); - - try - { - // Arrange: Enable defaults - Enable (ConfigLocations.HardCoded); - Assert.Equal (ThemeManager.DEFAULT_THEME_NAME, ThemeManager.Theme); - Assert.Single (ThemeManager.Themes!); - Assert.True (ThemeManager.Themes.ContainsKey (ThemeManager.DEFAULT_THEME_NAME)); - - // Act: Load a runtime config that introduces a custom theme and selects it - ThrowOnJsonErrors = true; - - RuntimeConfig = """ - { - "Theme": "Custom", - "Themes": [ - { - "Custom": { - "Schemes": [ - { - "Base": { - "Normal": { - "Foreground": "Yellow", - "Background": "Black" - } - } - } - ] - } - } - ] - } - """; - - // Capture dynamically created hardCoded hard-coded scheme colors - ImmutableSortedDictionary hardCodedSchemes = SchemeManager.GetHardCodedSchemes ()!; - - Color hardCodedBaseNormalFg = hardCodedSchemes ["Base"].Normal.Foreground; - Assert.Equal (Color.None.ToString (), hardCodedBaseNormalFg.ToString ()); - - Load (ConfigLocations.Runtime); - Apply (); - - // Verify the runtime selection took effect - Assert.Equal ("Custom", ThemeManager.Theme); - - // Now simulate "CM.Enable(true)" semantics: re-enable with HardCoded to reset - Disable (); - Enable (ConfigLocations.HardCoded); - - // Assert: selection and dictionary have been reset to hard-coded defaults - Assert.Equal (ThemeManager.DEFAULT_THEME_NAME, ThemeManager.Theme); - Assert.Single (ThemeManager.Themes!); - Assert.True (ThemeManager.Themes.ContainsKey (ThemeManager.DEFAULT_THEME_NAME)); - - // Also assert the Base scheme is back to defaults (sanity check) - Scheme baseScheme = SchemeManager.GetSchemes () ["Base"]; - Assert.Equal (hardCodedBaseNormalFg.ToString (), SchemeManager.GetSchemes () ["Base"]!.Normal.Foreground.ToString ()); - } - finally - { - Disable (true); - Application.ResetState (true); - } - } - - [Fact] - public void Apply_Applies_Theme () - { - Assert.False (IsEnabled); - Enable (ConfigLocations.HardCoded); - - var theme = new ThemeScope (); - theme.LoadHardCodedDefaults (); - Assert.NotEmpty (theme); - - Assert.True (ThemeManager.Themes!.TryAdd ("testTheme", theme)); - Assert.Equal (2, ThemeManager.Themes.Count); - - Assert.Equal (LineStyle.Rounded, FrameView.DefaultBorderStyle); - theme ["FrameView.DefaultBorderStyle"].PropertyValue = LineStyle.Double; - - ThemeManager.Theme = "testTheme"; - Apply (); - - Assert.Equal (LineStyle.Double, FrameView.DefaultBorderStyle); - - Disable (true); - } - - [Fact] - public void Apply_Raises_Applied () - { - Assert.False (IsEnabled); - - Enable (ConfigLocations.HardCoded); - - Applied += ConfigurationManagerApplied; - var fired = false; - - void ConfigurationManagerApplied (object sender, ConfigurationManagerEventArgs obj) - { - fired = true; - - // assert - Assert.Equal (KeyCode.Q, Application.GetDefaultKey (Command.Quit).KeyCode); - Assert.Equal (KeyCode.F, Application.GetDefaultKey (Command.NextTabGroup).KeyCode); - Assert.Equal (KeyCode.B, Application.GetDefaultKey (Command.PreviousTabGroup).KeyCode); - } - - // act - Dictionary bindings = new ((Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!); - bindings [Command.Quit] = Bind.All (Key.Q); - bindings [Command.NextTabGroup] = Bind.All (Key.F); - bindings [Command.PreviousTabGroup] = Bind.All (Key.B); - Settings! ["Application.DefaultKeyBindings"].PropertyValue = bindings; - - Apply (); - - // assert - Assert.True (fired); - - Applied -= ConfigurationManagerApplied; - - Disable (true); - Application.ResetState (true); - } - - [Fact] - public void Load_Raises_Updated () - { - Assert.False (IsEnabled); - - var fired = false; - Enable (ConfigLocations.HardCoded); - - ThrowOnJsonErrors = true; - Dictionary loadBindings = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Esc, loadBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - - Updated += ConfigurationManagerUpdated; - - // Act - // Only select locations under test control - Load (ConfigLocations.LibraryResources | ConfigLocations.AppResources | ConfigLocations.Runtime); - - // assert - Assert.True (fired); - - // clean up - Updated -= ConfigurationManagerUpdated; - Disable (true); - Application.ResetState (true); - - return; - - void ConfigurationManagerUpdated (object sender, ConfigurationManagerEventArgs obj) { fired = true; } - } - - [Fact] - public void Load_And_Apply_Performance_Check () - { - Assert.False (IsEnabled); - - Enable (ConfigLocations.HardCoded); - - try - { - // Start stopwatch - var stopwatch = new Stopwatch (); - stopwatch.Start (); - - // Act - Load (ConfigLocations.All); - Apply (); - - // Stop stopwatch - stopwatch.Stop (); - - // Assert - output.WriteLine ($"Load took {stopwatch.ElapsedMilliseconds} ms"); - - // Ensure load time is reasonable (adjust threshold as needed) - Assert.True ( - stopwatch.ElapsedMilliseconds < 1000, - $"Loading configuration took {stopwatch.ElapsedMilliseconds}ms, which exceeds reasonable threshold"); - } - finally - { - Disable (true); - Application.ResetState (true); - } - } - - [Fact] - public void Load_Loads_Custom_Json () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - ThrowOnJsonErrors = true; - - Assert.Equal (Key.Esc, Application.GetDefaultKey (Command.Quit)); - - // act - RuntimeConfig = """ - - { - "Application.DefaultKeyBindings": { "Quit": { "All": ["Ctrl-Q"] } } - } - """; - Load (ConfigLocations.Runtime); - - // assert - Dictionary loadedBindings = (Dictionary)Settings ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Q.WithCtrl, loadedBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - } - finally - { - // clean up - Disable (true); - Application.ResetState (true); - } - } - - [Fact (Skip = "Events disabled")] - public void ResetToCurrentValues_Raises_Updated () - { - Assert.False (IsEnabled); - - var fired = false; - - try - { - Enable (ConfigLocations.HardCoded); - - Dictionary bindings6 = new ((Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!); - bindings6 [Command.Quit] = Bind.All (Key.Q); - Settings! ["Application.DefaultKeyBindings"].PropertyValue = bindings6; - - Updated += ConfigurationManagerUpdated; - - // Act - UpdateToCurrentValues (); - - // assert - Assert.True (fired); - } - finally - { - Updated -= ConfigurationManagerUpdated; - - Disable (true); - Application.ResetState (true); - } - - return; - - void ConfigurationManagerUpdated (object sender, ConfigurationManagerEventArgs obj) { fired = true; } - } - - [Fact] - public void ResetToHardCodedDefaults_and_Load_LibraryResourcesOnly_are_same () - { - Assert.False (IsEnabled); - - try - { - // arrange - Enable (ConfigLocations.HardCoded); - - Dictionary bindings7 = new ((Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!); - bindings7 [Command.Quit] = Bind.All (Key.Q); - bindings7 [Command.NextTabGroup] = Bind.All (Key.F); - bindings7 [Command.PreviousTabGroup] = Bind.All (Key.B); - Settings! ["Application.DefaultKeyBindings"].PropertyValue = bindings7; - Settings.Apply (); - - // assert apply worked - Assert.Equal (KeyCode.Q, Application.GetDefaultKey (Command.Quit).KeyCode); - Assert.Equal (KeyCode.F, Application.GetDefaultKey (Command.NextTabGroup).KeyCode); - Assert.Equal (KeyCode.B, Application.GetDefaultKey (Command.PreviousTabGroup).KeyCode); - - //act - ResetToHardCodedDefaults (); - - // assert - Assert.NotEmpty (ThemeManager.Themes!); - Assert.Equal ("Default", ThemeManager.Theme); - Assert.Equal (Key.Esc, Application.GetDefaultKey (Command.Quit)); - Assert.Equal (Key.F6, Application.GetDefaultKey (Command.NextTabGroup)); - Assert.Equal (Key.F6.WithShift, Application.GetDefaultKey (Command.PreviousTabGroup)); - - // arrange - Dictionary bindings7b = new ((Dictionary)Settings ["Application.DefaultKeyBindings"].PropertyValue!); - bindings7b [Command.Quit] = Bind.All (Key.Q); - bindings7b [Command.NextTabGroup] = Bind.All (Key.F); - bindings7b [Command.PreviousTabGroup] = Bind.All (Key.B); - Settings ["Application.DefaultKeyBindings"].PropertyValue = bindings7b; - Settings.Apply (); - - // act - ResetToHardCodedDefaults first, then load library resources on top - ResetToHardCodedDefaults (); - Load (ConfigLocations.LibraryResources); - Apply (); - - // assert - Assert.NotEmpty (ThemeManager.Themes); - Assert.Equal ("Default", ThemeManager.Theme); - Assert.Equal (KeyCode.Esc, Application.GetDefaultKey (Command.Quit).KeyCode); - Assert.Equal (Key.F6, Application.GetDefaultKey (Command.NextTabGroup)); - Assert.Equal (Key.F6.WithShift, Application.GetDefaultKey (Command.PreviousTabGroup)); - } - finally - { - Disable (true); - Application.ResetState (true); - } - } - - [Fact] - public void ResetToHardCodedDefaults_Resets () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - - // Capture dynamically created hardCoded hard-coded scheme colors - ImmutableSortedDictionary hardCodedSchemesViaSchemeManager = SchemeManager.GetHardCodedSchemes ()!; - - Dictionary hardCodedSchemes = - GetHardCodedConfigPropertiesByScope ("ThemeScope")!.ToFrozenDictionary () ["Schemes"].PropertyValue as Dictionary; - - Color hardCodedBaseNormalFg = hardCodedSchemesViaSchemeManager ["Base"].Normal.Foreground; - - Assert.Equal (Color.None.ToString (), hardCodedBaseNormalFg.ToString ()); - - // Capture current scheme colors - Dictionary currentSchemes = SchemeManager.GetSchemes ()!; - - Color currentBaseNormalFg = currentSchemes ["Base"].Normal.Foreground; - - Assert.Equal (hardCodedBaseNormalFg.ToString (), currentBaseNormalFg.ToString ()); - - // Arrange - var json = @" -{ - ""$schema"": ""https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json"", - ""Application.DefaultKeyBindings"": { ""Quit"": { ""All"": [""Alt-Z""] } }, - ""Theme"": ""Default"", - ""Themes"": [ - { - ""Default"": { - ""MessageBox.DefaultButtonAlignment"": ""End"", - ""Schemes"": [ - { - ""Accent"": { - ""Normal"": { - ""Foreground"": ""BrightGreen"", - ""Background"": ""Black"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""Cyan"" - }, - ""HotNormal"": { - ""Foreground"": ""Yellow"", - ""Background"": ""Black"" - }, - ""HotFocus"": { - ""Foreground"": ""Blue"", - ""Background"": ""Cyan"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""Black"" - } - } - }, - { - ""Base"": { - ""Normal"": { - ""Foreground"": ""White"", - ""Background"": ""Blue"" - }, - ""Focus"": { - ""Foreground"": ""Black"", - ""Background"": ""Gray"" - }, - ""HotNormal"": { - ""Foreground"": ""BrightCyan"", - ""Background"": ""Blue"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightBlue"", - ""Background"": ""Gray"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""Blue"" - } - } - }, - { - ""Dialog"": { - ""Normal"": { - ""Foreground"": ""Black"", - ""Background"": ""Gray"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""DarkGray"" - }, - ""HotNormal"": { - ""Foreground"": ""Blue"", - ""Background"": ""Gray"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""DarkGray"" - }, - ""Disabled"": { - ""Foreground"": ""Gray"", - ""Background"": ""DarkGray"" - } - } - }, - { - ""Menu"": { - ""Normal"": { - ""Foreground"": ""White"", - ""Background"": ""DarkGray"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""Black"" - }, - ""HotNormal"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""DarkGray"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""Black"" - }, - ""Disabled"": { - ""Foreground"": ""Gray"", - ""Background"": ""DarkGray"" - } - } - }, - { - ""Error"": { - ""Normal"": { - ""Foreground"": ""Red"", - ""Background"": ""White"" - }, - ""Focus"": { - ""Foreground"": ""Black"", - ""Background"": ""BrightRed"" - }, - ""HotNormal"": { - ""Foreground"": ""Black"", - ""Background"": ""White"" - }, - ""HotFocus"": { - ""Foreground"": ""White"", - ""Background"": ""BrightRed"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""White"" - } - } - } - ] - } - } - ] -} - "; - - // ResetToCurrentValues (); - - ThrowOnJsonErrors = true; - ConfigurationManager.SourcesManager?.Load (Settings, json, "UpdateFromJson", ConfigLocations.Runtime); - - Assert.Equal ("Default", ThemeManager.Theme); - Assert.Equal (KeyCode.Esc, Application.GetDefaultKey (Command.Quit).KeyCode); - Dictionary settingsBindings8 = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (KeyCode.Z | KeyCode.AltMask, settingsBindings8 [Command.Quit].GetCurrentPlatformKeys ().First ().KeyCode); - Assert.Equal (Alignment.Center, MessageBox.DefaultButtonAlignment); - - // Get current scheme colors again - currentSchemes = SchemeManager.GetSchemes ()!; - - currentBaseNormalFg = currentSchemes ["Base"].Normal.Foreground; - - Assert.Equal (Color.White.ToString (), currentBaseNormalFg.ToString ()); - - // Now Apply - Apply (); - - Assert.Equal ("Default", ThemeManager.Theme); - Assert.Equal (KeyCode.Z | KeyCode.AltMask, Application.GetDefaultKey (Command.Quit).KeyCode); - Assert.Equal (Alignment.End, MessageBox.DefaultButtonAlignment); - - Assert.Equal (Color.White.ToString (), currentBaseNormalFg.ToString ()); - - // Reset - ResetToHardCodedDefaults (); - - hardCodedSchemes = - GetHardCodedConfigPropertiesByScope ("ThemeScope")!.ToFrozenDictionary () ["Schemes"].PropertyValue as Dictionary; - hardCodedBaseNormalFg = hardCodedSchemes! ["Base"].Normal.Foreground; - Assert.Equal (Color.None.ToString (), hardCodedBaseNormalFg.ToString ()); - - FrozenDictionary hardCodedCache = GetHardCodedConfigPropertyCache ()!; - - Assert.Equal (hardCodedCache ["Theme"].PropertyValue, ThemeManager.Theme); - Dictionary hardCodedBindings = (Dictionary)hardCodedCache ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (hardCodedBindings [Command.Quit].GetCurrentPlatformKeys ().First (), Application.GetDefaultKey (Command.Quit)); - - // Themes - Assert.Equal (hardCodedCache ["MessageBox.DefaultButtonAlignment"].PropertyValue, MessageBox.DefaultButtonAlignment); - - Assert.Equal (GetHardCodedConfigPropertyCache ()! ["MessageBox.DefaultButtonAlignment"].PropertyValue, MessageBox.DefaultButtonAlignment); - - // Schemes - currentSchemes = SchemeManager.GetSchemes ()!; - currentBaseNormalFg = currentSchemes ["Base"].Normal.Foreground; - Assert.Equal (hardCodedBaseNormalFg.ToString (), currentBaseNormalFg.ToString ()); - - Scheme baseScheme = SchemeManager.GetScheme ("Base"); - - Attribute attr = baseScheme.Normal; - - // Use ToString so Assert.Equal shows the actual vs expected values on failure - Assert.Equal (hardCodedBaseNormalFg.ToString (), attr.Foreground.ToString ()); - } - finally - { - output.WriteLine ("Disabling CM to clean up."); - - Disable (true); - } - } - - [Fact (Skip = "ResetToCurrentValues corrupts hard coded cache")] - public void ResetToCurrentValues_Enabled_Resets () - { - Assert.False (IsEnabled); - - try - { - // Act - Enable (ConfigLocations.HardCoded); - - Application.DefaultKeyBindings! [Command.Quit] = Bind.All (Key.A); - - UpdateToCurrentValues (); - - Dictionary bindings9 = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.A, bindings9 [Command.Quit].GetCurrentPlatformKeys ().First ()); - Assert.NotNull (Settings); - Assert.NotNull (AppSettings); - Assert.NotNull (ThemeManager.Themes); - - // Default Theme should be "Default" - Assert.Single (ThemeManager.Themes); - Assert.Equal (ThemeManager.DEFAULT_THEME_NAME, ThemeManager.Theme); - } - finally - { - Disable (true); - } - } - - [Fact] - public void ConfigurationManager_DefaultPrecedence_IsRespected () - { - Assert.False (IsEnabled); - - try - { - // arrange - Enable (ConfigLocations.HardCoded); - - ThrowOnJsonErrors = true; - - // Setup multiple configurations with the same setting - // with different precedence levels - RuntimeConfig = """ - { - "Application.DefaultKeyBindings": { "Quit": { "All": ["Alt+Q"] } } - } - """; - - var defaultConfig = """ - { - "Application.DefaultKeyBindings": { "Quit": { "All": ["Ctrl+X"] } } - } - """; - - // Update default config first (lower precedence) - ConfigurationManager.SourcesManager?.Load (Settings, defaultConfig, "default-test", ConfigLocations.LibraryResources); - - // Then load runtime config, which should override default - Load (ConfigLocations.Runtime); - - // Assert - the runtime config should win due to precedence - Dictionary precedenceBindings = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Q.WithAlt, precedenceBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - } - finally - { - Disable (true); - } - } - - /// Save the `config.json` file; this can be used to update the file in `Terminal.Gui.Resources.config.json'. - /// - /// IMPORTANT: For the file generated to be valid, this must be the ONLY test run. Config Properties are all - /// static and thus can be overwritten by other tests. - /// - [Fact] - public void Save_HardCodedDefaults_To_config_json () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - - // Get the hard coded settings - ResetToHardCodedDefaults (); - - // Serialize to a JSON string - string json = ConfigurationManager.SourcesManager?.ToJson (Settings); - - // Write the JSON string to the file - File.WriteAllText ("hard_coded_defaults_config.json", json); - - // Verify the file was created - Assert.True (File.Exists ("hard_coded_defaults_config.json"), "Failed to create config.json file"); - } - finally - { - Disable (true); - } - } - - /// Save the `config.json` file; this can be used to update the file in `Terminal.Gui.Resources.config.json'. - /// - /// IMPORTANT: For the file generated to be valid, this must be the ONLY test run. Config Properties are all - /// static and thus can be overwritten by other tests. - /// - [Fact] - public void Save_Library_Defaults_To_config_json () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.LibraryResources); - - // Serialize to a JSON string - string json = ConfigurationManager.SourcesManager?.ToJson (Settings); - - // Write the JSON string to the file - File.WriteAllText ("library_defaults_config.json", json); - - // Verify the file was created - Assert.True (File.Exists ("library_defaults_config.json"), "Failed to create config.json file"); - } - finally - { - Disable (true); - } - } - - [Fact] - public void Save_HardCodedDefaults_ToJson_Omits_Invalid_Null_ValueType_Entries_And_RoundTrips () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - ResetToHardCodedDefaults (); - - string json = ConfigurationManager.SourcesManager?.ToJson (Settings); - - Assert.NotNull (json); - Assert.DoesNotContain ("\"MessageBox.DefaultBorderStyle\": null", json); - Assert.DoesNotContain ("\"Button.DefaultShadow\": null", json); - - SettingsScope roundTripped = JsonSerializer.Deserialize (json, SerializerContext.SettingsScope); - - Assert.NotNull (roundTripped); - Assert.NotNull (roundTripped! ["Themes"].PropertyValue); - } - finally - { - Disable (true); - } - } - - [Fact] - public void Load_And_Save_AppSettings_With_Custom_Property_Converter_Works_In_Jit () - { - Assert.False (IsEnabled); - - try - { - CustomConvertedBoolean = false; - ThrowOnJsonErrors = true; - Enable (ConfigLocations.HardCoded); - - RuntimeConfig = """ - { - "AppSettings": { - "ConfigurationMangerTests.CustomConvertedBoolean": "yes" - } - } - """; - - Load (ConfigLocations.Runtime); - Apply (); - - Assert.True (CustomConvertedBoolean); - - string json = ConfigurationManager.SourcesManager?.ToJson (Settings); - - Assert.NotNull (json); - Assert.Contains ("\"ConfigurationMangerTests.CustomConvertedBoolean\": \"yes\"", json); - } - finally - { - Disable (true); - CustomConvertedBoolean = false; - } - } - - [Fact] - public void TestConfigProperties () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - - Assert.NotEmpty (Settings!); - - // test that all ConfigProperties have our attribute - Assert.All ( - Settings, - item => Assert.Contains ( - item.Value.PropertyInfo!.CustomAttributes, - a => a.AttributeType - == typeof (ConfigurationPropertyAttribute) - )); - -#pragma warning disable xUnit2030 - Assert.DoesNotContain ( - Settings, - cp => cp.Value.PropertyInfo!.GetCustomAttribute ( - typeof (ConfigurationPropertyAttribute) - ) - == null - ); -#pragma warning restore xUnit2030 - - // Application is a static class - PropertyInfo pi = typeof (Application).GetProperty ("DefaultKeyBindings"); - Assert.Equal (pi, Settings ["Application.DefaultKeyBindings"].PropertyInfo); - - // FrameView is not a static class and DefaultBorderStyle is Scope.Scheme - pi = typeof (FrameView).GetProperty ("DefaultBorderStyle"); - Assert.False (Settings.ContainsKey ("FrameView.DefaultBorderStyle")); - Assert.True (ThemeManager.GetCurrentTheme ().ContainsKey ("FrameView.DefaultBorderStyle")); - Assert.Equal (pi, ThemeManager.GetCurrentTheme () ["FrameView.DefaultBorderStyle"].PropertyInfo); - } - finally - { - Disable (true); - } - } - - [Fact] - public void Load_And_Apply_HardCoded () - { - Assert.False (IsEnabled); - - try - { - // Spot check by setting some of the config properties - Application.DefaultKeyBindings! [Command.Quit] = Bind.All (Key.X.WithCtrl); - FileDialog.MaxSearchResults = 1; - - Enable (ConfigLocations.HardCoded); - Load (ConfigLocations.HardCoded); - - // Spot check - Dictionary hcBindings = (Dictionary)Settings ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Esc, hcBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - Assert.Equal (10000, (int)Settings ["FileDialog.MaxSearchResults"].PropertyValue!); - - Assert.Single (ThemeManager.Themes!); - Assert.Equal ("Default", ThemeManager.Theme); - Assert.NotEmpty (ThemeManager.Themes [ThemeManager.Theme]); - - // Verify schemes are properly initialized - Assert.NotNull (SchemeManager.GetSchemes ()); - Assert.NotEmpty (SchemeManager.GetSchemes ()); - - // Verify "Base" has correct values - //Assert.Equal (Color.White, SchemeManager.GetSchemes () ["Base"]!.Normal.Foreground); - //Assert.Equal (Color.Blue, SchemeManager.GetSchemes () ["Base"].Normal.Background); - - Apply (); - Assert.Equal (Key.Esc, Application.GetDefaultKey (Command.Quit)); - Assert.Equal (10000, FileDialog.MaxSearchResults); - } - finally - { - Disable (true); - } - } - - [Fact] - public void Load_And_Apply_LibraryResources () - { - Assert.False (IsEnabled); - - try - { - // Spot check by setting some of the config properties - Application.DefaultKeyBindings! [Command.Quit] = Bind.All (Key.X.WithCtrl); - FileDialog.MaxSearchResults = 1; - - Enable (ConfigLocations.HardCoded); - Load (ConfigLocations.LibraryResources); - - // Spot check - Dictionary lrBindings = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Esc, lrBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - Assert.Equal (10000, (int)Settings ["FileDialog.MaxSearchResults"].PropertyValue!); - - Assert.NotEmpty (ThemeManager.Themes!); - Assert.Equal ("Default", ThemeManager.Theme); - Assert.NotEmpty (ThemeManager.Themes [ThemeManager.Theme]); - - // Verify schemes are properly initialized - Assert.NotNull (SchemeManager.GetSchemes ()); - Assert.NotEmpty (SchemeManager.GetSchemes ()); - - // This is too fragile as the default scheme may change - // Verify "Base" has correct values - //Assert.Equal (Color.White, SchemeManager.Schemes ["Base"]!.Normal.Foreground); - //Assert.Equal (Color.Blue, SchemeManager.Schemes ["Base"].Normal.Background); - - Apply (); - Assert.Equal (Key.Esc, Application.GetDefaultKey (Command.Quit)); - Assert.Equal (10000, FileDialog.MaxSearchResults); - } - finally - { - Disable (true); - } - } - - [Fact] - public void Load_And_Apply_RuntimeConfig () - { - Assert.False (IsEnabled); - - try - { - // Spot check by setting some of the config properties - Application.DefaultKeyBindings! [Command.Quit] = Bind.All (Key.X.WithCtrl); - FileDialog.MaxSearchResults = 1; - Glyphs.Apple = new ('z'); - - ThrowOnJsonErrors = true; - Enable (ConfigLocations.HardCoded); - - RuntimeConfig = """ - { - "Application.DefaultKeyBindings": { "Quit": { "All": ["Alt-Q"] } }, - "FileDialog.MaxSearchResults":9, - "Themes" : [ - { - "Default" : { - "Glyphs.Apple": "a" - } - } - ] - } - """; - Load (ConfigLocations.Runtime); - - Dictionary rtBindings = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (Key.Q.WithAlt, rtBindings [Command.Quit].GetCurrentPlatformKeys ().First ()); - Assert.Equal (9, (int)Settings ["FileDialog.MaxSearchResults"].PropertyValue!); - Assert.Equal (new Rune ('a'), ThemeManager.GetCurrentTheme () ["Glyphs.Apple"].PropertyValue); - - Apply (); - Assert.Equal (Key.Q.WithAlt, Application.GetDefaultKey (Command.Quit)); - Assert.Equal (9, FileDialog.MaxSearchResults); - Assert.Equal (new ('a'), Glyphs.Apple); - } - finally - { - Disable (true); - } - } - - [Fact] - public void InvalidJsonLogs () - { - Assert.False (IsEnabled); - - Enable (ConfigLocations.HardCoded); - - ThrowOnJsonErrors = false; - - // "brown" is not a color - var json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""hotNormal"": { - ""foreground"": ""brown"", - ""background"": ""1234"" - } - } - } - ] - } - } - } - }"; - - ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime); - - // AbNormal is not a Scheme attribute - json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""AbNormal"": { - ""foreground"": ""green"", - ""background"": ""black"" - } - } - } - ] - } - } - } - }"; - - ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime); - - // Modify hotNormal background only - json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""hotNormal"": { - ""background"": ""cyan"" - } - } - } - ] - } - } - } - }"; - - ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime); - - ConfigurationManager.SourcesManager?.Load (Settings, "{}}", "test", ConfigLocations.Runtime); - - Assert.NotEqual (0, _jsonErrors.Length); - - ThrowOnJsonErrors = false; - - Disable (true); - } - - [Fact] - public void InvalidJsonThrows () - { - Assert.False (IsEnabled); - Enable (ConfigLocations.HardCoded); - - ThrowOnJsonErrors = true; - - // "yellow" is not a color - var json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""hotNormal"": { - ""foreground"": ""brownish"", - ""background"": ""1234"" - } - } - } - ] - } - } - ] - }"; - - var jsonException = Assert.Throws (() => ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime)); - Assert.StartsWith ("foreground: \"\"brownish\"\"", jsonException.Message); - - // AbNormal is not a Scheme attribute - json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""AbNormal"": { - ""foreground"": ""green"", - ""background"": ""black"" - } - } - } - ] - } - } - ] - }"; - - jsonException = Assert.Throws (() => ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime)); - Assert.StartsWith ("AbNormal:", jsonException.Message); - - // Modify hotNormal background only - json = @" - { - ""Themes"" : [ - { - ""Default"" : { - ""Schemes"": [ - { - ""UserDefined"": { - ""hotNormal"": { - ""background"": ""cyan"" - } - } - } - ] - } - } - ] - }"; - - jsonException = Assert.Throws (() => ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime)); - Assert.StartsWith ("background:", jsonException.Message); - - // Unknown property - json = @" - { - ""Unknown"" : ""Not known"" - }"; - - jsonException = Assert.Throws (() => ConfigurationManager.SourcesManager?.Load (Settings, json, "test", ConfigLocations.Runtime)); - Assert.StartsWith ("Unknown:", jsonException.Message); - - Assert.Equal (0, _jsonErrors.Length); - - ThrowOnJsonErrors = false; - - Disable (true); - } - - [Fact] - public void SourcesManager_Load_FromJson_Loads () - { - Assert.False (IsEnabled); - - try - { - Enable (ConfigLocations.HardCoded); - - // Arrange - var json = @" -{ - ""$schema"": ""https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json"", - ""Application.DefaultKeyBindings"": { ""Quit"": { ""All"": [""Alt-Z""] } }, - ""Theme"": ""Default"", - ""Themes"": [ - { - ""Default"": { - ""MessageBox.DefaultButtonAlignment"": ""End"", - ""Schemes"": [ - { - ""Accent"": { - ""Normal"": { - ""Foreground"": ""BrightGreen"", - ""Background"": ""Black"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""Cyan"" - }, - ""HotNormal"": { - ""Foreground"": ""Yellow"", - ""Background"": ""Black"" - }, - ""HotFocus"": { - ""Foreground"": ""Blue"", - ""Background"": ""Cyan"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""Black"" - } - } - }, - { - ""Base"": { - ""Normal"": { - ""Foreground"": ""White"", - ""Background"": ""Blue"" - }, - ""Focus"": { - ""Foreground"": ""Black"", - ""Background"": ""Gray"" - }, - ""HotNormal"": { - ""Foreground"": ""BrightCyan"", - ""Background"": ""Blue"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightBlue"", - ""Background"": ""Gray"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""Blue"" - } - } - }, - { - ""Dialog"": { - ""Normal"": { - ""Foreground"": ""Black"", - ""Background"": ""Gray"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""DarkGray"" - }, - ""HotNormal"": { - ""Foreground"": ""Blue"", - ""Background"": ""Gray"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""DarkGray"" - }, - ""Disabled"": { - ""Foreground"": ""Gray"", - ""Background"": ""DarkGray"" - } - } - }, - { - ""Menu"": { - ""Normal"": { - ""Foreground"": ""White"", - ""Background"": ""DarkGray"" - }, - ""Focus"": { - ""Foreground"": ""White"", - ""Background"": ""Black"" - }, - ""HotNormal"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""DarkGray"" - }, - ""HotFocus"": { - ""Foreground"": ""BrightYellow"", - ""Background"": ""Black"" - }, - ""Disabled"": { - ""Foreground"": ""Gray"", - ""Background"": ""DarkGray"" - } - } - }, - { - ""Error"": { - ""Normal"": { - ""Foreground"": ""Red"", - ""Background"": ""White"" - }, - ""Focus"": { - ""Foreground"": ""Black"", - ""Background"": ""BrightRed"" - }, - ""HotNormal"": { - ""Foreground"": ""Black"", - ""Background"": ""White"" - }, - ""HotFocus"": { - ""Foreground"": ""White"", - ""Background"": ""BrightRed"" - }, - ""Disabled"": { - ""Foreground"": ""DarkGray"", - ""Background"": ""White"" - } - } - } - ] - } - } - ] -} - "; - - //ResetToCurrentValues (); - ThrowOnJsonErrors = true; - - ConfigurationManager.SourcesManager?.Load (Settings, json, "UpdateFromJson", ConfigLocations.Runtime); - - Assert.Equal ("Default", ThemeManager.Theme); - - Assert.Equal (KeyCode.Esc, Application.GetDefaultKey (Command.Quit).KeyCode); - Dictionary smBindings = (Dictionary)Settings! ["Application.DefaultKeyBindings"].PropertyValue!; - Assert.Equal (KeyCode.Z | KeyCode.AltMask, smBindings [Command.Quit].GetCurrentPlatformKeys ().First ().KeyCode); - Assert.Equal (Alignment.Center, MessageBox.DefaultButtonAlignment); - - // Now Apply - Apply (); - - Assert.Equal ("Default", ThemeManager.Theme); - Assert.Equal (KeyCode.Z | KeyCode.AltMask, Application.GetDefaultKey (Command.Quit).KeyCode); - Assert.Equal (Alignment.End, MessageBox.DefaultButtonAlignment); - - Assert.Equal (Color.White, SchemeManager.GetSchemes ()! ["Base"].Normal.Foreground); - Assert.Equal (Color.Blue, SchemeManager.GetSchemes ()! ["Base"].Normal.Background); - } - finally - { - output.WriteLine ("Disabling CM to clean up."); - - Disable (true); - } - } - - [ConfigurationProperty (Scope = typeof (CMTestsScope))] - public static bool? TestProperty { get; set; } - - private class CMTestsScope : Scope - { } - - [Fact] - public void GetConfigPropertiesByScope_Gets () - { - IEnumerable> props = GetUninitializedConfigPropertiesByScope ("CMTestsScope"); - - Assert.NotNull (props); - Assert.NotEmpty (props); - } - - [Fact] - public void ConfigLocations_LoadOrder_IsCorrect () - { - Assert.False (IsEnabled); - - try - { - // Arrange - Enable (ConfigLocations.HardCoded); - ThrowOnJsonErrors = true; - - // Test that each location overrides the previous ones - // Priority order (lowest to highest): HardCoded → LibraryResources → AppResources → GlobalHome → GlobalCurrent → AppHome → AppCurrent → Env → Runtime - - // Start with HardCoded - Assert.Equal (10000, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load LibraryResources (should override HardCoded) - var libraryConfig = """ - { - "FileDialog.MaxSearchResults": 1 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, libraryConfig, "library-test", ConfigLocations.LibraryResources); - Assert.Equal (1, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load AppResources (should override LibraryResources) - var appResourcesConfig = """ - { - "FileDialog.MaxSearchResults": 2 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, appResourcesConfig, "appresources-test", ConfigLocations.AppResources); - Assert.Equal (2, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load GlobalHome (should override AppResources) - var globalHomeConfig = """ - { - "FileDialog.MaxSearchResults": 3 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, globalHomeConfig, "globalhome-test", ConfigLocations.GlobalHome); - Assert.Equal (3, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load GlobalCurrent (should override GlobalHome) - var globalCurrentConfig = """ - { - "FileDialog.MaxSearchResults": 4 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, globalCurrentConfig, "globalcurrent-test", ConfigLocations.GlobalCurrent); - Assert.Equal (4, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load AppHome (should override GlobalCurrent) - var appHomeConfig = """ - { - "FileDialog.MaxSearchResults": 5 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, appHomeConfig, "apphome-test", ConfigLocations.AppHome); - Assert.Equal (5, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load AppCurrent (should override AppHome) - var appCurrentConfig = """ - { - "FileDialog.MaxSearchResults": 6 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, appCurrentConfig, "appcurrent-test", ConfigLocations.AppCurrent); - Assert.Equal (6, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load Env (should override AppCurrent) - var envConfig = """ - { - "FileDialog.MaxSearchResults": 7 - } - """; - ConfigurationManager.SourcesManager?.Load (Settings, envConfig, "env-test", ConfigLocations.Env); - Assert.Equal (7, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Load Runtime (should override Env - highest priority) - RuntimeConfig = """ - { - "FileDialog.MaxSearchResults": 8 - } - """; - Load (ConfigLocations.Runtime); - Assert.Equal (8, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - } - finally - { - Disable (true); - } - } - - [Fact] - public void ConfigLocations_Env_LoadsFromEnvironmentVariable () - { - Assert.False (IsEnabled); - - try - { - // Arrange - Enable (ConfigLocations.HardCoded); - ThrowOnJsonErrors = true; - - // Set environment variable - Environment.SetEnvironmentVariable ( - SourcesManager.TUI_CONFIG_ENV_S, - """ - { - "FileDialog.MaxSearchResults": 42 - } - """); - - // Act - Load (ConfigLocations.Env); - - // Assert - Assert.Equal (42, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - } - finally - { - Environment.SetEnvironmentVariable (SourcesManager.TUI_CONFIG_ENV_S, null); - Disable (true); - } - } - - [Fact] - public void ConfigLocations_Runtime_HasHighestPriority () - { - Assert.False (IsEnabled); - - try - { - // Arrange - Enable (ConfigLocations.HardCoded); - ThrowOnJsonErrors = true; - - // Set Env config - Environment.SetEnvironmentVariable ( - SourcesManager.TUI_CONFIG_ENV_S, - """ - { - "FileDialog.MaxSearchResults": 7 - } - """); - - // Set Runtime config - RuntimeConfig = """ - { - "FileDialog.MaxSearchResults": 8 - } - """; - - // Act - Load both Env and Runtime - Load (ConfigLocations.Env | ConfigLocations.Runtime); - - // Assert - Runtime should win (highest priority) - Assert.Equal (8, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - } - finally - { - Environment.SetEnvironmentVariable (SourcesManager.TUI_CONFIG_ENV_S, null); - Disable (true); - } - } - - [Fact] - public void ConfigLocations_All_LoadsInCorrectOrder () - { - Assert.False (IsEnabled); - - try - { - // Arrange - Set up all possible configuration sources - Enable (ConfigLocations.HardCoded); - ThrowOnJsonErrors = true; - - // Set environment variable (second-highest priority) - Environment.SetEnvironmentVariable ( - SourcesManager.TUI_CONFIG_ENV_S, - """ - { - "FileDialog.MaxSearchResults": 7 - } - """); - - // Set runtime config (highest priority) - RuntimeConfig = """ - { - "FileDialog.MaxSearchResults": 8 - } - """; - - // Act - Load all locations - Load (ConfigLocations.All); - - // Assert - Runtime should win (highest priority) - Assert.Equal (8, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - - // Now test without Runtime - RuntimeConfig = null; - LoadHardCodedDefaults (); - - Environment.SetEnvironmentVariable ( - SourcesManager.TUI_CONFIG_ENV_S, - """ - { - "FileDialog.MaxSearchResults": 7 - } - """); - Load (ConfigLocations.Env); - - // Assert - Env should be used when Runtime is not set - Assert.Equal (7, (int)Settings! ["FileDialog.MaxSearchResults"].PropertyValue!); - } - finally - { - Environment.SetEnvironmentVariable (SourcesManager.TUI_CONFIG_ENV_S, null); - Disable (true); - } - } - - private sealed class YesNoBooleanJsonConverter : JsonConverter - { - public override bool? Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - string value = reader.GetString (); - - return value?.ToLowerInvariant () switch - { - "yes" => true, - "no" => false, - _ => throw new JsonException ($"Unexpected boolean token '{value}'.") - }; - } - - public override void Write (Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) - { - writer.WriteStringValue (value == true ? "yes" : "no"); - } - } -} diff --git a/Tests/UnitTests.NonParallelizable/Configuration/GlyphTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/GlyphTests.cs deleted file mode 100644 index 2cc49b3f5d..0000000000 --- a/Tests/UnitTests.NonParallelizable/Configuration/GlyphTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Reflection; -using System.Text; -using System.Text.Json; -using static Terminal.Gui.Configuration.ConfigurationManager; - -namespace UnitTests.NonParallelizable.ConfigurationTests; - -public class GlyphTests -{ - [Fact] - public void Apply_Applies_Over_Defaults () - { - try - { - // arrange - Enable (ConfigLocations.HardCoded); - - Assert.Equal ((Rune)'⟦', Glyphs.LeftBracket); - - var glyph = (Rune)ThemeManager.GetCurrentTheme () ["Glyphs.LeftBracket"].PropertyValue!; - Assert.Equal ((Rune)'⟦', glyph); - - ThrowOnJsonErrors = true; - - // act - RuntimeConfig = """ - { - "Themes": [ - { - "Default": - { - "Glyphs.LeftBracket": "[" - } - } - ] - } - """; - - Load (ConfigLocations.Runtime); - - Apply (); - - // assert - glyph = (Rune)ThemeManager.GetCurrentTheme () ["Glyphs.LeftBracket"].PropertyValue!; - Assert.Equal ((Rune)'[', glyph); - Assert.Equal ((Rune)'[', Glyphs.LeftBracket); - } - finally - { - // clean up - Disable (resetToHardCodedDefaults: true); - - } - } -} diff --git a/Tests/UnitTests.NonParallelizable/Configuration/SourcesManagerLoadNullJsonTests.cs b/Tests/UnitTests.NonParallelizable/Configuration/SourcesManagerLoadNullJsonTests.cs deleted file mode 100644 index c49ec14a21..0000000000 --- a/Tests/UnitTests.NonParallelizable/Configuration/SourcesManagerLoadNullJsonTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text; -using System.Text.Json; -using static Terminal.Gui.Configuration.ConfigurationManager; - -namespace UnitTests.NonParallelizable.ConfigurationTests; - -public class SourcesManagerLoadNullJsonTests -{ - [Fact] - public void Load_Stream_WithJsonNull_AndThrowOnJsonErrorsFalse_ReturnsFalse_AndAddsJsonError () - { - // Copilot - SourcesManager sourcesManager = new (); - SettingsScope settingsScope = new (); - string source = "Load_Stream_WithJsonNull_AndThrowOnJsonErrorsFalse_ReturnsFalse_AndAddsJsonError"; - ConfigLocations location = ConfigLocations.Runtime; - Stream stream = new MemoryStream (Encoding.UTF8.GetBytes ("null")); - - bool? originalThrowOnJsonErrors = ThrowOnJsonErrors; - string originalJsonErrors = _jsonErrors.ToString (); - - try - { - ThrowOnJsonErrors = false; - _jsonErrors.Clear (); - - bool result = sourcesManager.Load (settingsScope, stream, source, location); - - Assert.False (result); - Assert.Empty (sourcesManager.Sources); - Assert.Contains ($"Error reading {source}:", _jsonErrors.ToString ()); - } - finally - { - ThrowOnJsonErrors = originalThrowOnJsonErrors; - _jsonErrors.Clear (); - _jsonErrors.Append (originalJsonErrors); - } - } - - [Fact] - public void Load_Stream_WithJsonNull_AndThrowOnJsonErrorsTrue_ThrowsJsonException () - { - // Copilot - SourcesManager sourcesManager = new (); - SettingsScope settingsScope = new (); - string source = "Load_Stream_WithJsonNull_AndThrowOnJsonErrorsTrue_ThrowsJsonException"; - ConfigLocations location = ConfigLocations.Runtime; - Stream stream = new MemoryStream (Encoding.UTF8.GetBytes ("null")); - - bool? originalThrowOnJsonErrors = ThrowOnJsonErrors; - string originalJsonErrors = _jsonErrors.ToString (); - - try - { - ThrowOnJsonErrors = true; - _jsonErrors.Clear (); - - JsonException exception = Assert.Throws (() => sourcesManager.Load (settingsScope, stream, source, location)); - Assert.Contains ("null", exception.Message, StringComparison.OrdinalIgnoreCase); - Assert.Empty (sourcesManager.Sources); - Assert.Equal (0, _jsonErrors.Length); - } - finally - { - ThrowOnJsonErrors = originalThrowOnJsonErrors; - _jsonErrors.Clear (); - _jsonErrors.Append (originalJsonErrors); - } - } -} diff --git a/Tests/UnitTestsParallelizable/AppTestHelpers/AppTestHelperShutdownTests.cs b/Tests/UnitTestsParallelizable/AppTestHelpers/AppTestHelperShutdownTests.cs new file mode 100644 index 0000000000..ef44565102 --- /dev/null +++ b/Tests/UnitTestsParallelizable/AppTestHelpers/AppTestHelperShutdownTests.cs @@ -0,0 +1,24 @@ +using AppTestHelpers; + +namespace AppTestHelpersTests; + +public class AppTestHelperShutdownTests +{ + // CoPilot - Grok 4.6 + /// + /// Regression for the macOS IntegrationTests NRE on #5657: + /// WaitIteration used ExternalCancellationTokenSource! during Stop/Dispose. + /// CleanupApplication can null that source before Finished is set. + /// + [Fact] + public void WaitIteration_WhenExternalCancellationTokenSourceIsNull_DoesNotThrow () + { + using AppTestHelper helper = new (DriverRegistry.Names.DOTNET); + + helper.ClearExternalCancellationTokenSourceForTests (); + + Exception? ex = Record.Exception (() => helper.WaitIteration ()); + + Assert.Null (ex); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs index 5febabda5f..e60a1eb8ec 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs @@ -46,11 +46,146 @@ async Task TaskWithAsyncContinuation () } [Fact] - public void Dispose_Resets_SyncContext () + public void Dispose_LeavesForeignAmbientSyncContext () + { + SynchronizationContext? previous = SynchronizationContext.Current; + SynchronizationContext marker = new (); + + try + { + SynchronizationContext.SetSynchronizationContext (marker); + + IApplication app = Application.Create (); + app.Dispose (); + + Assert.Same (marker, SynchronizationContext.Current); + } + finally + { + SynchronizationContext.SetSynchronizationContext (previous); + } + } + + [Fact] + public void Init_Posts_To_The_App_Main_Thread () { IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + int mainThreadId = app.MainThreadId ?? Thread.CurrentThread.ManagedThreadId; + int? callbackThreadId = null; + ManualResetEventSlim callbackCalled = new (false); + + // As of #5636 Init no longer installs the app context as the thread's ambient context; + // posting to the app's own context before Run still lands on the main thread once the loop runs. + ((ApplicationImpl)app).SynchronizationContext!.Post (_ => + { + callbackThreadId = Thread.CurrentThread.ManagedThreadId; + callbackCalled.Set (); + }, + null); + + app.AddTimeout (TimeSpan.FromMilliseconds (100), + () => + { + app.RequestStop (); + + return false; + }); + + app.Run (new Window ()); + + Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken)); + Assert.Equal (mainThreadId, callbackThreadId); + app.Dispose (); - Assert.Null (SynchronizationContext.Current); + } + + [Fact] + public void Init_Posts_To_The_App_Instance_Main_Thread () + { + IApplication app1 = Application.Create (); + app1.Init (DriverRegistry.Names.ANSI); + + IApplication app2 = Application.Create (); + app2.Init (DriverRegistry.Names.ANSI); + + int mainThreadId1 = app1.MainThreadId ?? Thread.CurrentThread.ManagedThreadId; + int? callbackThreadId1 = null; + SynchronizationContext? synchronizationContext1 = null; + + ManualResetEventSlim callbackCalled = new (false); + + app1.AddTimeout (TimeSpan.FromMilliseconds (100), + () => + { + if (synchronizationContext1 is null) + { + synchronizationContext1 = SynchronizationContext.Current; + synchronizationContext1?.Post (_ => + { + callbackThreadId1 = Thread.CurrentThread.ManagedThreadId; + callbackCalled.Set (); + }, + null); + + return true; + } + else + { + app1.RequestStop (); + + return false; + } + }); + + app1.Run (new Window ()); + + Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken)); + Assert.Equal (mainThreadId1, callbackThreadId1); + + // The ambient context observed during app1's run is app1's own context; Run restores the + // caller's ambient context on exit (#5636), so compare against the app instance's context. + Assert.Same (((ApplicationImpl)app1).SynchronizationContext, synchronizationContext1); + + callbackCalled.Reset (); + + int mainThreadId2 = app2.MainThreadId ?? Thread.CurrentThread.ManagedThreadId; + int? callbackThreadId2 = null; + SynchronizationContext? synchronizationContext2 = null; + + app2.AddTimeout (TimeSpan.FromMilliseconds (100), + () => + { + if (synchronizationContext2 is null) + { + synchronizationContext2 = SynchronizationContext.Current; + synchronizationContext2?.Post (_ => + { + callbackThreadId2 = Thread.CurrentThread.ManagedThreadId; + callbackCalled.Set (); + }, + null); + + return true; + } + else + { + app2.RequestStop (); + + return false; + } + }); + + app2.Run (new Window ()); + + Assert.True (callbackCalled.Wait (TimeSpan.FromMilliseconds (200), TestContext.Current.CancellationToken)); + Assert.Equal (mainThreadId2, callbackThreadId2); + Assert.Same (((ApplicationImpl)app2).SynchronizationContext, synchronizationContext2); + Assert.NotSame (synchronizationContext1, synchronizationContext2); + + app1.Dispose (); + app2.Dispose (); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Application/Keyboard/ApplicationDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Application/Keyboard/ApplicationDefaultKeyBindingsTests.cs index d84c1fe29f..19f2dd26ae 100644 --- a/Tests/UnitTestsParallelizable/Application/Keyboard/ApplicationDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Keyboard/ApplicationDefaultKeyBindingsTests.cs @@ -65,8 +65,8 @@ public void Application_DefaultKeyBindings_HasConfigurationPropertyAttribute () Assert.NotNull (propertyInfo); - var attr = propertyInfo.GetCustomAttribute (); - Assert.NotNull (attr); + System.Attribute? attr = propertyInfo.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); + Assert.Null (attr); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Application/Keyboard/KeyboardTests.cs b/Tests/UnitTestsParallelizable/Application/Keyboard/KeyboardTests.cs index e6b2e29b00..b17584674e 100644 --- a/Tests/UnitTestsParallelizable/Application/Keyboard/KeyboardTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Keyboard/KeyboardTests.cs @@ -394,7 +394,7 @@ public void InvokeCommandsBoundToKey_Suspend_ReturnsNotNull () bool? result = keyboard.InvokeCommandsBoundToKey (key); // Assert — Suspend is NonWindows only, so on Windows this key is unbound - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (OperatingSystem.IsWindows ()) { Assert.Null (result); } diff --git a/Tests/UnitTestsParallelizable/Application/Popover/Application.PopoverTests.cs b/Tests/UnitTestsParallelizable/Application/Popover/Application.PopoverTests.cs index c3a0d5de47..fccb67d82f 100644 --- a/Tests/UnitTestsParallelizable/Application/Popover/Application.PopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Popover/Application.PopoverTests.cs @@ -35,6 +35,145 @@ public void DeRegister_RemovesPopover () Assert.DoesNotContain (popover, popoverManager.Popovers); } + // CoPilot - Grok 4.6 + /// + /// Regression for https://github.com/tui-cs/Terminal.Gui/issues/5635. + /// DeRegister of the active popover must Hide it so the covered region is invalidated. + /// + [Fact] + public void DeRegister_ActivePopover_HidesAndInvalidatesTopRunnable () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (40, 15); + + using Runnable top = new (); + SessionToken? token = app.Begin (top); + + PopoverTestClass popover = new () { App = app, Width = 10, Height = 3 }; + app.Popovers!.Register (popover); + app.Popovers.Show (popover); + app.LayoutAndDraw (); + + Assert.Equal (popover, app.Popovers.GetActivePopover ()); + Assert.True (popover.Visible); + Assert.False (top.NeedsDraw); + + bool result = app.Popovers.DeRegister (popover); + + Assert.True (result); + Assert.DoesNotContain (popover, app.Popovers.Popovers); + Assert.Null (app.Popovers.GetActivePopover ()); + Assert.False (popover.Visible); + Assert.True (top.NeedsDraw); + + app.End (token!); + } + + // CoPilot - Grok 4.6 + /// + /// If Hide raises VisibleChanged and a handler calls Show, DeRegister must not + /// leave that popover active after removing it from the registry. + /// + [Fact] + public void DeRegister_ActivePopover_ReentrantShow_DoesNotLeaveUnregisteredActive () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (40, 15); + + using Runnable top = new (); + SessionToken? token = app.Begin (top); + + PopoverTestClass popover = new () { App = app, Width = 10, Height = 3 }; + app.Popovers!.Register (popover); + app.Popovers.Show (popover); + + var reopened = false; + + popover.VisibleChanged += (_, _) => + { + if (popover.Visible || reopened) + { + return; + } + + if (!app.Popovers.IsRegistered (popover)) + { + return; + } + + reopened = true; + app.Popovers.Show (popover); + }; + + bool result = app.Popovers.DeRegister (popover); + + Assert.True (result); + Assert.False (reopened); + Assert.DoesNotContain (popover, app.Popovers.Popovers); + Assert.Null (app.Popovers.GetActivePopover ()); + Assert.False (popover.Visible); + + app.End (token!); + } + + // CoPilot - Grok 4.6 + /// + /// Codex P2 on #5657: Hide runs while the popover is still registered, so a + /// VisibleChanged handler can call Show and restore Visible. After DeRegister + /// the popover must be unregistered, not active, and not visible. + /// + [Fact] + public void DeRegister_VisibleChangedShow_LeavesUnregisteredInactiveAndHidden () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (40, 15); + + using Runnable top = new (); + SessionToken? token = app.Begin (top); + app.LayoutAndDraw (); + + PopoverTestClass popover = new () { App = app, Width = 10, Height = 3 }; + app.Popovers!.Register (popover); + app.Popovers.Show (popover); + app.LayoutAndDraw (); + + var registeredDuringHide = false; + var reopened = false; + + popover.VisibleChanged += (_, _) => + { + if (popover.Visible) + { + return; + } + + registeredDuringHide = app.Popovers.IsRegistered (popover); + + if (!registeredDuringHide) + { + return; + } + + reopened = true; + app.Popovers.Show (popover); + }; + + bool result = app.Popovers.DeRegister (popover); + + Assert.True (result); + Assert.False (registeredDuringHide, "DeRegister must unregister before Hide so Show cannot run during VisibleChanged."); + Assert.False (reopened); + Assert.False (app.Popovers.IsRegistered (popover)); + Assert.Null (app.Popovers.GetActivePopover ()); + Assert.False (popover.Visible); + Assert.True (top.NeedsDraw); + + app.End (token!); + } + [Fact] public void Show_SetsActivePopover () { diff --git a/Tests/UnitTestsParallelizable/Application/RunAsyncTests.cs b/Tests/UnitTestsParallelizable/Application/RunAsyncTests.cs index 74f2e6a84c..6830d13b30 100644 --- a/Tests/UnitTestsParallelizable/Application/RunAsyncTests.cs +++ b/Tests/UnitTestsParallelizable/Application/RunAsyncTests.cs @@ -96,6 +96,38 @@ void OnIteration (object? s, EventArgs a) app.Dispose (); } + // Claude - Fable 5 + // Repro for #5636: an await between Init and RunAsync must not deadlock. Pre-fix, Init + // installed the app's MainLoopSyncContext as the thread's ambient context; the await's + // continuation was posted to the not-yet-running main loop, so RunAsync was never reached. + [Fact] + public async Task RunAsync_AfterAwaitFollowingInit_DoesNotDeadlock () + { + Task scenario = Task.Run (async () => + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + // Resumes via the thread's ambient SynchronizationContext when one is installed. + await Task.Yield (); + + Runnable runnable = new (); + using CancellationTokenSource cts = new (); + + void OnIteration (object? s, EventArgs a) => cts.Cancel (); + + app.Iteration += OnIteration; + await app.RunAsync (runnable, cts.Token); + app.Iteration -= OnIteration; + + runnable.Dispose (); + app.Dispose (); + }); + + // Fails with a timeout when the continuation after Task.Yield is stranded. + await scenario.WaitAsync (TimeSpan.FromSeconds (10), TestContext.Current.CancellationToken); + } + [Fact] public async Task RunAsync_UnhandledException_FaultedTask () { diff --git a/Tests/UnitTestsParallelizable/Application/SyncContextLifecycleTests.cs b/Tests/UnitTestsParallelizable/Application/SyncContextLifecycleTests.cs new file mode 100644 index 0000000000..adc3cd059f --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/SyncContextLifecycleTests.cs @@ -0,0 +1,184 @@ +// Claude - Fable 5 +#nullable enable + +namespace ApplicationTests; + +/// +/// Tests for the ambient contract across the application +/// lifecycle (#5636): must not install the app's main-loop context +/// as the thread's ambient context (an await before Run would capture a context whose +/// continuations only run once the loop is pumping — a startup deadlock). The app context is +/// ambient only while a session is running, and the caller's context is restored afterwards. +/// +[Collection ("Application Tests")] +public class SyncContextLifecycleTests +{ + [Fact] + public void Init_DoesNotChangeAmbientSynchronizationContext () + { + SynchronizationContext? previous = SynchronizationContext.Current; + SynchronizationContext marker = new (); + + try + { + SynchronizationContext.SetSynchronizationContext (marker); + + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + Assert.Same (marker, SynchronizationContext.Current); + + app.Dispose (); + + Assert.Same (marker, SynchronizationContext.Current); + } + finally + { + SynchronizationContext.SetSynchronizationContext (previous); + } + } + + [Fact] + public void Run_SetsAppSyncContextDuringRun_AndRestoresAmbientAfter () + { + SynchronizationContext? previous = SynchronizationContext.Current; + SynchronizationContext marker = new (); + + try + { + SynchronizationContext.SetSynchronizationContext (marker); + + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + SynchronizationContext? duringRun = null; + + void OnIteration (object? s, EventArgs a) + { + duringRun = SynchronizationContext.Current; + app.RequestStop (); + } + + app.Iteration += OnIteration; + + using (Runnable runnable = new ()) + { + app.Run (runnable); + } + + app.Iteration -= OnIteration; + + Assert.Same (((ApplicationImpl)app).SynchronizationContext, duringRun); + Assert.Same (marker, SynchronizationContext.Current); + + app.Dispose (); + } + finally + { + SynchronizationContext.SetSynchronizationContext (previous); + } + } + + // The documented Begin/End building-block sequence must restore the caller's ambient context + // just like Run does — including nested sessions, where only the outermost End restores it. + [Fact] + public void Begin_End_RestoresAmbientSyncContext_IncludingNestedSessions () + { + SynchronizationContext? previous = SynchronizationContext.Current; + SynchronizationContext marker = new (); + + try + { + SynchronizationContext.SetSynchronizationContext (marker); + + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + SynchronizationContext appContext = ((ApplicationImpl)app).SynchronizationContext!; + + using Runnable outer = new (); + using Runnable inner = new (); + + SessionToken outerToken = app.Begin (outer)!; + Assert.Same (appContext, SynchronizationContext.Current); + + SessionToken innerToken = app.Begin (inner)!; + Assert.Same (appContext, SynchronizationContext.Current); + + app.End (innerToken); + + // The outer session is still active; the app context stays ambient. + Assert.Same (appContext, SynchronizationContext.Current); + + app.End (outerToken); + + Assert.Same (marker, SynchronizationContext.Current); + + app.Dispose (); + } + finally + { + SynchronizationContext.SetSynchronizationContext (previous); + } + } + + // A continuation that captured the app context during a session can resume after the session + // ends; with no loop pumping (and the app still Initialized), it must not be stranded. + [Fact] + public void Post_AfterSessionEnded_StillExecutesCallback () + { + IApplication app = Application.Create (); + + try + { + app.Init (DriverRegistry.Names.ANSI); + + void OnIteration (object? s, EventArgs a) => app.RequestStop (); + + app.Iteration += OnIteration; + + using (Runnable runnable = new ()) + { + app.Run (runnable); + } + + app.Iteration -= OnIteration; + + ApplicationImpl impl = (ApplicationImpl)app; + + // After the session ends the loop is no longer a reliable pump; Post must + // fall back to the thread pool (#5636). Ubuntu CI failed this at 2s under + // parallel load. The pool can take longer than a tight Wait to inject a worker. + Assert.False (impl.CanPumpPostedWork); + + SynchronizationContext context = impl.SynchronizationContext!; + + using ManualResetEventSlim callbackCalled = new (false); + context.Post (_ => callbackCalled.Set (), null); + + Assert.True ( + callbackCalled.Wait (TimeSpan.FromSeconds (10), TestContext.Current.CancellationToken), + "Post after session end must run on the thread pool, not wait for a loop that is no longer pumping."); + } + finally + { + app.Dispose (); + } + } + + [Fact] + public void Post_AfterDispose_StillExecutesCallback () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + SynchronizationContext context = ((ApplicationImpl)app).SynchronizationContext!; + app.Dispose (); + + // No main loop can ever pump this; the callback must not be stranded (or throw). + using ManualResetEventSlim callbackCalled = new (false); + context.Post (_ => callbackCalled.Set (), null); + + Assert.True ( + callbackCalled.Wait (TimeSpan.FromSeconds (10), TestContext.Current.CancellationToken), + "Post after Dispose must run on the thread pool."); + } +} diff --git a/Tests/UnitTestsParallelizable/AssemblyInfo.cs b/Tests/UnitTestsParallelizable/AssemblyInfo.cs index 9ec321c5cf..964cd4a96e 100644 --- a/Tests/UnitTestsParallelizable/AssemblyInfo.cs +++ b/Tests/UnitTestsParallelizable/AssemblyInfo.cs @@ -1,6 +1,5 @@ -global using Attribute = Terminal.Gui.Drawing.Attribute; +global using Attribute = Terminal.Gui.Drawing.Attribute; global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; global using Terminal.Gui.Time; global using Terminal.Gui.Testing; diff --git a/Tests/UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs b/Tests/UnitTestsParallelizable/Configuration/AppSettingsTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs rename to Tests/UnitTestsParallelizable/Configuration/AppSettingsTests.cs index c11e2b96f6..07382b817a 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/AppSettingsTests.cs @@ -6,7 +6,7 @@ namespace ConfigurationTests; /// Tests for the app-developer workflow. [Collection ("StaticSettingsTests")] -public class MecAppSettingsTests +public class AppSettingsTests { /// Example app settings POCO for testing. private class SampleAppSettings diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigErrorHandlingTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigErrorHandlingTests.cs new file mode 100644 index 0000000000..792cfc25a8 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/ConfigErrorHandlingTests.cs @@ -0,0 +1,164 @@ +// Claude - Fable 5 +using Microsoft.Extensions.Configuration; +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// Malformed or legacy-shaped configuration sources must never crash the app (the build runs from a +/// module initializer). Errors are collected in and printed at shutdown; +/// valid sources still apply. +/// +[Collection ("StaticSettingsTests")] +public class ConfigErrorHandlingTests +{ + [Fact] + public void Build_MalformedRuntimeConfig_DoesNotThrow_AndCollectsError () + { + TuiJsonErrors.Print (); + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = "{ this is not json"; + + IConfiguration config = tuiBuilder.Configuration; + + Assert.NotNull (config); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("RuntimeConfig")); + } + + [Fact] + public void ApplyToStaticFacades_MalformedRuntimeConfig_DoesNotThrow_OtherSourcesStillApply () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiJsonErrors.Print (); + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = "{ this is not json"; + + tuiBuilder.ApplyToStaticFacades (); + + // The library's embedded config.json still applied. + Assert.True (tuiBuilder.Configuration.GetSection ("Themes").Exists ()); + } + + [Fact] + public void AddTuiRuntimeConfig_LegacyShape_IsSkipped () + { + IConfiguration config = new ConfigurationBuilder () + .AddTuiRuntimeConfig ("""{ "Button.DefaultShadow": "None" }""") + .Build (); + + Assert.Null (config ["Button.DefaultShadow"]); + } + + [Fact] + public void AddTuiInlineJson_Malformed_IsSkipped_AndCollectsError () + { + TuiJsonErrors.Print (); + + IConfigurationBuilder builder = new ConfigurationBuilder (); + TuiConfigurationExtensions.AddTuiInlineJson (builder, "{ this is not json", "TUI_CONFIG"); + IConfiguration config = builder.Build (); + + Assert.Empty (config.GetChildren ()); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("TUI_CONFIG")); + } + + [Fact] + public void AddTuiInlineJson_LegacyShape_IsSkipped_AndWarns () + { + IConfigurationBuilder builder = new ConfigurationBuilder (); + TuiConfigurationExtensions.AddTuiInlineJson (builder, """{ "Themes": [ { "Dark": {} } ] }""", "TUI_CONFIG"); + IConfiguration config = builder.Build (); + + Assert.Empty (config.GetChildren ()); + } + + // Claude - Fable 5 + // A non-object root parses as valid JSON but would throw inside builder.Build(), past every + // per-source handler — discarding ALL sources. It must be skipped at add time instead. + [Fact] + public void AddTuiInlineJson_NonObjectRoot_IsSkipped_AndCollectsError () + { + TuiJsonErrors.Print (); + + IConfigurationBuilder builder = new ConfigurationBuilder (); + TuiConfigurationExtensions.AddTuiInlineJson (builder, "[]", "TUI_CONFIG"); + IConfiguration config = builder.Build (); + + Assert.Empty (config.GetChildren ()); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("TUI_CONFIG")); + } + + // Claude - Fable 5 + // Duplicate top-level keys would throw inside MEC's parser at builder.Build(), past every + // per-source handler; the source must be skipped at add time instead. + [Fact] + public void AddTuiInlineJson_DuplicateKeys_IsSkipped_AndCollectsError () + { + TuiJsonErrors.Print (); + + IConfigurationBuilder builder = new ConfigurationBuilder (); + TuiConfigurationExtensions.AddTuiInlineJson (builder, """{ "Theme": "A", "Theme": "B" }""", "TUI_CONFIG"); + IConfiguration config = builder.Build (); + + Assert.Empty (config.GetChildren ()); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("TUI_CONFIG")); + } + + // Claude - Fable 5 + [Fact] + public void ApplyToStaticFacades_NonObjectRuntimeConfig_OtherSourcesStillApply () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiJsonErrors.Print (); + TuiConfigurationBuilder tuiBuilder = new (null, null, includeUserSources: false); + tuiBuilder.RuntimeConfig = "[]"; + + tuiBuilder.ApplyToStaticFacades (); + + // The library's embedded config.json still applied. + Assert.True (tuiBuilder.Configuration.GetSection ("Themes").Exists ()); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("RuntimeConfig")); + } + + // Claude - Fable 5 + // Each build clears previously collected errors, so a watcher re-applying a persistently + // malformed source reports it once per effective configuration, not once per rebuild. + [Fact] + public void Reload_PersistentlyMalformedSource_DoesNotAccumulateDuplicateErrors () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiJsonErrors.Print (); + TuiConfigurationBuilder tuiBuilder = new (null, null, includeUserSources: false); + tuiBuilder.RuntimeConfig = "{ this is not json"; + + _ = tuiBuilder.Configuration; + tuiBuilder.Reload (); + _ = tuiBuilder.Configuration; + tuiBuilder.Reload (); + _ = tuiBuilder.Configuration; + + Assert.Equal (1, TuiJsonErrors.GetErrors ().Count (e => e.Contains ("RuntimeConfig"))); + } + + // A theme whose name is numeric (e.g. "2077") is a legitimate nested MEC theme. Legacy array + // shapes can no longer leak numeric section keys in, so numeric names must not be filtered out. + [Fact] + public void ThemeNames_NumericThemeName_IsListedAndSwitchable () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Themes": { "2077": { "Button": { "DefaultShadow": "None" } } } + } + """; + tuiBuilder.ApplyToStaticFacades (); + + MecThemeManager manager = new (tuiBuilder); + + Assert.Contains ("2077", manager.ThemeNames); + Assert.True (manager.SwitchTheme ("2077")); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigJsonMigratorTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigJsonMigratorTests.cs new file mode 100644 index 0000000000..019d8aeb83 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/ConfigJsonMigratorTests.cs @@ -0,0 +1,136 @@ +// Grok - grok-4.6 +using System.Text.Json.Nodes; +using Terminal.Gui.Tools.MigrateConfig; + +namespace ConfigurationTests; + +/// Tests for the standalone config.json migrator used by Tools/MigrateConfig. +public class ConfigJsonMigratorTests +{ + [Fact] + public void MigrateObject_EmptyThemesArray_BecomesEmptyDictionary () + { + JsonObject src = JsonNode.Parse ("""{ "Themes": [] }""")!.AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.IsType (migrated ["Themes"]); + Assert.Empty (migrated ["Themes"]!.AsObject ()); + } + + [Fact] + public void MigrateObject_EmptySchemesArray_BecomesEmptyDictionary () + { + JsonObject src = JsonNode.Parse ("""{ "Schemes": [] }""")!.AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.IsType (migrated ["Schemes"]); + Assert.Empty (migrated ["Schemes"]!.AsObject ()); + } + + [Fact] + public void MigrateObject_ArrayThemes_BecomeDictionary () + { + JsonObject src = JsonNode.Parse ( + """ + { + "Themes": [ + { "Dark": { "Button.DefaultShadow": "None" } } + ] + } + """)! + .AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.Equal ("None", migrated ["Themes"]? ["Dark"]? ["Button"]? ["DefaultShadow"]?.GetValue ()); + } + + [Fact] + public void IsLegacyConfigShape_DottedTopLevelKey_IsTrue () + { + Assert.True (TuiConfigurationExtensions.IsLegacyConfigShape ("""{ "Button.DefaultShadow": "None" }""")); + } + + [Fact] + public void IsLegacyConfigShape_ArrayThemes_IsTrue () + { + Assert.True (TuiConfigurationExtensions.IsLegacyConfigShape ("""{ "Themes": [ { "Dark": {} } ] }""")); + } + + [Fact] + public void IsLegacyConfigShape_NestedMec_IsFalse () + { + Assert.False (TuiConfigurationExtensions.IsLegacyConfigShape ("""{ "Button": { "DefaultShadow": "None" }, "Themes": { "Dark": {} } }""")); + } + + // Claude - Fable 5 + [Fact] + public void MigrateObject_DottedKeyThenPlainSection_MergesBoth () + { + JsonObject src = JsonNode.Parse ( + """ + { + "Button.DefaultShadow": "Opaque", + "Button": { "DefaultMouseHighlightStates": "In" } + } + """)! + .AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.Equal ("Opaque", migrated ["Button"]? ["DefaultShadow"]?.GetValue ()); + Assert.Equal ("In", migrated ["Button"]? ["DefaultMouseHighlightStates"]?.GetValue ()); + } + + // Claude - Fable 5 + [Fact] + public void MigrateObject_PlainSectionThenDottedKey_MergesBoth () + { + JsonObject src = JsonNode.Parse ( + """ + { + "Button": { "DefaultMouseHighlightStates": "In" }, + "Button.DefaultShadow": "Opaque" + } + """)! + .AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.Equal ("Opaque", migrated ["Button"]? ["DefaultShadow"]?.GetValue ()); + Assert.Equal ("In", migrated ["Button"]? ["DefaultMouseHighlightStates"]?.GetValue ()); + } + + // Claude - Fable 5 + [Fact] + public void MigrateObject_TwoDottedKeysDeepPath_MergesBoth () + { + JsonObject src = JsonNode.Parse ( + """ + { + "FileDialogStyle.ColorProviderName": "Ext", + "FileDialogStyle.UseColors": true + } + """)! + .AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.Equal ("Ext", migrated ["FileDialogStyle"]? ["ColorProviderName"]?.GetValue ()); + Assert.True (migrated ["FileDialogStyle"]? ["UseColors"]?.GetValue ()); + } + + // Claude - Fable 5 + [Fact] + public void MigrateObject_NestedObjectsUnderSameKey_DeepMerges () + { + JsonObject src = JsonNode.Parse ( + """ + { + "Themes.Dark.Button": { "DefaultShadow": "None" }, + "Themes": { "Dark": { "Dialog": { "DefaultShadow": "Opaque" } } } + } + """)! + .AsObject (); + JsonObject migrated = ConfigJsonMigrator.MigrateObject (src); + + Assert.Equal ("None", migrated ["Themes"]? ["Dark"]? ["Button"]? ["DefaultShadow"]?.GetValue ()); + Assert.Equal ("Opaque", migrated ["Themes"]? ["Dark"]? ["Dialog"]? ["DefaultShadow"]?.GetValue ()); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs deleted file mode 100644 index 1c8d0afc43..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Claude - Opus 4.7 - -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace ConfigurationTests; - -public class ConfigPropertyHostTypesTests -{ - /// - /// Guard-rail test: the set on GetTypes must exactly match the - /// _types array it returns. The attributes are what the trimmer actually reads to preserve members; if - /// someone adds a type to the array but forgets the matching attribute (or vice-versa), AOT builds would silently - /// lose rooting again without the reflected-hosts test above catching it. - /// - [Fact] - public void ConfigPropertyHostTypes_DynamicDependencies_Match_Returned_Types () - { - // Arrange - MethodInfo getTypes = - typeof (ConfigPropertyHostTypes).GetMethod (nameof (ConfigPropertyHostTypes.GetTypes), - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!; - - HashSet rooted = getTypes.GetCustomAttributes ().Select (a => a.Type!).ToHashSet (); - - HashSet returned = ConfigPropertyHostTypes.GetTypes ().ToHashSet (); - - // Assert - Type [] missingRoots = returned.Except (rooted).ToArray (); - Type [] extraRoots = rooted.Except (returned).ToArray (); - - Assert.True (missingRoots.Length == 0 && extraRoots.Length == 0, - $"ConfigPropertyHostTypes [DynamicDependency] drift detected.\nTypes in array but not rooted by attribute: { - string.Join (", ", missingRoots.Select (t => t.FullName)) - }\nTypes rooted by attribute but not in array: { - string.Join (", ", extraRoots.Select (t => t.FullName)) - }"); - } - - /// - /// Guard-rail test: the hard-coded list in must exhaustively cover every type in - /// the Terminal.Gui assembly that declares a property. Drift between - /// the list and the attribute usage would silently reintroduce the trim/AOT failure fixed by - /// . - /// - [Fact] - public void ConfigPropertyHostTypes_GetTypes_Matches_Reflected_Hosts_In_TerminalGui_Assembly () - { - // Arrange - Assembly terminalGuiAssembly = typeof (ConfigurationManager).Assembly; - - HashSet reflected = terminalGuiAssembly.GetTypes ().Where (HasConfigurationProperty).ToHashSet (); - - // Act - HashSet registered = ConfigPropertyHostTypes.GetTypes ().ToHashSet (); - - // Assert - Type [] missing = reflected.Except (registered).ToArray (); - Type [] extra = registered.Except (reflected).ToArray (); - - Assert.True (missing.Length == 0 && extra.Length == 0, - $"ConfigPropertyHostTypes drift detected.\nMissing (add to list): { - string.Join (", ", missing.Select (t => t.FullName)) - }\nExtra (remove from list): { - string.Join (", ", extra.Select (t => t.FullName)) - }"); - } - - private static bool HasConfigurationProperty (Type type) - { - // Mirror the production scan in ConfigProperty.Initialize (), which calls type.GetProperties () — i.e., - // public properties only (includes inherited public members; excludes non-public and, by default, statics - // not declared on `type` itself). Widening the binding flags here would flag types the production scan - // could never discover and the trimmer wouldn't preserve via PublicProperties. - PropertyInfo [] properties = type.GetProperties (); - - foreach (PropertyInfo property in properties) - { - if (property.GetCustomAttribute () is { }) - { - return true; - } - } - - return false; - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs deleted file mode 100644 index 48b493a474..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Concurrent; - -namespace ConfigurationTests; - -public class ConfigPropertyTests -{ - private class Dummy - { - public string Value { get; set; } = string.Empty; - } - - [Fact] - public void PropertyValue_CanBeSetAndReadConcurrently () - { - var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value)); - var configProperty = new ConfigProperty { PropertyInfo = propertyInfo }; - - int numTasks = 20; - var values = new string [numTasks]; - for (int i = 0; i < numTasks; i++) - { - values [i] = $"Value_{i}"; - } - - Parallel.For (0, numTasks, i => - { - configProperty.PropertyValue = values [i]; - // Remove the per-thread assertion, as it is not valid in a concurrent context. - // Optionally, you can check that the value is one of the expected values: - var currentValue = configProperty.PropertyValue as string; - Assert.Contains (currentValue, values); - }); - } - - - [Fact] - public void UpdateFrom_CanBeCalledConcurrently () - { - var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value)); - var configProperty = new ConfigProperty { PropertyInfo = propertyInfo, PropertyValue = "Initial" }; - - int numTasks = 20; - Parallel.For (0, numTasks, i => - { - var newValue = $"Thread_{i}"; - configProperty.UpdateFrom (newValue); - }); - - var finalValue = configProperty.PropertyValue as string; - Assert.StartsWith ("Thread_", finalValue); - } - - - [Fact] - public void DeepCloner_DeepClone_IsThreadSafe () - { - var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value)); - var configProperty = new ConfigProperty { PropertyInfo = propertyInfo, PropertyValue = "DeepCloneValue" }; - - int numTasks = 20; - Parallel.For (0, numTasks, i => - { - var clone = DeepCloner.DeepClone (configProperty); - Assert.NotSame (configProperty, clone); - Assert.Equal ("DeepCloneValue", clone!.PropertyValue); - }); - } - - [Fact] - public void ConcurrentDictionary_UpdateFrom_IsThreadSafe () - { - var propertyInfo = typeof (Dummy).GetProperty (nameof (Dummy.Value)); - var destinationDict = new ConcurrentDictionary (StringComparer.InvariantCultureIgnoreCase); - destinationDict.TryAdd ("prop1", new ConfigProperty { PropertyValue = "Original", HasValue = true }); - - var configProperty = new ConfigProperty - { - PropertyInfo = propertyInfo, - PropertyValue = destinationDict - }; - - int numTasks = 20; - Parallel.For (0, numTasks, i => - { - var sourceDict = new ConcurrentDictionary (StringComparer.InvariantCultureIgnoreCase); - sourceDict.TryAdd ($"prop{i}", new ConfigProperty { PropertyValue = $"Value_{i}", HasValue = true }); - configProperty.UpdateFrom (sourceDict); - }); - - var resultDict = configProperty.PropertyValue as ConcurrentDictionary; - Assert.NotNull (resultDict); - for (int i = 0; i < numTasks; i++) - { - Assert.True (resultDict!.ContainsKey ($"prop{i}")); - Assert.Equal ($"Value_{i}", resultDict [$"prop{i}"].PropertyValue); - } - Assert.True (resultDict!.ContainsKey ("prop1")); - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs deleted file mode 100644 index 21b383efa8..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -#nullable enable - -using System.Reflection; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace ConfigurationTests; - -public class ConfigurationPropertyAttributeTests -{ - - /// - /// If this test fails, you need to add a new property to to support serialization of the new property type. - /// - [Fact] - public void Verify_Types_Added_To_JsonSerializerContext () - { - // The assembly containing the types to inspect - var assembly = Assembly.GetAssembly (typeof (SourceGenerationContext)); - - // Get all types from the assembly - var types = assembly!.GetTypes (); - - // Find all properties with the [ConfigurationProperty] attribute - var properties = new List (); - foreach (var type in types) - { - properties.AddRange (type.GetProperties ().Where (p => - p.GetCustomAttributes (typeof (ConfigurationPropertyAttribute), false).Any ())); - } - - // Get the types of the properties - var propertyTypes = properties.Select (p => p.PropertyType).Distinct (); - - // Get the types registered in the JsonSerializerContext derived class - var contextType = typeof (SourceGenerationContext); - var contextTypes = GetRegisteredTypes (contextType); - - // Ensure all property types are included in the JsonSerializerContext derived class - IEnumerable collection = contextTypes as Type [] ?? contextTypes.ToArray (); - - foreach (var type in propertyTypes) - { - Assert.Contains (type, collection); - } - - // Ensure no property has the generic JsonStringEnumConverter<> - EnsureNoSpecifiedConverters (properties, new [] { typeof (JsonStringEnumConverter<>) }); - // Ensure no property has the type RuneJsonConverter - EnsureNoSpecifiedConverters (properties, new [] { typeof (RuneJsonConverter) }); - // Ensure no property has the type KeyJsonConverter - EnsureNoSpecifiedConverters (properties, new [] { typeof (KeyJsonConverter) }); - - // Find all classes with the JsonConverter attribute of type ScopeJsonConverter<> - var classesWithScopeJsonConverter = types.Where (t => - t.GetCustomAttributes (typeof (JsonConverterAttribute), false) - .Any (attr => ((JsonConverterAttribute)attr).ConverterType!.IsGenericType && - ((JsonConverterAttribute)attr).ConverterType!.GetGenericTypeDefinition () == typeof (ScopeJsonConverter<>))); - - // Ensure all these classes are included in the JsonSerializerContext derived class - foreach (var type in classesWithScopeJsonConverter) - { - Assert.Contains (type, collection); - } - } - - - [Fact] - public void OmitClassName_Omits () - { - // Color.Schemes is serialized as "Schemes", not "Colors.Schemes" - PropertyInfo pi = typeof (SchemeManager).GetProperty ("Schemes")!; - var scp = (ConfigurationPropertyAttribute)pi!.GetCustomAttribute (typeof (ConfigurationPropertyAttribute))!; - Assert.True (scp!.Scope == typeof (ThemeScope)); - Assert.True (scp.OmitClassName); - } - - private static IEnumerable GetRegisteredTypes (Type contextType) - { - // Use reflection to find which types are registered in the JsonSerializerContext - var registeredTypes = new List (); - - var properties = contextType.GetProperties (BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); - foreach (var property in properties) - { - if (property.PropertyType.IsGenericType && - property.PropertyType.GetGenericTypeDefinition () == typeof (JsonTypeInfo<>)) - { - registeredTypes.Add (property.PropertyType.GetGenericArguments () [0]); - } - } - - return registeredTypes.Distinct (); - } - - private static void EnsureNoSpecifiedConverters (List properties, IEnumerable converterTypes) - { - // Ensure no property has any of the specified converter types - foreach (var property in properties) - { - var jsonConverterAttributes = property.GetCustomAttributes (typeof (JsonConverterAttribute), false) - .Cast (); - - foreach (var attribute in jsonConverterAttributes) - { - foreach (var converterType in converterTypes) - { - if (attribute.ConverterType!.IsGenericType && - attribute.ConverterType.GetGenericTypeDefinition () == converterType) - { - Assert.Fail ($"Property '{property.Name}' should not use the converter '{converterType.Name}'."); - } - - if (!attribute.ConverterType!.IsGenericType && - attribute.ConverterType == converterType) - { - Assert.Fail ($"Property '{property.Name}' should not use the converter '{converterType.Name}'."); - } - } - } - } - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs b/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs deleted file mode 100644 index a83e018136..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs +++ /dev/null @@ -1,685 +0,0 @@ -#nullable enable - -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; - -namespace ConfigurationTests; - -/// -/// Unit tests for the class, ensuring robust deep cloning for -/// Terminal.Gui's configuration system. -/// -public class DeepClonerTests -{ - // Test classes for complex scenarios - private class SimpleValueType - { - public int Number { get; set; } - public bool Flag { get; init; } - } - - private class SimpleReferenceType - { - public string? Name { get; set; } - public int Count { get; set; } - - public override bool Equals (object? obj) { return obj is SimpleReferenceType other && Name == other.Name && Count == other.Count; } - - // ReSharper disable twice NonReadonlyMemberInGetHashCode - public override int GetHashCode () { return HashCode.Combine (Name, Count); } - } - - private class CollectionContainer - { - public List? Strings { get; init; } - public Dictionary? Counts { get; init; } - public int []? Numbers { get; init; } - } - - private class NestedObject - { - public SimpleReferenceType? Inner { get; init; } - public List? Values { get; init; } - } - - private class CircularReference - { - public CircularReference? Self { get; set; } - public string? Name { get; set; } - } - - private class ConfigPropertyMock - { - public object? PropertyValue { get; init; } - public bool Immutable { get; init; } - } - - private class ComplexKey - { - public int Id { get; init; } - public override bool Equals (object? obj) { return obj is ComplexKey key && Id == key.Id; } - - public override int GetHashCode () { return Id.GetHashCode (); } - } - - private class KeyEqualityComparer : IEqualityComparer - { - public bool Equals (Key? x, Key? y) { return x?.KeyCode == y?.KeyCode; } - - public int GetHashCode (Key obj) { return obj.KeyCode.GetHashCode (); } - } - - // Fundamentals - - [Fact] - public void Null_ReturnsNull () - { - object? source = null; - object? result = DeepCloner.DeepClone (source); - - Assert.Null (result); - } - - [Fact] - public void SimpleValueType_ReturnsEqualValue () - { - var source = 42; - int result = DeepCloner.DeepClone (source); - - Assert.Equal (source, result); - } - - [Fact] - public void String_ReturnsSameString () - { - var source = "Hello"; - string? result = DeepCloner.DeepClone (source); - - Assert.Equal (source, result); - Assert.Same (source, result); // Strings are immutable - } - - [Fact] - public void Rune_ReturnsEqualRune () - { - Rune source = new ('A'); - Rune result = DeepCloner.DeepClone (source); - - Assert.Equal (source, result); - } - - [Fact] - public void Key_CreatesDeepCopy () - { - Key? source = new (KeyCode.A); - source.Handled = true; - Key? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source.KeyCode, result.KeyCode); - Assert.Equal (source.Handled, result.Handled); - - // Modify result, ensure source unchanged - result.Handled = false; - Assert.True (source.Handled); - } - - [Fact] - public void Enum_ReturnsEqualEnum () - { - var source = DayOfWeek.Monday; - DayOfWeek result = DeepCloner.DeepClone (source); - - Assert.Equal (source, result); - } - - [Fact] - public void Boolean_ReturnsEqualValue () - { - var source = true; - bool result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - - source = false; - result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - } - - [Fact] - public void Attribute_ReturnsEqualValue () - { - var source = new Attribute (Color.Black); - Attribute result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - - source = new (Color.White); - result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - } - - [Fact] - public void Scheme_Normal_Set_ReturnsEqualValue () - { - var source = new Scheme (new Scheme (new Attribute (Color.Red, Color.Green, TextStyle.Bold))); - Scheme? result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - - source = new Scheme (new Scheme ()); - result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - } - - [Fact] - public void Scheme_All_Set_ReturnsEqualValue () - { - Scheme? source = new () - { - Normal = new ("LightGray", "RaisinBlack", TextStyle.None), - Focus = new ("White", "DarkGray", TextStyle.None), - HotNormal = new ("Silver", "RaisinBlack", TextStyle.Underline), - Disabled = new ("DarkGray", "RaisinBlack", TextStyle.Faint), - HotFocus = new ("White", "Green", TextStyle.Underline), - Active = new ("White", "Charcoal", TextStyle.Bold), - HotActive = new ("Yellow", "Charcoal", TextStyle.Underline | TextStyle.Bold), - Highlight = new ("White", "Onyx", TextStyle.None), - Editable = new ("LightYellow", "RaisinBlack", TextStyle.None), - ReadOnly = new ("Gray", "RaisinBlack", TextStyle.Italic) - }; - Scheme? result = DeepCloner.DeepClone (source); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Normal, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotNormal, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Focus, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotFocus, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.HotActive, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Highlight, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Editable, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.ReadOnly, out _)); - Assert.True (source.TryGetExplicitlySetAttributeForRole (VisualRole.Disabled, out _)); - - Assert.Equal (source, result); - - source = new Scheme (new Scheme ()); - result = DeepCloner.DeepClone (source); - Assert.Equal (source, result); - } - - [Fact] - public void Dictionary_Of_Schemes_Preserves_Styled_Normal_Attribute () - { - // Copilot - Dictionary source = new () - { - ["Base"] = new Scheme - { - Normal = new Attribute (Color.White, Color.Black, TextStyle.Bold) - } - }; - - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.Equal (TextStyle.Bold, result ["Base"]!.Normal.Style); - Assert.Equal (source, result); - } - - // Simple Reference Types - - [Fact] - public void SimpleReferenceType_CreatesDeepCopy () - { - SimpleReferenceType? source = new () { Name = "Test", Count = 10 }; - SimpleReferenceType? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source.Name, result!.Name); - Assert.Equal (source.Count, result.Count); - Assert.True (source.Equals (result)); // Verify Equals method - Assert.Equal (source.GetHashCode (), result.GetHashCode ()); // Verify GetHashCode - - // Modify result, ensure source unchanged - result.Name = "Modified"; - result.Count = 20; - Assert.Equal ("Test", source.Name); - Assert.Equal (10, source.Count); - } - - // Collections - - [Fact] - public void List_CreatesDeepCopy () - { - List? source = new () { "One", "Two" }; - List? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); - - // Modify result, ensure source unchanged - result!.Add ("Three"); - Assert.Equal (2, source.Count); - Assert.Equal (3, result.Count); - } - - [Fact] - public void Dictionary_CreatesDeepCopy () - { - Dictionary? source = new () { { "A", 1 }, { "B", 2 } }; - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); - - // Modify result, ensure source unchanged - result! ["C"] = 3; - Assert.Equal (2, source.Count); - Assert.Equal (3, result.Count); - } - - [Fact] - public void Dictionary_CreatesDeepCopy_Including_Comparer_Options () - { - Dictionary? source = new (StringComparer.InvariantCultureIgnoreCase) { { "A", 1 }, { "B", 2 } }; - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); - Assert.Equal (source.Comparer, result.Comparer); - - // Modify result, ensure source unchanged - result! ["C"] = 3; - Assert.Equal (2, source.Count); - Assert.Equal (3, result.Count); - - Assert.Contains ("A", result); - Assert.Contains ("a", result); - } - - [Fact] - public void Dictionary_CreatesDeepCopy_WithCapacity () - { - // Arrange: Create a dictionary with a specific capacity - Dictionary source = new (100) // Set initial capacity to 100 - { - { "Key1", 1 }, - { "Key2", 2 } - }; - - // Act: Clone the dictionary - Dictionary? result = DeepCloner.DeepClone (source); - - // Assert: Verify the dictionary was cloned correctly - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); // Verify key-value pairs are cloned - - // Verify that the capacity is preserved (if supported) - Assert.True (result.Count <= result.EnsureCapacity (0)); // EnsureCapacity(0) returns the current capacity - Assert.True (source.Count <= source.EnsureCapacity (0)); // EnsureCapacity(0) returns the current capacity - } - - [Fact] - public void ConcurrentDictionary_CreatesDeepCopy () - { - ConcurrentDictionary? source = new (new Dictionary () { { "A", 1 }, { "B", 2 } }); - ConcurrentDictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); - - // Modify result, ensure source unchanged - result! ["C"] = 3; - Assert.Equal (2, source.Count); - Assert.Equal (3, result.Count); - } - - [Fact] - public void Array_CreatesDeepCopy () - { - int []? source = { 1, 2, 3 }; - int []? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source, result); - - // Modify result, ensure source unchanged - result! [0] = 99; - Assert.Equal (1, source [0]); - Assert.Equal (99, result [0]); - } - - [Fact] - public void ImmutableList_ThrowsNotSupported () - { - ImmutableList source = ImmutableList.Create ("One", "Two"); - Assert.Throws (() => DeepCloner.DeepClone (source)); - } - - [Fact] - public void ImmutableDictionary_ThrowsNotSupported () - { - ImmutableDictionary source = ImmutableDictionary.Create ().Add ("A", 1); - Assert.Throws (() => DeepCloner.DeepClone (source)); - } - - [Fact] - public void Dictionary_SourceAddsItem_ClonesCorrectly () - { - Dictionary? source = new () - { - { "Disabled", new (Color.White) }, - { "Normal", new (Color.Blue) } - }; - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (2, result.Count); - Assert.Equal ((Attribute)source ["Disabled"], (Attribute)result ["Disabled"]); - Assert.Equal (source ["Normal"], result ["Normal"]); - } - - [Fact] - public void Dictionary_SourceUpdatesOneItem_ClonesCorrectly () - { - Dictionary? source = new () { { "Disabled", new (Color.White) } }; - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Single (result); - Assert.Equal (source ["Disabled"], result ["Disabled"]); - } - - [Fact] - public void Dictionary_WithComplexKeys_ClonesCorrectly () - { - Dictionary? source = new () - { - { new() { Id = 1 }, "Value1" } - }; - Dictionary? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source.Keys.First ().Id, result.Keys.First ().Id); - Assert.Equal (source.Values.First (), result.Values.First ()); - } - - [Fact] - public void Dictionary_WithCustomKeyComparer_ClonesCorrectly () - { - Dictionary source = new (new KeyEqualityComparer ()) - { - { new (KeyCode.Esc), "Esc" } - }; - Dictionary result = DeepCloner.DeepClone (source)!; - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Single (result); - Assert.True (result.ContainsKey (new (KeyCode.Esc))); - Assert.Equal ("Esc", result [new (KeyCode.Esc)]); - - // Modify result, ensure source unchanged - result [new (KeyCode.Q)] = "Q"; - Assert.False (source.ContainsKey (new (KeyCode.Q))); - } - - // Nested Objects - - [Fact] - public void CollectionContainer_CreatesDeepCopy () - { - CollectionContainer? source = new () - { - Strings = ["A", "B"], - Counts = new () { { "X", 1 }, { "Y", 2 } }, - Numbers = [10, 20] - }; - CollectionContainer? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.NotSame (source.Strings, result!.Strings); - Assert.NotSame (source.Counts, result.Counts); - Assert.NotSame (source.Numbers, result.Numbers); - Assert.Equal (source.Strings, result.Strings); - Assert.Equal (source.Counts, result.Counts); - Assert.Equal (source.Numbers, result.Numbers); - - // Modify result, ensure source unchanged - result.Strings!.Add ("C"); - result.Counts! ["Z"] = 3; - result.Numbers! [0] = 99; - Assert.Equal (2, source.Strings.Count); - Assert.Equal (2, source.Counts.Count); - Assert.Equal (10, source.Numbers [0]); - } - - [Fact] - public void NestedObject_CreatesDeepCopy () - { - NestedObject? source = new () - { - Inner = new () { Name = "Inner", Count = 5 }, - Values = new () - { - new() { Number = 1, Flag = true }, - new() { Number = 2, Flag = false } - } - }; - NestedObject? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.NotSame (source.Inner, result!.Inner); - Assert.NotSame (source.Values, result.Values); - Assert.Equal (source.Inner!.Name, result.Inner!.Name); - Assert.Equal (source.Inner.Count, result.Inner.Count); - Assert.Equal (source.Values! [0].Number, result.Values! [0].Number); - Assert.Equal (source.Values [0].Flag, result.Values [0].Flag); - - // Modify result, ensure source unchanged - result.Inner.Name = "Modified"; - result.Values [0].Number = 99; - Assert.Equal ("Inner", source.Inner.Name); - Assert.Equal (1, source.Values [0].Number); - } - - // Circular References - - [Fact] - public void CircularReference_HandlesCorrectly () - { - CircularReference? source = new () { Name = "Cycle" }; - source.Self = source; // Create circular reference - CircularReference? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.Equal (source.Name, result!.Name); - Assert.NotNull (result.Self); - Assert.Same (result, result.Self); // Circular reference preserved - Assert.NotSame (source.Self, result.Self); - - // Modify result, ensure source unchanged - result.Name = "Modified"; - Assert.Equal ("Cycle", source.Name); - } - - // Terminal.Gui-Specific Types - - [Fact] - public void ConfigPropertyMock_CreatesDeepCopy () - { - ConfigPropertyMock? source = new () - { - PropertyValue = new List { "Red", "Blue" }, - Immutable = true - }; - ConfigPropertyMock? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotSame (source, result); - Assert.NotSame (source.PropertyValue, result!.PropertyValue); - Assert.Equal ((List)source.PropertyValue, (List)result.PropertyValue!); - Assert.Equal (source.Immutable, result.Immutable); - - // Modify result, ensure source unchanged - ((List)result.PropertyValue!).Add ("Green"); - Assert.Equal (2, ((List)source.PropertyValue).Count); - } - - [Fact] - public void ConfigProperty_CreatesDeepCopy () - { - ConfigProperty? source = ConfigProperty.CreateImmutableWithAttributeInfo (CM.GetHardCodedConfigPropertyCache ()! ["Application.DefaultKeyBindings"].PropertyInfo!); - source.Immutable = false; - source.PropertyValue = Application.DefaultKeyBindings; - ConfigProperty? result = DeepCloner.DeepClone (source); - - Assert.NotNull (result); - Assert.NotNull (result.PropertyInfo); - Assert.NotSame (source, result); - Assert.NotSame (source.PropertyValue, result!.PropertyValue); - // PropertyInfo is effectively a simple type - Assert.Same (source.PropertyInfo, result!.PropertyInfo); - Assert.Equal (source.Immutable, result.Immutable); - } - - [Fact] - public void LargeObject_PerformsWithinLimit () - { - List source = new (Enumerable.Range (1, 10000)); - var stopwatch = Stopwatch.StartNew (); - List result = DeepCloner.DeepClone (source)!; - stopwatch.Stop (); - - Assert.Equal (source, result); - Assert.True (stopwatch.ElapsedMilliseconds < 1000); // Ensure it completes within 1 second - } - - [Fact] - public void CloneDictionary_ShouldClone_NormalDictionary () - { - // Arrange: A supported generic dictionary - var original = new Dictionary - { - ["one"] = 1, - ["two"] = 2, - ["three"] = 3 - }; - - // Act - var cloned = DeepCloner.DeepClone (original); - - // Assert - Assert.NotNull (cloned); - Assert.NotSame (original, cloned); // must be a new instance - Assert.Equal (original, cloned); // must have same contents - Assert.Equal (original.Count, cloned.Count); - Assert.Equal (original ["one"], cloned ["one"]); - Assert.Equal (original ["two"], cloned ["two"]); - Assert.Equal (original ["three"], cloned ["three"]); - } - - [Fact] - public void CloneDictionary_ShouldClone_ConcurrentDictionary () - { - // Arrange - var original = new ConcurrentDictionary - { - ["a"] = "alpha", - ["b"] = "beta" - }; - - // Act - var cloned = DeepCloner.DeepClone (original); - - // Assert - Assert.NotSame (original, cloned); - Assert.Equal (original, cloned); - } - - [Fact] - public void CloneDictionary_Empty_Dictionary_ShouldWork () - { - // Arrange - var original = new Dictionary (); - - // Act - var cloned = DeepCloner.DeepClone (original); - - // Assert - Assert.NotSame (original, cloned); - Assert.Empty (cloned!); - } - - [Fact] - public void CloneDictionary_Empty_ConcurrentDictionary_ShouldWork () - { - // Arrange - var original = new ConcurrentDictionary (); - - // Act - var cloned = DeepCloner.DeepClone (original); - - // Assert - Assert.NotSame (original, cloned); - Assert.Empty (cloned!); - } - - // Copilot - [Fact] - public void CloneDictionary_ShouldClone_ColorName16Dictionary () - { - // Arrange: Dictionary — the type that crashes under AOT - // when CreateDictionaryInstance uses Activator.CreateInstance (see issue #5259). - Dictionary original = new () - { - [ColorName16.Black] = "000000", - [ColorName16.Red] = "FF0000", - [ColorName16.White] = "FFFFFF" - }; - - // Act - Dictionary? cloned = DeepCloner.DeepClone (original); - - // Assert - Assert.NotNull (cloned); - Assert.NotSame (original, cloned); - Assert.Equal (original.Count, cloned.Count); - Assert.Equal (original [ColorName16.Black], cloned [ColorName16.Black]); - Assert.Equal (original [ColorName16.Red], cloned [ColorName16.Red]); - Assert.Equal (original [ColorName16.White], cloned [ColorName16.White]); - } - - [Fact] - public void CloneDictionary_With_Unsupported_Dictionary_Throws () - { - // Arrange: A generic dictionary type (SortedDictionary for example) - var unsupportedDict = new SortedDictionary - { - { 1, "A" }, - { 2, "B" } - }; - - // Act & Assert: DeepCloner should throw - Assert.ThrowsAny (() => - { - // This should throw, because DeepCloner does not support SortedDictionary - _ = DeepCloner.DeepClone (unsupportedDict); - }); - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/DottedKeyTests.cs b/Tests/UnitTestsParallelizable/Configuration/DottedKeyTests.cs new file mode 100644 index 0000000000..1b2413abec --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/DottedKeyTests.cs @@ -0,0 +1,123 @@ +// Claude - Opus 4.8 +// Grok - grok-4.6 +using System.Text; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Configuration; +using Terminal.Gui.Configuration; +using Terminal.Gui.Input; +using Terminal.Gui.Tools.MigrateConfig; + +namespace ConfigurationTests; + +/// +/// Nested MEC binding is the supported contract. Flat dotted keys are a pre-MEC shape: +/// they do not bind until converted with . +/// +[Collection ("StaticSettingsTests")] +public class DottedKeyTests +{ + [Fact] + public void JsonProvider_DotInKey_DoesNotCreateSection () + { + string json = """{ "Driver.Force16Colors": true }"""; + + IConfiguration config = new ConfigurationBuilder () + .AddTuiRuntimeConfig (json) + .Build (); + + IConfigurationSection section = config.GetSection ("Driver"); + + Assert.False (section.Exists (), "MEC JSON does not auto-split dotted keys into sections"); + } + + [Fact] + public void ApplyToStaticFacades_DoesNotBindFlatDottedKeys () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = """ + { + "Driver.Force16Colors": true, + "Application.IsMouseDisabled": true, + "Key.Separator": "-", + "PopoverMenu.DefaultKey": "Ctrl+P" + } + """; + builder.ApplyToStaticFacades (); + + Assert.False (DriverSettings.Defaults.Force16Colors); + Assert.False (ApplicationSettings.Defaults.IsMouseDisabled); + Assert.NotEqual (new Rune ('-'), KeySettings.Defaults.Separator); + Assert.NotEqual (Key.P.WithCtrl, PopoverMenu.DefaultKey); + } + + [Fact] + public void ApplyToStaticFacades_BindsScalarThemeKey () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = """{ "Theme": "Dark" }"""; + builder.ApplyToStaticFacades (); + + Assert.Equal ("Dark", ThemeSettings.Defaults.Theme); + } + + [Fact] + public void ApplyToStaticFacades_BindsNestedSectionFormat () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = """{ "Driver": { "Force16Colors": true } }"""; + builder.ApplyToStaticFacades (); + + Assert.True (DriverSettings.Defaults.Force16Colors); + } + + [Fact] + public void ApplyToStaticFacades_BindsNestedKeyTypedProperty () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = """{ "PopoverMenu": { "DefaultKey": "Ctrl+P" } }"""; + builder.ApplyToStaticFacades (); + + Assert.Equal (Key.P.WithCtrl, PopoverMenu.DefaultKey); + } + + [Fact] + public void ApplyToStaticFacades_BindsRuneViaJsonConverterFormats () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = """{ "Key": { "Separator": "U+002D" } }"""; + builder.ApplyToStaticFacades (); + + Assert.Equal (new Rune ('-'), KeySettings.Defaults.Separator); + } + + [Fact] + public void MigratedDottedJson_BindsAfterMigrateObject () + { + using SettingsFacadeSnapshot snapshot = new (); + + string dotted = """ + { + "Driver.Force16Colors": true, + "Key.Separator": "-" + } + """; + JsonObject migrated = ConfigJsonMigrator.MigrateObject (JsonNode.Parse (dotted)!.AsObject ()); + + TuiConfigurationBuilder builder = new (); + builder.RuntimeConfig = migrated.ToJsonString (); + builder.ApplyToStaticFacades (); + + Assert.True (DriverSettings.Defaults.Force16Colors); + Assert.Equal (new Rune ('-'), KeySettings.Defaults.Separator); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/GlyphConfigConversionTests.cs b/Tests/UnitTestsParallelizable/Configuration/GlyphConfigConversionTests.cs new file mode 100644 index 0000000000..f1f7b10194 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/GlyphConfigConversionTests.cs @@ -0,0 +1,63 @@ +// Claude - Fable 5 +using System.Text; +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// Tests for conversion when glyph values arrive through MEC configuration. +/// A single-character string is always the glyph itself; multi-digit strings are legacy +/// JSON-number codepoints flattened to strings by MEC. +/// +[Collection ("StaticSettingsTests")] +public class GlyphConfigConversionTests +{ + [Fact] + public void ApplyToStaticFacades_SingleDigitGlyphString_IsTheGlyphNotACodepoint () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """{ "Glyphs": { "CheckStateChecked": "6" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (new Rune ('6'), GlyphSettings.Current.CheckStateChecked); + } + + [Fact] + public void ApplyToStaticFacades_MultiDigitGlyphString_IsALegacyDecimalCodepoint () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + // Legacy CM allowed JSON numbers (e.g. 9733 for '★'); MEC flattens them to strings. + tuiBuilder.RuntimeConfig = """{ "Glyphs": { "CheckStateChecked": 9733 } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (new Rune ('★'), GlyphSettings.Current.CheckStateChecked); + } + + [Fact] + public void ApplyToStaticFacades_UPlusHexGlyphString_Binds () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """{ "Glyphs": { "CheckStateChecked": "U+2611" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (new Rune ('☑'), GlyphSettings.Current.CheckStateChecked); + } + + [Fact] + public void ApplyToStaticFacades_PlainGlyphString_Binds () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """{ "Glyphs": { "CheckStateChecked": "X" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (new Rune ('X'), GlyphSettings.Current.CheckStateChecked); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/KeyBindingConfigurationTests.cs b/Tests/UnitTestsParallelizable/Configuration/KeyBindingConfigurationTests.cs new file mode 100644 index 0000000000..bb43e66326 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/KeyBindingConfigurationTests.cs @@ -0,0 +1,230 @@ +// Grok - grok-4.6 +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Configuration; +using Terminal.Gui.App; +using Terminal.Gui.Configuration; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; + +namespace ConfigurationTests; + +/// +/// Nested MEC key-binding dictionaries overlay hard-coded defaults. +/// +[Collection ("StaticSettingsTests")] +public class KeyBindingConfigurationTests +{ + [Fact] + public void ApplyToStaticFacades_OverlaysApplicationDefaultKeyBindings () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Ctrl+Q"] } + } + } + } + """; + builder.ApplyToStaticFacades (); + + Assert.NotNull (Application.DefaultKeyBindings); + Assert.Equal (Key.Q.WithCtrl, Application.DefaultKeyBindings [Command.Quit].All! [0]); + Assert.True (Application.DefaultKeyBindings.ContainsKey (Command.Arrange)); + } + + [Fact] + public void ApplyToStaticFacades_OverlaysViewDefaultKeyBindings () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "View": { + "DefaultKeyBindings": { + "Copy": { "All": ["Ctrl+Insert"] } + } + } + } + """; + builder.ApplyToStaticFacades (); + + Assert.NotNull (View.DefaultKeyBindings); + Assert.Equal (Key.InsertChar.WithCtrl, View.DefaultKeyBindings [Command.Copy].All! [0]); + Assert.True (View.DefaultKeyBindings.ContainsKey (Command.Paste)); + } + + [Fact] + public void ApplyToStaticFacades_OverlaysViewKeyBindingsByType () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "View": { + "ViewKeyBindings": { + "TextField": { + "CutToEndOfLine": { "All": ["Ctrl+K"] } + } + } + } + } + """; + builder.ApplyToStaticFacades (); + + Assert.NotNull (View.ViewKeyBindings); + Assert.True (View.ViewKeyBindings.ContainsKey ("TextField")); + Assert.Equal (Key.K.WithCtrl, View.ViewKeyBindings ["TextField"] [Command.CutToEndOfLine].All! [0]); + } + + [Fact] + public void ApplyToStaticFacades_MigratedFlatKeyBindingJson_Binds () + { + using SettingsFacadeSnapshot snapshot = new (); + + string nested = """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } + } + } + } + """; + + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = nested; + builder.ApplyToStaticFacades (); + + Key [] quitKeys = Application.DefaultKeyBindings! [Command.Quit].All!; + Assert.Equal (2, quitKeys.Length); + Assert.Equal (Key.Esc, quitKeys [0]); + Assert.Equal (Key.Q.WithCtrl, quitKeys [1]); + } + + [Fact] + public void ToJson_RebuildsMecIndexChildrenAsJsonArrays () + { + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } + } + } + } + """; + + JsonNode? node = ConfigurationSectionJson.ToJson ( + builder.Configuration.GetSection ("Application").GetSection ("DefaultKeyBindings")); + + Assert.NotNull (node); + Assert.Equal (JsonValueKind.Array, node ["Quit"]! ["All"]!.GetValueKind ()); + Assert.Equal (2, node ["Quit"]! ["All"]!.AsArray ().Count); + Assert.Equal ("Esc", node ["Quit"]! ["All"]! [0]!.GetValue ()); + Assert.Equal ("Ctrl+Q", node ["Quit"]! ["All"]! [1]!.GetValue ()); + } + + // Claude - Fable 5 + // A binding removed from configuration must regain its default on re-apply — "unmentioned + // commands keep hard-coded defaults" also holds across Reload, not just at first apply. + [Fact] + public void ApplyToStaticFacades_RemovedBinding_RestoresDefault () + { + using SettingsFacadeSnapshot snapshot = new (); + + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Ctrl+X"] } + } + } + } + """; + builder.ApplyToStaticFacades (); + Assert.Equal (Key.X.WithCtrl, Application.DefaultKeyBindings! [Command.Quit].All! [0]); + + builder.RuntimeConfig = "{ }"; + builder.ApplyToStaticFacades (); + + Assert.Equal (Key.Esc, Application.DefaultKeyBindings! [Command.Quit].All! [0]); + } + + // Claude - Fable 5 + // A higher-priority source's array must atomically replace a lower-priority source's array; + // MEC's per-index merge would otherwise keep the longer array's tail elements bound. + [Fact] + public void ApplyToStaticFacades_ShorterArrayInHigherPrioritySource_ReplacesWholesale () + { + using SettingsFacadeSnapshot snapshot = new (); + + string root = Directory.CreateTempSubdirectory ("tui-kb-test-").FullName; + string tuiDir = Path.Combine (root, TuiConfigurationExtensions.TUI_CONFIG_FOLDER); + Directory.CreateDirectory (tuiDir); + + File.WriteAllText ( + Path.Combine (tuiDir, TuiConfigurationExtensions.CONFIG_FILENAME), + """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } + } + } + } + """); + + try + { + TuiConfigurationBuilder builder = new (appName: null, currentDirectory: root); + builder.RuntimeConfig = """ + { + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Ctrl+C"] } + } + } + } + """; + builder.ApplyToStaticFacades (); + + Key [] quitKeys = Application.DefaultKeyBindings! [Command.Quit].All!; + Assert.Single (quitKeys); + Assert.Equal (Key.C.WithCtrl, quitKeys [0]); + } + finally + { + Directory.Delete (root, recursive: true); + } + } + + [Fact] + public void ToJson_NonContiguousNumericKeys_StayObject () + { + TuiConfigurationBuilder builder = new (null, null, includeUserSources: false); + builder.RuntimeConfig = """ + { + "Probe": { + "10": "Esc", + "2": "Ctrl+Q" + } + } + """; + + JsonNode? node = ConfigurationSectionJson.ToJson (builder.Configuration.GetSection ("Probe")); + + Assert.NotNull (node); + Assert.Equal (JsonValueKind.Object, node.GetValueKind ()); + Assert.Equal ("Esc", node ["10"]!.GetValue ()); + Assert.Equal ("Ctrl+Q", node ["2"]!.GetValue ()); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/MecDottedKeyTests.cs b/Tests/UnitTestsParallelizable/Configuration/MecDottedKeyTests.cs deleted file mode 100644 index ceebb43f70..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/MecDottedKeyTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Claude - Opus 4.8 -// Tests that verify MEC binding against the flat dotted-key format used by the shipped config.json. - -using System.Text; -using Microsoft.Extensions.Configuration; -using Terminal.Gui.Configuration; -using Terminal.Gui.Input; - -namespace ConfigurationTests; - -/// -/// Verifies that flat dotted keys (e.g. Driver.Force16Colors) and the scalar Theme key, -/// as used by the shipped config.json, are correctly bound by . -/// -[Collection ("StaticSettingsTests")] -public class MecDottedKeyTests -{ - /// - /// Documents the root cause of CR feedback #1: the MEC JSON provider does NOT interpret a dot in a key - /// as a section separator (only : is). A flat key like Driver.Force16Colors is stored - /// literally, so GetSection ("Driver") finds nothing — which is why flat-key binding is needed. - /// - [Fact] - public void JsonProvider_DotInKey_DoesNotCreateSection () - { - string json = """{ "Driver.Force16Colors": true }"""; - - IConfiguration config = new ConfigurationBuilder () - .AddTuiRuntimeConfig (json) - .Build (); - - IConfigurationSection section = config.GetSection ("Driver"); - - Assert.False (section.Exists (), "MEC JSON does not auto-split dotted keys into sections"); - } - - /// - /// Verifies CR feedback #1 fix: a flat dotted bool key binds to its POCO property. - /// - [Fact] - public void ApplyToStaticFacades_BindsFlatDottedBoolKey () - { - DriverSettings original = DriverSettings.Defaults; - - try - { - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = """{ "Driver.Force16Colors": true }"""; - builder.ApplyToStaticFacades (); - - Assert.True (DriverSettings.Defaults.Force16Colors); - } - finally - { - DriverSettings.Defaults = original; - } - } - - /// - /// Verifies CR feedback #1 fix: multiple flat dotted keys across different sections bind correctly. - /// - [Fact] - public void ApplyToStaticFacades_BindsFlatDottedKeysAcrossSections () - { - DriverSettings originalDriver = DriverSettings.Defaults; - ApplicationSettings originalApp = ApplicationSettings.Defaults; - - try - { - string json = """ - { - "Driver.Force16Colors": true, - "Application.IsMouseDisabled": true - } - """; - - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = json; - builder.ApplyToStaticFacades (); - - Assert.True (DriverSettings.Defaults.Force16Colors); - Assert.True (ApplicationSettings.Defaults.IsMouseDisabled); - } - finally - { - DriverSettings.Defaults = originalDriver; - ApplicationSettings.Defaults = originalApp; - } - } - - /// - /// Verifies CR feedback #1 fix: a flat dotted key (Key.Separator) - /// binds via the value converter. - /// - [Fact] - public void ApplyToStaticFacades_BindsFlatDottedRuneKey () - { - KeySettings original = KeySettings.Defaults; - - try - { - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = """{ "Key.Separator": "-" }"""; - builder.ApplyToStaticFacades (); - - Assert.Equal (new Rune ('-'), KeySettings.Defaults.Separator); - } - finally - { - KeySettings.Defaults = original; - } - } - - /// - /// Verifies CR feedback #1 fix: the active theme is a scalar Theme key, not a nested section, - /// and binds to . - /// - [Fact] - public void ApplyToStaticFacades_BindsScalarThemeKey () - { - ThemeSettings original = ThemeSettings.Defaults; - - try - { - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = """{ "Theme": "Dark" }"""; - builder.ApplyToStaticFacades (); - - Assert.Equal ("Dark", ThemeSettings.Defaults.Theme); - } - finally - { - ThemeSettings.Defaults = original; - } - } - - /// - /// Verifies that the nested-section format still binds, so existing nested config files keep working. - /// - [Fact] - public void ApplyToStaticFacades_StillBindsNestedSectionFormat () - { - DriverSettings original = DriverSettings.Defaults; - - try - { - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = """{ "Driver": { "Force16Colors": true } }"""; - builder.ApplyToStaticFacades (); - - Assert.True (DriverSettings.Defaults.Force16Colors); - } - finally - { - DriverSettings.Defaults = original; - } - } - - /// - /// Verifies issue #5561 fix: a flat dotted key (PopoverMenu.DefaultKey) binds via - /// the AOT-safe fast path — i.e. without the trim-unsafe TypeDescriptor.GetConverter - /// fallback that was removed. - /// - [Fact] - public void ApplyToStaticFacades_BindsFlatDottedKeyTypedProperty () - { - PopoverMenuSettings original = PopoverMenuSettings.Defaults; - - try - { - TuiConfigurationBuilder builder = new (); - builder.RuntimeConfig = """{ "PopoverMenu.DefaultKey": "Ctrl+P" }"""; - builder.ApplyToStaticFacades (); - - Assert.Equal (Key.P.WithCtrl, PopoverMenuSettings.Defaults.DefaultKey); - } - finally - { - PopoverMenuSettings.Defaults = original; - } - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/MecThemeManagerLeakTests.cs b/Tests/UnitTestsParallelizable/Configuration/MecThemeManagerLeakTests.cs index cceb9a48dc..6d6b3ff08c 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MecThemeManagerLeakTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/MecThemeManagerLeakTests.cs @@ -105,11 +105,11 @@ public void LegacyThemeChanged_IsForwarded_OnlyWhileSubscribed () void Handler (object? sender, EventArgs e) => count++; manager.ThemeChanged += Handler; - ThemeManager.OnThemeChanged ("A", "B"); + ThemeManager.RaiseThemeChanged ("B"); Assert.Equal (1, count); manager.ThemeChanged -= Handler; - ThemeManager.OnThemeChanged ("B", "C"); + ThemeManager.RaiseThemeChanged ("C"); Assert.Equal (1, count); } diff --git a/Tests/UnitTestsParallelizable/Configuration/MenuBarDefaultKeyTests.cs b/Tests/UnitTestsParallelizable/Configuration/MenuBarDefaultKeyTests.cs new file mode 100644 index 0000000000..624b66c55b --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/MenuBarDefaultKeyTests.cs @@ -0,0 +1,71 @@ +// Claude - Fable 5 +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// has a public setter (as does ). +/// An app-set value must survive theme switches and configuration re-application, matching the legacy +/// SettingsScope behavior where theme switches never reset it. +/// +[Collection ("StaticSettingsTests")] +public class MenuBarDefaultKeyTests +{ + [Fact] + public void DefaultKey_SetByApp_SurvivesThemeOverlayReapply () + { + using SettingsFacadeSnapshot snapshot = new (); + + try + { + MenuBar.DefaultKey = Key.F12; + + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Theme": "Dark", "Themes": { "Dark": {} } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (Key.F12, MenuBar.DefaultKey); + } + finally + { + MenuBar.ResetDefaultKeyOverride (); + } + } + + [Fact] + public void DefaultKey_SetByApp_SurvivesThemeSwitch () + { + using SettingsFacadeSnapshot snapshot = new (); + + try + { + MenuBar.DefaultKey = Key.F12; + + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Themes": { "Dark": {} } }"""; + tuiBuilder.ApplyToStaticFacades (); + + MecThemeManager manager = new (tuiBuilder); + Assert.True (manager.SwitchTheme ("Dark")); + + Assert.Equal (Key.F12, MenuBar.DefaultKey); + } + finally + { + MenuBar.ResetDefaultKeyOverride (); + } + } + + [Fact] + public void DefaultKey_NotSetByApp_ComesFromConfiguration () + { + using SettingsFacadeSnapshot snapshot = new (); + + MenuBar.ResetDefaultKeyOverride (); + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "MenuBar": { "DefaultKey": "F11" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (Key.F11, MenuBar.DefaultKey); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyCollisionTests.cs b/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyCollisionTests.cs index ff9277fe24..29c15ea3c5 100644 --- a/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyCollisionTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyCollisionTests.cs @@ -16,12 +16,10 @@ public class PopoverMenuDefaultKeyCollisionTests [Fact] public void FileDialog_Ctor_DoesNotThrow_WhenDefaultKeyCollidesWithTableViewBinding () { - PopoverMenuSettings original = PopoverMenuSettings.Defaults; - try { // TableView binds Ctrl+P -> Command.Up by default; the context-menu key must win without throwing. - PopoverMenuSettings.Defaults = new () { DefaultKey = Key.P.WithCtrl }; + PopoverMenu.DefaultKey = Key.P.WithCtrl; MockFileSystem fs = new (); fs.AddDirectory ("/testdir"); @@ -32,19 +30,18 @@ public void FileDialog_Ctor_DoesNotThrow_WhenDefaultKeyCollidesWithTableViewBind } finally { - PopoverMenuSettings.Defaults = original; + // Setting DefaultKey pins an app override; tests must clear it, not assign the old value back. + PopoverMenu.ResetDefaultKeyOverride (); } } [Fact] public void CharMap_Ctor_DoesNotThrow_WhenDefaultKeyCollidesWithExistingBinding () { - PopoverMenuSettings original = PopoverMenuSettings.Defaults; - try { // CharMap binds Key.End -> Command.End before binding the context-menu key. - PopoverMenuSettings.Defaults = new () { DefaultKey = Key.End }; + PopoverMenu.DefaultKey = Key.End; using CharMap charMap = new (); @@ -52,7 +49,8 @@ public void CharMap_Ctor_DoesNotThrow_WhenDefaultKeyCollidesWithExistingBinding } finally { - PopoverMenuSettings.Defaults = original; + // Setting DefaultKey pins an app override; tests must clear it, not assign the old value back. + PopoverMenu.ResetDefaultKeyOverride (); } } } diff --git a/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyTests.cs b/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyTests.cs new file mode 100644 index 0000000000..5a239df82c --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/PopoverMenuDefaultKeyTests.cs @@ -0,0 +1,71 @@ +// Claude - Fable 5 +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// has a public setter, like . +/// An app-set value must survive theme switches and configuration re-application +/// ( is wholesale-replaced on each apply). +/// +[Collection ("StaticSettingsTests")] +public class PopoverMenuDefaultKeyTests +{ + [Fact] + public void DefaultKey_SetByApp_SurvivesThemeOverlayReapply () + { + using SettingsFacadeSnapshot snapshot = new (); + + try + { + PopoverMenu.DefaultKey = Key.F9; + + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Theme": "Dark", "Themes": { "Dark": {} } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (Key.F9, PopoverMenu.DefaultKey); + } + finally + { + PopoverMenu.ResetDefaultKeyOverride (); + } + } + + [Fact] + public void DefaultKey_SetByApp_SurvivesThemeSwitch () + { + using SettingsFacadeSnapshot snapshot = new (); + + try + { + PopoverMenu.DefaultKey = Key.F9; + + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Themes": { "Dark": {} } }"""; + tuiBuilder.ApplyToStaticFacades (); + + MecThemeManager manager = new (tuiBuilder); + Assert.True (manager.SwitchTheme ("Dark")); + + Assert.Equal (Key.F9, PopoverMenu.DefaultKey); + } + finally + { + PopoverMenu.ResetDefaultKeyOverride (); + } + } + + [Fact] + public void DefaultKey_NotSetByApp_ComesFromConfiguration () + { + using SettingsFacadeSnapshot snapshot = new (); + + PopoverMenu.ResetDefaultKeyOverride (); + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "PopoverMenu": { "DefaultKey": "F11" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (Key.F11, PopoverMenu.DefaultKey); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs index 222b29924e..a0a2e88a06 100644 --- a/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs @@ -143,4 +143,49 @@ public void Json_With_Decimal_Works () // Assert Assert.Equal ("a", deserialized.ToString ()); } + + // CoPilot - Grok 4.6 + [Theory] + [InlineData ("a", "a")] + [InlineData ("☑", "☑")] + [InlineData ("\\u2611", "☑")] + [InlineData ("U+2611", "☑")] + [InlineData ("🍎", "🍎")] + [InlineData ("U+1F34E", "🍎")] + [InlineData ("\\U0001F34E", "🍎")] + [InlineData ("\\ud83d \\udc69", "👩")] + [InlineData ("\\ud83d\\udc69", "👩")] + [InlineData ("U+d83d U+dc69", "👩")] + [InlineData ("U+1F469", "👩")] + [InlineData ("\\U0001F469", "👩")] + [InlineData ("\\u0065\\u0301", "é")] + [InlineData ("6", "6")] + public void TryParse_DoesNotNeedJsonSerializer_Positive (string rune, string expected) + { + Assert.True (RuneJsonConverter.TryParse (rune, out Rune result)); + Assert.Equal (expected, result.ToString ()); + } + + // CoPilot - Grok 4.6 + [Theory] + [InlineData (null)] + [InlineData ("")] + [InlineData ("aa")] + [InlineData ("☑☑")] + [InlineData ("\\x2611")] + [InlineData ("Z+2611")] + [InlineData ("🍎🍎")] + [InlineData ("U+FFF1F34E")] + [InlineData ("\\UFFF1F34E")] + [InlineData ("\\ud83d")] + [InlineData ("\\udc3d")] + [InlineData ("\\ud83d \\u1c69")] + [InlineData ("\\ud83ddc69")] + [InlineData ("U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467")] + [InlineData ("\\U0001F469\\u200D\\U0001F469\\u200D\\U0001F467\\u200D\\U0001F467")] + [InlineData ("9733")] + public void TryParse_DoesNotNeedJsonSerializer_Negative (string? rune) + { + Assert.False (RuneJsonConverter.TryParse (rune, out _)); + } } diff --git a/Tests/UnitTestsParallelizable/Configuration/SchemeManagerConfigTests.cs b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerConfigTests.cs new file mode 100644 index 0000000000..478e79abe7 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerConfigTests.cs @@ -0,0 +1,182 @@ +// Claude - Fable 5 +using System.Text.Json.Nodes; +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// Tests for : runtime-added and runtime-updated +/// schemes survive theme switches, root Schemes customizations merge with theme overlays +/// (matching the root-then-overlay contract of every other ThemeScope section), and invalid scheme +/// JSON is reported, not swallowed. +/// +[Collection ("StaticSettingsTests")] +public class SchemeManagerConfigTests +{ + /// Captures and restores the process-wide scheme state SchemeManager holds. + private sealed class SchemeSnapshot : IDisposable + { + private readonly Dictionary _schemes = new (SchemeManager.GetSchemes (), StringComparer.InvariantCultureIgnoreCase); + private readonly Dictionary _runtime = SchemeManager.GetRuntimeSchemes (); + + public void Dispose () + { + SchemeManager.ReplaceSchemes (_schemes); + SchemeManager.SetRuntimeSchemes (_runtime); + } + } + + private static JsonObject MergedJson (string json) => JsonNode.Parse (json)!.AsObject (); + + [Fact] + public void ApplyFromConfiguration_PreservesRuntimeAddedSchemes () + { + using SchemeSnapshot snapshot = new (); + + Scheme custom = new () { Normal = new (Color.White, Color.Blue) }; + SchemeManager.AddScheme ("MyScheme", custom); + + SchemeManager.ApplyFromConfiguration (MergedJson ("""{ "Themes": { "Dark": {} } }"""), "Dark"); + + Assert.True (SchemeManager.TryGetScheme ("MyScheme", out Scheme? survived)); + Assert.Equal (Color.White, survived!.Normal.Foreground); + } + + // A runtime UPDATE to an existing scheme must survive re-apply just like a runtime ADD; the + // documented AddScheme contract is "If the name already exists, it is updated". + [Fact] + public void ApplyFromConfiguration_PreservesRuntimeUpdatedBuiltInScheme () + { + using SchemeSnapshot snapshot = new (); + + Scheme custom = new () { Normal = new (Color.White, Color.Magenta) }; + SchemeManager.AddScheme ("Base", custom); + + SchemeManager.ApplyFromConfiguration (MergedJson ("""{ "Themes": { "Dark": {} } }"""), "Dark"); + + Assert.True (SchemeManager.TryGetScheme ("Base", out Scheme? survived)); + Assert.Equal (Color.Magenta, survived!.Normal.Background); + } + + [Fact] + public void ApplyFromConfiguration_RootSchemes_ApplyWhenThemeHasNoSchemesSection () + { + using SchemeSnapshot snapshot = new (); + + JsonObject merged = MergedJson ( + """ + { + "Schemes": { + "Base": { "Normal": { "Foreground": "White", "Background": "Blue" } } + }, + "Themes": { "Dark": {} } + } + """); + SchemeManager.ApplyFromConfiguration (merged, "Dark"); + + Assert.True (SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme)); + Assert.Equal (Color.White, baseScheme!.Normal.Foreground); + Assert.Equal (Color.Blue, baseScheme.Normal.Background); + } + + [Fact] + public void ApplyFromConfiguration_RootSchemes_SurviveThemeWithOwnSchemesSection () + { + using SchemeSnapshot snapshot = new (); + + JsonObject merged = MergedJson ( + """ + { + "Schemes": { + "Base": { "Normal": { "Foreground": "White", "Background": "Blue" } } + }, + "Themes": { + "Dark": { + "Schemes": { + "Error": { "Normal": { "Foreground": "Red", "Background": "Black" } } + } + } + } + } + """); + SchemeManager.ApplyFromConfiguration (merged, "Dark"); + + Assert.True (SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme)); + Assert.Equal (Color.White, baseScheme!.Normal.Foreground); + Assert.True (SchemeManager.TryGetScheme ("Error", out Scheme? errorScheme)); + Assert.Equal (Color.Red, errorScheme!.Normal.Foreground); + } + + [Fact] + public void ApplyFromConfiguration_ThemeOverlay_MergesPerRoleWithRootScheme () + { + using SchemeSnapshot snapshot = new (); + + JsonObject merged = MergedJson ( + """ + { + "Schemes": { + "Base": { "Normal": { "Foreground": "White", "Background": "Blue" } } + }, + "Themes": { + "Dark": { + "Schemes": { + "Base": { "Focus": { "Foreground": "Black", "Background": "Gray" } } + } + } + } + } + """); + SchemeManager.ApplyFromConfiguration (merged, "Dark"); + + Assert.True (SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme)); + + // The overlay's Focus wins; the root's Normal survives (root-then-overlay merge). + Assert.Equal (Color.White, baseScheme!.Normal.Foreground); + Assert.Equal (Color.Black, baseScheme.Focus.Foreground); + Assert.Equal (Color.Gray, baseScheme.Focus.Background); + } + + [Fact] + public void ApplyFromConfiguration_InvalidSchemeValue_KeepsHardCodedScheme_AndCollectsError () + { + using SchemeSnapshot snapshot = new (); + TuiJsonErrors.Print (); + + JsonObject merged = MergedJson ( + """ + { + "Schemes": { + "Base": { "Normal": { "Foreground": "NotAColor", "Background": "Blue" } } + } + } + """); + SchemeManager.ApplyFromConfiguration (merged, "Default"); + + Scheme hardCodedBase = SchemeManager.GetHardCodedSchemes () ["Base"]!; + Assert.True (SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme)); + Assert.Equal (hardCodedBase.Normal, baseScheme!.Normal); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains ("Base")); + } + + [Fact] + public void ApplyFromConfiguration_ConfigSourcedScheme_RemovedWhenNoLongerInConfig () + { + using SchemeSnapshot snapshot = new (); + + JsonObject withScheme = MergedJson ( + """ + { + "Schemes": { + "FromConfig": { "Normal": { "Foreground": "White", "Background": "Blue" } } + } + } + """); + SchemeManager.ApplyFromConfiguration (withScheme, "Default"); + Assert.True (SchemeManager.TryGetScheme ("FromConfig", out _)); + + SchemeManager.ApplyFromConfiguration (MergedJson ("""{ }"""), "Default"); + + Assert.False (SchemeManager.TryGetScheme ("FromConfig", out _)); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs deleted file mode 100644 index b6c907962a..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -#nullable enable -using System.Text.Json; - -namespace ConfigurationTests; - -public class ScopeJsonConverterTests -{ - [Theory] - [InlineData ("\"ConfigurationManager.ThrowOnJsonErrors\":true")] - [InlineData ("\"Key.Separator\":\"@\"")] - [InlineData ("\"Themes\":[]")] - [InlineData ("\"Themes\":[{\"themeName\":{}}]")] - [InlineData ("\"Themes\":[{\"themeName\":{\"Dialog.DefaultButtonAlignment\":\"End\"}}]")] - public void RoundTripConversion_Property_Positive (string configPropertyJson) - { - // Arrange - string scopeJson = $"{{{configPropertyJson}}}"; - - // Act - SettingsScope? deserialized = JsonSerializer.Deserialize (scopeJson, TuiSerializerContext.Instance.Options); - - string? json = JsonSerializer.Serialize (deserialized!, TuiSerializerContext.Instance.Options); - - // Strip all whitespace - json = json.Replace (" ", string.Empty); - json = json.Replace ("\n", string.Empty); - json = json.Replace ("\r", string.Empty); - json = json.Replace ("\t", string.Empty); - - // Assert - Assert.Contains (configPropertyJson, json); - } - - [Fact] - public void RoundTripConversion_JsonIncludeProperty_WithNullValue_WritesNull () - { - // Copilot - // Arrange - SettingsScope settingsScope = new () { Schema = null! }; - - // Act - string json = JsonSerializer.Serialize (settingsScope, TuiSerializerContext.Instance.SettingsScope); - JsonDocument document = JsonDocument.Parse (json); - - // Assert - Assert.True (document.RootElement.TryGetProperty ("$schema", out JsonElement schema)); - Assert.Equal (JsonValueKind.Null, schema.ValueKind); - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs deleted file mode 100644 index d19be9a882..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs +++ /dev/null @@ -1,251 +0,0 @@ -#nullable enable -using System.Reflection; - -namespace ConfigurationTests; - -public class ScopeTests -{ - [Fact] - public void Constructor_Scope_Is_Empty () - { - // Arrange - - // Act - var scope = new Scope (); - - // Assert - Assert.NotNull (scope); - Assert.Empty (scope); - } - - // The property key will be "ScopeTests.BoolProperty" - [ConfigurationProperty (Scope = typeof (ScopeTestsScope))] - public static bool? BoolProperty { get; set; } = true; - - // The property key will be "ScopeTests.StringProperty" - [ConfigurationProperty (Scope = typeof (ScopeTestsScope))] - public static string? StringProperty { get; set; } // null - - // The property key will be "ScopeTests.KeyProperty" - [ConfigurationProperty (Scope = typeof (ScopeTestsScope))] - public static Key? KeyProperty { get; set; } = Key.A; - - // The property key will be "ScopeTests.DictionaryProperty" - [ConfigurationProperty (Scope = typeof (ScopeTestsScope))] - public static Dictionary? DictionaryProperty { get; set; } - - public class ScopeTestsScope : Scope - { - } - - // The property key will be "ScopeTests.DictionaryItemProperty1" - [ConfigurationProperty (Scope = typeof (ScopeTestsDictionaryItemScope))] - public static string? DictionaryItemProperty1 { get; set; } // null - - // The property key will be "ScopeTests.DictionaryItemProperty2" - [ConfigurationProperty (Scope = typeof (ScopeTestsDictionaryItemScope))] - public static string? DictionaryItemProperty2 { get; set; } // null - public class ScopeTestsDictionaryItemScope : Scope - { - } - - - [Fact] - public void TestScope_Constructor_Creates_Properties () - { - // Arrange - - // Act - var scope = new ScopeTestsScope (); - scope.LoadHardCodedDefaults (); - - var cache = CM.GetHardCodedConfigPropertyCache (); - - Assert.NotNull (cache); - - // Assert - Assert.NotNull (scope); - Assert.True (scope.ContainsKey ("ScopeTests.BoolProperty")); - - Assert.Equal (typeof (ScopeTests).GetProperty ("BoolProperty"), scope ["ScopeTests.BoolProperty"].PropertyInfo); - Assert.True ((bool)scope ["ScopeTests.BoolProperty"].PropertyValue!); - Assert.True (scope ["ScopeTests.BoolProperty"].HasValue); - - Assert.Equal (typeof (ScopeTests).GetProperty ("StringProperty"), scope ["ScopeTests.StringProperty"].PropertyInfo); - Assert.Null (scope ["ScopeTests.StringProperty"].PropertyValue); - Assert.True (scope ["ScopeTests.StringProperty"].HasValue); - } - - [Fact] - public void UpdateFrom_Unknown_Key_Adds () - { - // Arrange - var scope = new ScopeTestsScope (); - scope.LoadHardCodedDefaults (); - var scopeWithAddedProperty = new ScopeTestsScope (); - scopeWithAddedProperty.TryAdd ("AddedProperty", new ConfigProperty () - { - Immutable = false, - PropertyInfo = scope ["ScopeTests.BoolProperty"].PropertyInfo, // cheat and reuse the same PropertyInfo - PropertyValue = false - }); - - // Act - scope.UpdateFrom (scopeWithAddedProperty); - - // Assert - Assert.NotNull (scope); - Assert.NotEmpty (scope); - Assert.True (scope.ContainsKey ("AddedProperty")); - Assert.Equal (false, scopeWithAddedProperty ["AddedProperty"].PropertyValue); - } - - [Fact] - public void UpdateFrom_HasValue_Updates () - { - // Arrange - ScopeTestsScope originalScope = new ScopeTestsScope (); - originalScope.LoadHardCodedDefaults (); - Assert.Equal (Key.A, originalScope ["ScopeTests.KeyProperty"].PropertyValue); - - ScopeTestsScope sourceScope = new ScopeTestsScope (); - sourceScope.TryAdd ("ScopeTests.KeyProperty", new ConfigProperty - { - Immutable = false, - PropertyInfo = originalScope ["ScopeTests.KeyProperty"].PropertyInfo, - }); - sourceScope ["ScopeTests.KeyProperty"].PropertyValue = Key.B; - - // StringProperty is set to null - Assert.DoesNotContain ("ScopeTests.StringProperty", sourceScope); - - Assert.True (sourceScope ["ScopeTests.KeyProperty"].HasValue); - - // Act - originalScope.UpdateFrom (sourceScope); - - // Assert - Assert.True (originalScope ["ScopeTests.StringProperty"].HasValue); - Assert.True (originalScope ["ScopeTests.KeyProperty"].HasValue); - Assert.Equal (Key.B, originalScope ["ScopeTests.KeyProperty"].PropertyValue); - } - - [Fact] - public void UpdateFrom_HasValue_Dictionary_Updates () - { - // Arrange - ScopeTestsScope originalScope = new ScopeTestsScope (); - originalScope.LoadHardCodedDefaults (); - Assert.Null (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue); - - // QUESTION: Should this be done automatically? - originalScope ["ScopeTests.DictionaryProperty"].PropertyValue = new Dictionary (); - - ScopeTestsScope sourceScope = new ScopeTestsScope (); - sourceScope.LoadHardCodedDefaults (); - sourceScope ["ScopeTests.DictionaryProperty"].PropertyValue = new Dictionary () - { - { "item1", ConfigProperty.GetAllConfigProperties () ["ScopeTests.DictionaryItemProperty1"] }, - { "item2", ConfigProperty.GetAllConfigProperties () ["ScopeTests.DictionaryItemProperty2"] } - }; - - Assert.True (sourceScope ["ScopeTests.KeyProperty"].HasValue); - Assert.True (sourceScope ["ScopeTests.DictionaryProperty"].HasValue); - - Dictionary? sourceDict = sourceScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary; - sourceDict! ["item1"].Immutable = false; - sourceDict ["item2"].Immutable = false; - Assert.NotNull (sourceDict); - Assert.Equal (2, sourceDict!.Count); - Assert.True (sourceDict.ContainsKey ("item1")); - Assert.True (sourceDict.ContainsKey ("item2")); - Assert.False (sourceDict ["item1"].HasValue); - Assert.False (sourceDict ["item1"].Immutable); - - // Update the original scope with the source scope, which has no values - originalScope.UpdateFrom (sourceScope); - - // Confirm original is unchanged - Assert.NotNull (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue); - Dictionary? destDict = originalScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary; - Assert.NotNull (destDict); - Assert.Empty (destDict); - Assert.False (destDict.ContainsKey ("item1")); - Assert.False (destDict.ContainsKey ("item2")); - - // Confirm source is unchanged - sourceDict ["item1"].PropertyValue = "hello"; - Assert.True (sourceDict ["item1"].HasValue); - Assert.Equal ("hello", sourceDict ["item1"].PropertyValue); - - // Now update the original scope with the source scope again - originalScope.UpdateFrom (sourceScope); - - // Confirm the original has been updated with only the values in source that have been set - Assert.NotNull (originalScope ["ScopeTests.DictionaryProperty"].PropertyValue); - destDict = originalScope ["ScopeTests.DictionaryProperty"].PropertyValue as Dictionary; - - // 1 item (item1) should now be in the original scope - Assert.Single (destDict!); - Assert.True (destDict! ["item1"].HasValue); - Assert.Equal ("hello", destDict ["item1"].PropertyValue); - - originalScope.Apply (); - - // Verify apply worked - Assert.Equal ("hello", DictionaryProperty? ["item1"].PropertyValue); - - // The item property should not have had its value set - Assert.Null (DictionaryItemProperty1); - - DictionaryItemProperty1 = null; - } - - #region Concurrency Testing - - [Fact] - public void Scope_Concurrent_Additions () - { - // Arrange - var scope = new Scope (); - int threadCount = 10; - int itemsPerThread = 100; - var tasks = new List (); - - // Act - for (int t = 0; t < threadCount; t++) - { - int threadId = t; - - tasks.Add (Task.Run (() => - { - for (var i = 0; i < itemsPerThread; i++) - { - var key = $"Thread{threadId}_Item{i}"; - scope.TryAdd (key, new ConfigProperty { PropertyValue = i }); - } - }, - TestContext.Current.CancellationToken)); - } - -#pragma warning disable xUnit1031 - Task.WaitAll (tasks.ToArray (), TestContext.Current.CancellationToken); -#pragma warning restore xUnit1031 - - // Assert - Assert.Equal (threadCount * itemsPerThread, scope.Count); - for (int t = 0; t < threadCount; t++) - { - for (int i = 0; i < itemsPerThread; i++) - { - string key = $"Thread{t}_Item{i}"; - Assert.True (scope.ContainsKey (key)); - Assert.Equal (i, scope [key].PropertyValue); - } - } - } - - - #endregion - -} diff --git a/Tests/UnitTestsParallelizable/Configuration/SettingsFacadeSnapshot.cs b/Tests/UnitTestsParallelizable/Configuration/SettingsFacadeSnapshot.cs new file mode 100644 index 0000000000..48cc01fea9 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/SettingsFacadeSnapshot.cs @@ -0,0 +1,100 @@ +// Grok - grok-4.6 +using Terminal.Gui.App; +using Terminal.Gui.Configuration; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; + +namespace ConfigurationTests; + +/// +/// Captures and restores every process-wide settings facade that +/// can mutate. +/// +internal sealed class SettingsFacadeSnapshot : IDisposable +{ + private readonly ApplicationSettings _application = ApplicationSettings.Defaults; + private readonly DriverSettings _driver = DriverSettings.Defaults; + private readonly FileDialogSettings _fileDialog = FileDialogSettings.Defaults; + private readonly FileDialogStyleSettings _fileDialogStyle = FileDialogStyleSettings.Defaults; + private readonly KeySettings _key = KeySettings.Defaults; + private readonly TraceSettings _trace = TraceSettings.Defaults; + private readonly ThemeSettings _theme = ThemeSettings.Defaults; + private readonly ButtonSettings _button = ButtonSettings.Current; + private readonly CheckBoxSettings _checkBox = CheckBoxSettings.Current; + private readonly CharMapSettings _charMap = CharMapSettings.Current; + private readonly DialogSettings _dialog = DialogSettings.Current; + private readonly FrameViewSettings _frameView = FrameViewSettings.Current; + private readonly HexViewSettings _hexView = HexViewSettings.Current; + private readonly LinearRangeSettings _linearRange = LinearRangeSettings.Current; + private readonly MenuBarSettings _menuBar = MenuBarSettings.Current; + private readonly MenuSettings _menu = MenuSettings.Current; + private readonly MessageBoxSettings _messageBox = MessageBoxSettings.Current; + private readonly NerdFontsSettings _nerdFonts = NerdFontsSettings.Current; + private readonly PopoverMenuSettings _popoverMenu = PopoverMenuSettings.Current; + private readonly SelectorBaseSettings _selectorBase = SelectorBaseSettings.Current; + private readonly StatusBarSettings _statusBar = StatusBarSettings.Current; + private readonly TextFieldSettings _textField = TextFieldSettings.Current; + private readonly TextViewSettings _textView = TextViewSettings.Current; + private readonly WindowSettings _window = WindowSettings.Current; + private readonly GlyphSettings _glyphs = GlyphSettings.Current; + private readonly Dictionary? _applicationKeyBindings = CloneCommandBindings (Application.DefaultKeyBindings); + private readonly Dictionary? _viewKeyBindings = CloneCommandBindings (View.DefaultKeyBindings); + private readonly Dictionary>? _viewTypeKeyBindings = CloneViewKeyBindings (View.ViewKeyBindings); + + private readonly (Dictionary?, Dictionary?, + Dictionary>?) _keyBindingOverlayTracking = KeyBindingConfiguration.SnapshotOverlayTracking (); + + public void Dispose () + { + ApplicationSettings.Defaults = _application; + DriverSettings.Defaults = _driver; + FileDialogSettings.Defaults = _fileDialog; + FileDialogStyleSettings.Defaults = _fileDialogStyle; + KeySettings.Defaults = _key; + TraceSettings.Defaults = _trace; + ThemeSettings.Defaults = _theme; + ButtonSettings.Current = _button; + CheckBoxSettings.Current = _checkBox; + CharMapSettings.Current = _charMap; + DialogSettings.Current = _dialog; + FrameViewSettings.Current = _frameView; + HexViewSettings.Current = _hexView; + LinearRangeSettings.Current = _linearRange; + MenuBarSettings.Current = _menuBar; + MenuSettings.Current = _menu; + MessageBoxSettings.Current = _messageBox; + NerdFontsSettings.Current = _nerdFonts; + PopoverMenuSettings.Current = _popoverMenu; + SelectorBaseSettings.Current = _selectorBase; + StatusBarSettings.Current = _statusBar; + TextFieldSettings.Current = _textField; + TextViewSettings.Current = _textView; + WindowSettings.Current = _window; + GlyphSettings.Current = _glyphs; + Application.DefaultKeyBindings = CloneCommandBindings (_applicationKeyBindings); + View.DefaultKeyBindings = CloneCommandBindings (_viewKeyBindings); + View.ViewKeyBindings = CloneViewKeyBindings (_viewTypeKeyBindings); + KeyBindingConfiguration.RestoreOverlayTracking (_keyBindingOverlayTracking); + } + + private static Dictionary? CloneCommandBindings (Dictionary? source) => + source is null ? null : new (source); + + private static Dictionary>? CloneViewKeyBindings ( + Dictionary>? source) + { + if (source is null) + { + return null; + } + + Dictionary> clone = new (StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair> pair in source) + { + clone [pair.Key] = new (pair.Value); + } + + return clone; + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs deleted file mode 100644 index 704cb2c2ba..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs +++ /dev/null @@ -1,16 +0,0 @@ - -namespace ConfigurationTests; - -public class SettingsScopeTests -{ - [Fact] - public void Schema_Is_Correct () - { - // Arrange - var settingsScope = new SettingsScope (); - // Act - - // Assert - Assert.Equal ("https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", settingsScope.Schema); - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs b/Tests/UnitTestsParallelizable/Configuration/SettingsTests.cs similarity index 78% rename from Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs rename to Tests/UnitTestsParallelizable/Configuration/SettingsTests.cs index ab20a9d371..b34f2a59d4 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SettingsTests.cs @@ -7,7 +7,7 @@ namespace ConfigurationTests; /// Tests for the MEC-based settings POCOs and . [Collection ("StaticSettingsTests")] -public class MecSettingsTests +public class SettingsTests { [Fact] public void ApplicationSettings_Defaults_HasCorrectValues () @@ -25,7 +25,7 @@ public void DriverSettings_Defaults_HasCorrectValues () DriverSettings settings = new (); Assert.False (settings.Force16Colors); - Assert.Equal (SizeDetectionMode.AnsiQuery, settings.SizeDetection); + Assert.Equal (SizeDetectionMode.Polling, settings.SizeDetection); } [Fact] @@ -77,20 +77,11 @@ public void CheckBoxSettings_Defaults_HasCorrectValues () [Fact] public void StaticFacade_CanBeOverridden () { - // Save original - ButtonSettings original = ButtonSettings.Defaults; - - try - { - ButtonSettings custom = new () { DefaultShadow = ShadowStyles.None }; - ButtonSettings.Defaults = custom; - - Assert.Equal (ShadowStyles.None, ButtonSettings.Defaults.DefaultShadow); - } - finally - { - ButtonSettings.Defaults = original; - } + using SettingsFacadeSnapshot snapshot = new (); + ButtonSettings custom = new () { DefaultShadow = ShadowStyles.None }; + ButtonSettings.Current = custom; + + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); } [Fact] @@ -160,30 +151,19 @@ public void TuiConfigurationBuilder_RuntimeConfig_InvalidatesCache () [Fact] public void TuiConfigurationBuilder_ApplyToStaticFacades_UpdatesDefaults () { - // Save originals - ApplicationSettings originalApp = ApplicationSettings.Defaults; - DriverSettings originalDriver = DriverSettings.Defaults; - - try - { - TuiConfigurationBuilder tuiBuilder = new (); - - tuiBuilder.RuntimeConfig = """ - { - "Application": { "ForceDriver": "dotnet" }, - "Driver": { "Force16Colors": true } - } - """; - tuiBuilder.ApplyToStaticFacades (); - - Assert.Equal ("dotnet", ApplicationSettings.Defaults.ForceDriver); - Assert.True (DriverSettings.Defaults.Force16Colors); - } - finally - { - ApplicationSettings.Defaults = originalApp; - DriverSettings.Defaults = originalDriver; - } + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Application": { "ForceDriver": "dotnet" }, + "Driver": { "Force16Colors": true } + } + """; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal ("dotnet", ApplicationSettings.Defaults.ForceDriver); + Assert.True (DriverSettings.Defaults.Force16Colors); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs deleted file mode 100644 index bad5ec8d3b..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ /dev/null @@ -1,485 +0,0 @@ -using System.Reflection; - -namespace ConfigurationTests; - -public class SourcesManagerTests -{ - #region Update (Stream) - - [Fact] - public void Load_WithNullSettingsScope_ReturnsFalse () - { - // Arrange - var sourcesManager = new SourcesManager (); - var stream = new MemoryStream (); - var source = "Load_WithNullSettingsScope_ReturnsFalse"; - var location = ConfigLocations.AppCurrent; - - // Act - bool result = sourcesManager.Load (null, stream, source, location); - - // Assert - Assert.False (result); - } - - [Fact] - public void Load_WithValidStream_UpdatesSettingsScope () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = false; - - var json = """ - { - "ConfigurationManager.ThrowOnJsonErrors": true - } - """; - var location = ConfigLocations.HardCoded; - var source = "Load_WithValidStream_UpdatesSettingsScope"; - - var stream = new MemoryStream (); - var writer = new StreamWriter (stream); - writer.Write (json); - writer.Flush (); - stream.Position = 0; - - // Act - bool result = sourcesManager.Load (settingsScope, stream, source, location); - - // Assert - Assert.True (result); - Assert.Equal (true, settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue); - Assert.Contains (source, sourcesManager.Sources.Values); - } - - #endregion - - #region Update (FilePath) - - [Fact] - public void Load_WithNonExistentFile_AddsToSourcesAndReturnsTrue () - { - // Arrange - var sourcesManager = new SourcesManager (); - - var settingsScope = new SettingsScope (); - var filePath = "nonexistent.json"; - var location = ConfigLocations.AppCurrent; - - // Act - bool result = sourcesManager.Load (settingsScope, filePath, location); - - // Assert - Assert.True (result); - Assert.Contains (filePath, sourcesManager.Sources.Values); - } - - [Fact] - public void Load_WithValidFile_UpdatesSettingsScope () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = false; - - var json = """ - { - "ConfigurationManager.ThrowOnJsonErrors": true - } - """; - string source = Path.GetTempFileName (); - var location = ConfigLocations.HardCoded; - - File.WriteAllText (source, json); - - try - { - // Act - bool result = sourcesManager.Load (settingsScope, source, location); - - // Assert - Assert.True (result); - Assert.Equal (true, settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue); - Assert.Contains (source, sourcesManager.Sources.Values); - } - finally - { - // Cleanup - File.Delete (source); - } - } - - [Fact] - public void Load_WithIOException_RetriesAndFailsGracefully () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - var filePath = "locked.json"; - var json = "{\"Application.UseSystemConsole\": true}"; - File.WriteAllText (filePath, json); - - var location = ConfigLocations.AppCurrent; - - try - { - using FileStream fileStream = File.Open (filePath, FileMode.Open, FileAccess.Read, FileShare.None); - - // Act - bool result = sourcesManager.Load (settingsScope, filePath, location); - - // Assert - Assert.False (result); - } - finally - { - // Cleanup - File.Delete (filePath); - } - } - - #endregion - - #region Update (Json String) - - [Fact] - public void Load_WithNullOrEmptyJson_ReturnsFalse () - { - // Arrange - var sourcesManager = new SourcesManager (); - - var settingsScope = new SettingsScope (); - var source = "Load_WithNullOrEmptyJson_ReturnsFalse"; - var location = ConfigLocations.AppCurrent; - - // Act - bool resultWithNull = sourcesManager.Load (settingsScope, json: null, source, location); - bool resultWithEmpty = sourcesManager.Load (settingsScope, string.Empty, source, location); - - // Assert - Assert.False (resultWithNull); - Assert.False (resultWithEmpty); - } - - [Fact] - public void Load_WithValidJson_UpdatesSettingsScope () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = false; - - var json = """ - { - "ConfigurationManager.ThrowOnJsonErrors": true - } - """; - var source = "Load_WithValidJson_UpdatesSettingsScope"; - var location = ConfigLocations.HardCoded; - - // Act - bool result = sourcesManager.Load (settingsScope, json, source, location); - - // Assert - Assert.True (result); - Assert.Equal (true, settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue); - Assert.Contains (source, sourcesManager.Sources.Values); - } - - //[Fact] - //public void Update_WithValidJson_UpdatesThemeScope () - //{ - // // Arrange - // var sourcesManager = new SourcesManager (); - // var themeScope = new ThemeScope (); - // themeScope.LoadHardCodedDefaults (); - // themeScope ["Button.DefaultShadowStyle"].PropertyValue = ShadowStyle.Opaque; - - // var json = """ - // { - // "Button.DefaultShadowStyle": "None" - // } - // """; - // var source = "Update_WithValidJson_UpdatesThemeScope"; - // var location = ConfigLocations.HardCoded; - - // // Act - // bool result = sourcesManager.Load (themeScope, json, source, location); - - // // Assert - // Assert.True (result); - // Assert.Equal (Key.Z.WithCtrl, themeScope ["Application.QuitKey"].PropertyValue as Key); - // Assert.Contains (source, sourcesManager.Sources.Values); - //} - - #endregion - - #region Load - - [Fact] - public void Load_WithNullResourceName_ReturnsFalse () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = false; - - var assembly = Assembly.GetExecutingAssembly (); - var location = ConfigLocations.AppResources; - - // Act - bool result = sourcesManager.Load (settingsScope, assembly, string.Empty, location); - - // Assert - Assert.False (result); - } - - [Fact] - public void Load_WithValidResource_UpdatesSettingsScope () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - var assembly = Assembly.GetAssembly (typeof (ConfigurationManager)); - var resourceName = "Terminal.Gui.Resources.config.json"; - var location = ConfigLocations.LibraryResources; - - // Act - bool result = sourcesManager.Load (settingsScope, assembly!, resourceName, location); - - // Assert - Assert.True (result); - - // Verify settingsScope is updated as expected - } - - [Fact] - public void Load_Runtime_Overrides () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - var assembly = Assembly.GetAssembly (typeof (ConfigurationManager)); - var resourceName = "Terminal.Gui.Resources.config.json"; - var location = ConfigLocations.LibraryResources; - sourcesManager.Load (settingsScope, assembly!, resourceName, location); - - Assert.Equal (false, settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue); - - var runtimeJson = """ - { - "ConfigurationManager.ThrowOnJsonErrors": true - } - """; - var runtimeSource = "runtime.json"; - var runtimeLocation = ConfigLocations.Runtime; - var runtimeStream = new MemoryStream (); - var writer = new StreamWriter (runtimeStream); - writer.Write (runtimeJson); - writer.Flush (); - runtimeStream.Position = 0; - - // Act - bool result = sourcesManager.Load (settingsScope, runtimeStream, runtimeSource, runtimeLocation); - - // Assert - Assert.True (result); - - // Verify settingsScope is updated as expected - Assert.Equal (true, settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue); - } - - #endregion - - #region ToJson and ToStream - - [Fact] - public void ToJson_WithValidScope_ReturnsJsonString () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = true; - - // Act - string json = sourcesManager.ToJson (settingsScope); - - // Assert - Assert.Contains ("\"ConfigurationManager.ThrowOnJsonErrors\": true", json); - } - - [Fact] - public void ToStream_WithValidScope_ReturnsStream () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - settingsScope.LoadHardCodedDefaults (); - settingsScope ["ConfigurationManager.ThrowOnJsonErrors"].PropertyValue = true; - - // Act - var stream = sourcesManager.ToStream (settingsScope); - - // Assert - Assert.NotNull (stream); - stream.Position = 0; - StreamReader reader = new (stream); - string json = reader.ReadToEnd (); - Assert.Contains ("\"ConfigurationManager.ThrowOnJsonErrors\": true", json); - } - - #endregion - - #region Sources Dictionary Tests - - [Fact] - public void Sources_Dictionary_IsInitializedEmpty () - { - // Arrange & Act - var sourcesManager = new SourcesManager (); - - // Assert - Assert.NotNull (sourcesManager.Sources); - Assert.Empty (sourcesManager.Sources); - } - - [Fact] - public void Load_WhenCalledMultipleTimes_MaintainsLastSourceForLocation () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - // Act - Update with first source for location - var firstSource = "first.json"; - sourcesManager.Load (settingsScope, """{"ConfigurationManager.ThrowOnJsonErrors": true}""", firstSource, ConfigLocations.Runtime); - - // Update with second source for same location - var secondSource = "second.json"; - sourcesManager.Load (settingsScope, """{"ConfigurationManager.ThrowOnJsonErrors": false}""", secondSource, ConfigLocations.Runtime); - - // Assert - Only the last source should be stored for the location - Assert.Single (sourcesManager.Sources); - Assert.Equal (secondSource, sourcesManager.Sources [ConfigLocations.Runtime]); - } - - [Fact] - public void Load_WithDifferentLocations_AddsAllSourcesToCollection () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - ConfigLocations [] locations = [ConfigLocations.LibraryResources, ConfigLocations.Runtime, ConfigLocations.AppCurrent, ConfigLocations.GlobalHome]; - - // Act - Update with different sources for different locations - foreach (ConfigLocations location in locations) - { - var source = $"config-{location}.json"; - sourcesManager.Load (settingsScope, """{"ConfigurationManager.ThrowOnJsonErrors": true}""", source, location); - } - - // Assert - Assert.Equal (locations.Length, sourcesManager.Sources.Count); - - foreach (ConfigLocations location in locations) - { - Assert.Contains (location, sourcesManager.Sources.Keys); - Assert.Equal ($"config-{location}.json", sourcesManager.Sources [location]); - } - } - - [Fact] - public void Load_AddsResourceSourceToCollection () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - var assembly = Assembly.GetAssembly (typeof (ConfigurationManager)); - var resourceName = "Terminal.Gui.Resources.config.json"; - var location = ConfigLocations.LibraryResources; - - // Act - bool result = sourcesManager.Load (settingsScope, assembly!, resourceName, location); - - // Assert - Assert.True (result); - Assert.Contains (location, sourcesManager.Sources.Keys); - Assert.Equal ($"resource://[{assembly!.GetName ().Name}]/{resourceName}", sourcesManager.Sources [location]); - } - - [Fact] - public void Load_WithNonExistentFileAndDifferentLocations_TracksAllSources () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - // Define multiple files and locations - Dictionary fileLocations = new (StringComparer.InvariantCultureIgnoreCase) - { - { "file1.json", ConfigLocations.AppCurrent }, { "file2.json", ConfigLocations.GlobalHome }, { "file3.json", ConfigLocations.AppHome } - }; - - // Act - foreach (KeyValuePair pair in fileLocations) - { - sourcesManager.Load (settingsScope, pair.Key, pair.Value); - } - - // Assert - Assert.Equal (fileLocations.Count, sourcesManager.Sources.Count); - - foreach (KeyValuePair pair in fileLocations) - { - Assert.Contains (pair.Value, sourcesManager.Sources.Keys); - Assert.Equal (pair.Key, sourcesManager.Sources [pair.Value]); - } - } - - [Fact] - public void Sources_IsPreservedAcrossOperations () - { - // Arrange - var sourcesManager = new SourcesManager (); - var settingsScope = new SettingsScope (); - - // First operation - file update - var filePath = "testfile.json"; - var location1 = ConfigLocations.AppCurrent; - sourcesManager.Load (settingsScope, filePath, location1); - - // Second operation - json string update - var jsonSource = "jsonstring"; - var location2 = ConfigLocations.Runtime; - sourcesManager.Load (settingsScope, """{"ConfigurationManager.ThrowOnJsonErrors": true}""", jsonSource, location2); - - // Perform a stream operation - var streamSource = "streamdata"; - var location3 = ConfigLocations.GlobalCurrent; - var stream = new MemoryStream (); - var writer = new StreamWriter (stream); - writer.Write ("""{"ConfigurationManager.ThrowOnJsonErrors": true}"""); - writer.Flush (); - stream.Position = 0; - sourcesManager.Load (settingsScope, stream, streamSource, location3); - - // Assert - all sources should be preserved - Assert.Equal (3, sourcesManager.Sources.Count); - Assert.Equal (filePath, sourcesManager.Sources [location1]); - Assert.Equal (jsonSource, sourcesManager.Sources [location2]); - Assert.Equal (streamSource, sourcesManager.Sources [location3]); - } - - #endregion -} diff --git a/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs new file mode 100644 index 0000000000..2722ae107a --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/ThemeOverlayMergeTests.cs @@ -0,0 +1,290 @@ +// Copilot - Claude Opus 4.7 +// Grok - grok-4.6 + +using Terminal.Gui.Configuration; +using Terminal.Gui.Drawing; + +namespace ConfigurationTests; + +/// +/// End-to-end tests for the A2.1 two-pass MEC theme overlay applied through +/// . +/// +/// +/// +/// The contract under test: BindThemeScope<T> binds the root section first, then overlays +/// Themes:{active}:{section}. Properties present only in the root must survive; properties present +/// in the overlay must win. This mirrors legacy CM Scope.Apply property-level merge semantics. +/// +/// +[Collection ("StaticSettingsTests")] +public class ThemeOverlayMergeTests +{ + /// + /// When the theme overlay only mentions one property of a ThemeScope POCO, the other properties keep their + /// root-section values (not the compile-time defaults, not ). + /// + [Fact] + public void ApplyToStaticFacades_ThemeOverlay_PreservesRootDefaultsForUnmentionedProperties () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "Custom", + "Dialog": { + "DefaultShadow": "Opaque", + "DefaultBorderStyle": "Double", + "DefaultButtonAlignment": "Start" + }, + "Themes": { + "Custom": { + "Dialog": { + "DefaultBorderStyle": "Single" + } + } + } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (LineStyle.Single, DialogSettings.Current.DefaultBorderStyle); + Assert.Equal (ShadowStyles.Opaque, DialogSettings.Current.DefaultShadow); + Assert.Equal (Alignment.Start, DialogSettings.Current.DefaultButtonAlignment); + } + + /// + /// When no theme overlay exists for a POCO, the root section's values are applied verbatim. + /// + [Fact] + public void ApplyToStaticFacades_NoOverlay_UsesRootValuesAsIs () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "Custom", + "Button": { + "DefaultShadow": "None" + }, + "Themes": { "Custom": { } } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + } + + /// + /// The atomic-swap pattern produces a new reference on each apply, never + /// mutates the existing instance in place. A reader that captured the prior reference still sees the prior + /// values. + /// + [Fact] + public void ApplyToStaticFacades_AtomicSwap_DoesNotMutatePriorReference () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + tuiBuilder.RuntimeConfig = """{ "Button": { "DefaultShadow": "Transparent" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + ButtonSettings captured = ButtonSettings.Current; + Assert.Equal (ShadowStyles.Transparent, captured.DefaultShadow); + + tuiBuilder.RuntimeConfig = """{ "Button": { "DefaultShadow": "None" } }"""; + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal (ShadowStyles.Transparent, captured.DefaultShadow); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + Assert.NotSame (captured, ButtonSettings.Current); + } + + // Grok - grok-4.6 + [Fact] + public void SwitchTheme_AppliesOverlay_DoesNotResetActiveThemeToConfigDefault () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "Default", + "Button": { "DefaultShadow": "Opaque" }, + "Glyphs": { "CheckStateChecked": "☑" }, + "Themes": { + "EightBit": { + "Button": { "DefaultShadow": "None" }, + "Glyphs": { "CheckStateChecked": "X" } + } + } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + Assert.Equal (ShadowStyles.Opaque, ButtonSettings.Current.DefaultShadow); + + MecThemeManager manager = new (tuiBuilder); + Assert.True (manager.SwitchTheme ("EightBit")); + Assert.Equal ("EightBit", manager.CurrentThemeName); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + Assert.Equal ((System.Text.Rune)'X', Glyphs.CheckStateChecked); + } + + // Grok - grok-4.6 + [Fact] + public void SwitchTheme_ThemeChangedHandler_SeesNewOverlay () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "Default", + "Button": { "DefaultShadow": "Opaque" }, + "Themes": { + "EightBit": { + "Button": { "DefaultShadow": "None" } + } + } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + ShadowStyles seenInHandler = ShadowStyles.Opaque; + MecThemeManager manager = new (tuiBuilder); + manager.ThemeChanged += (_, _) => seenInHandler = ButtonSettings.Current.DefaultShadow; + + Assert.True (manager.SwitchTheme ("EightBit")); + Assert.Equal (ShadowStyles.None, seenInHandler); + } + + // Grok - grok-4.6 + [Fact] + public void SwitchTheme_IgnoresThemeNameCase () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "Default", + "Themes": { "Dark": { "Button": { "DefaultShadow": "None" } } } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + MecThemeManager manager = new (tuiBuilder); + + Assert.True (manager.SwitchTheme ("dark")); + Assert.Equal (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + } + + // Claude - Fable 5 + [Fact] + public void ApplyToStaticFacades_ConfigChangesActiveTheme_RaisesThemeChanged () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "A", + "Themes": { "A": {}, "B": {} } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + MecThemeManager manager = new (tuiBuilder); + Assert.True (manager.SwitchTheme ("B")); + Assert.Equal ("B", manager.CurrentThemeName); + + string? raisedTheme = null; + manager.ThemeChanged += Handler; + + try + { + // Re-applying configuration snaps the theme back to "A"; subscribers must be told. + tuiBuilder.ApplyToStaticFacades (); + } + finally + { + manager.ThemeChanged -= Handler; + } + + Assert.Equal ("A", manager.CurrentThemeName); + Assert.Equal ("A", raisedTheme); + + void Handler (object? sender, EventArgs e) { raisedTheme = e.Value; } + } + + // Claude - Fable 5 + // Re-applying configuration re-publishes every facade wholesale, so subscribers must be notified + // even when the theme NAME is unchanged — a hot reload that edits the current theme's colors + // would otherwise update state without anything repainting. + [Fact] + public void ApplyToStaticFacades_ActiveThemeUnchanged_StillRaisesThemeChanged () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "A", + "Themes": { "A": {} } + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + string? raisedTheme = null; + MecThemeManager manager = new (tuiBuilder); + manager.ThemeChanged += Handler; + + try + { + tuiBuilder.ApplyToStaticFacades (); + } + finally + { + manager.ThemeChanged -= Handler; + } + + Assert.Equal ("A", raisedTheme); + + void Handler (object? sender, EventArgs e) { raisedTheme = e.Value; } + } + + // Grok - grok-4.6 + [Fact] + public void ApplyToStaticFacades_LegacyArrayThemeShape_IsNotApplied () + { + using SettingsFacadeSnapshot snapshot = new (); + TuiConfigurationBuilder tuiBuilder = new (); + + tuiBuilder.RuntimeConfig = """ + { + "Theme": "EightBit", + "Glyphs": { "CheckStateChecked": "☑" }, + "Themes": [ + { + "EightBit": { + "Glyphs.CheckStateChecked": "X", + "Button.DefaultShadow": "None" + } + } + ] + } + """; + + tuiBuilder.ApplyToStaticFacades (); + + Assert.Equal ((System.Text.Rune)'☑', Glyphs.CheckStateChecked); + Assert.NotEqual (ShadowStyles.None, ButtonSettings.Current.DefaultShadow); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs deleted file mode 100644 index 811010e431..0000000000 --- a/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics; -using System.Text.Json; - -namespace ConfigurationTests; - -public class ThemeScopeTests -{ - [Fact] - public void Schemes_Property_Exists () - { - var scope = new ThemeScope (); - scope.LoadHardCodedDefaults(); - - Assert.NotEmpty (scope); - - Assert.NotNull(scope ["Schemes"].PropertyValue); - - Assert.NotEmpty (scope); - } - - //[Fact] - //public void RetrievValues_Gets_Default_Values () - //{ - // // Need to call Initialize to setup readonly statics - // ConfigurationManager.Initialize (); - - // var themeScope = new ThemeScope (); - - // Assert.NotEmpty (themeScope); - - // // Schemes exists, but is not initialized - // Assert.Null (themeScope ["Schemes"].PropertyValue); - - // themeScope.RetrieveValues (); - - // Assert.NotEmpty (themeScope); - - // // Schemes exists, and has correct # of eleements - // var schemes = themeScope ["Schemes"].PropertyValue as Dictionary; - // Assert.NotNull (schemes); - // Assert.Equal (5, schemes!.Count); - - // // Base has correct values - // var baseSchemee = schemes ["Base"]; - // Assert.Equal (new Attribute(Color.White, Color.Blue), baseSchemee.Normal); - - //} -} diff --git a/Tests/UnitTestsParallelizable/Configuration/MecThemeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeTests.cs similarity index 89% rename from Tests/UnitTestsParallelizable/Configuration/MecThemeTests.cs rename to Tests/UnitTestsParallelizable/Configuration/ThemeTests.cs index c8cae44c77..01e9410215 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MecThemeTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ThemeTests.cs @@ -2,12 +2,13 @@ using Terminal.Gui.App; using Terminal.Gui.Configuration; +using Terminal.Gui.Drawing; namespace ConfigurationTests; /// Tests for the MEC-backed theme and scheme manager interfaces. [Collection ("StaticSettingsTests")] -public class MecThemeTests +public class ThemeTests { [Fact] public void ThemeSettings_Defaults_HasDefaultThemeName () @@ -53,9 +54,9 @@ public void MecThemeManager_SwitchTheme_NonExistent_ReturnsFalse () } [Fact] - public void MecSchemeManager_SchemeNames_DoesNotThrow () + public void SchemeManager_SchemeNames_DoesNotThrow () { - MecSchemeManager manager = new (); + ISchemeManager manager = new SchemeManager (); IReadOnlyList names = manager.SchemeNames; // Should return a list without throwing (may be empty if CM state is odd in parallel tests) @@ -63,9 +64,9 @@ public void MecSchemeManager_SchemeNames_DoesNotThrow () } [Fact] - public void MecSchemeManager_GetScheme_ReturnsNull_ForInvalid () + public void SchemeManager_GetScheme_ReturnsNull_ForInvalid () { - MecSchemeManager manager = new (); + ISchemeManager manager = new SchemeManager (); Scheme? scheme = manager.GetScheme ("NonExistentScheme12345"); Assert.Null (scheme); } @@ -79,11 +80,11 @@ public void IThemeManager_Interface_IsImplementedByMecThemeManager () } [Fact] - public void ISchemeManager_Interface_IsImplementedByMecSchemeManager () + public void ISchemeManager_Interface_IsImplementedBySchemeManager () { TuiConfigurationBuilder builder = new (); ISchemeManager manager = builder.SchemeManager; - Assert.IsType (manager); + Assert.IsType (manager); } // Copilot - Claude Opus 4.7 diff --git a/Tests/UnitTestsParallelizable/Configuration/TuiConfigSchemaTests.cs b/Tests/UnitTestsParallelizable/Configuration/TuiConfigSchemaTests.cs new file mode 100644 index 0000000000..0f776a3aba --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/TuiConfigSchemaTests.cs @@ -0,0 +1,82 @@ +// Grok - grok-4.6 +using System.Text.Json; + +namespace ConfigurationTests; + +/// +/// Locks the shipped docfx/schemas/tui-config-schema.json to the nested MEC shape. +/// +public class TuiConfigSchemaTests +{ + private static readonly JsonDocumentOptions JsonOptions = new () + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + [Fact] + public void Schema_UsesNestedMecShape () + { + using JsonDocument doc = JsonDocument.Parse (File.ReadAllText (FindSchemaPath ()), JsonOptions); + JsonElement properties = doc.RootElement.GetProperty ("properties"); + + Assert.True (properties.TryGetProperty ("Application", out _)); + Assert.True (properties.TryGetProperty ("View", out _)); + Assert.True (properties.TryGetProperty ("Button", out _)); + Assert.True (properties.TryGetProperty ("Glyphs", out _)); + Assert.True (properties.TryGetProperty ("PopoverMenu", out _)); + Assert.True (properties.TryGetProperty ("Themes", out JsonElement themes)); + Assert.Equal ("object", themes.GetProperty ("type").GetString ()); + Assert.False (themes.TryGetProperty ("items", out _)); + + Assert.False (properties.TryGetProperty ("ConfigurationManager.ThrowOnJsonErrors", out _)); + Assert.False (properties.TryGetProperty ("Button.DefaultShadow", out _)); + Assert.False (properties.TryGetProperty ("Key.Separator", out _)); + + JsonElement definitions = doc.RootElement.GetProperty ("definitions"); + JsonElement applicationSettings = definitions.GetProperty ("applicationSettings").GetProperty ("properties"); + Assert.True (applicationSettings.TryGetProperty ("DefaultKeyBindings", out _)); + JsonElement viewSettings = definitions.GetProperty ("viewSettings").GetProperty ("properties"); + Assert.True (viewSettings.TryGetProperty ("DefaultKeyBindings", out _)); + Assert.True (viewSettings.TryGetProperty ("ViewKeyBindings", out _)); + } + + [Fact] + public void LibraryConfigJson_PointsAtHostedSchema_AndUsesNestedThemes () + { + using JsonDocument doc = JsonDocument.Parse (File.ReadAllText (FindLibraryConfigPath ()), JsonOptions); + JsonElement root = doc.RootElement; + + Assert.Equal ( + "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", + root.GetProperty ("$schema").GetString ()); + Assert.Equal (JsonValueKind.Object, root.GetProperty ("Themes").ValueKind); + Assert.Equal (JsonValueKind.Object, root.GetProperty ("Application").ValueKind); + Assert.False (root.TryGetProperty ("ConfigurationManager.ThrowOnJsonErrors", out _)); + } + + private static string FindSchemaPath () => + FindRepoFile (Path.Combine ("docfx", "schemas", "tui-config-schema.json")); + + private static string FindLibraryConfigPath () => + FindRepoFile (Path.Combine ("Terminal.Gui", "Resources", "config.json")); + + private static string FindRepoFile (string relativePath) + { + DirectoryInfo? dir = new (AppContext.BaseDirectory); + + while (dir is not null) + { + string candidate = Path.Combine (dir.FullName, relativePath); + + if (File.Exists (candidate)) + { + return candidate; + } + + dir = dir.Parent; + } + + throw new FileNotFoundException (relativePath); + } +} diff --git a/Tests/UnitTestsParallelizable/Configuration/UserFileConfigTests.cs b/Tests/UnitTestsParallelizable/Configuration/UserFileConfigTests.cs new file mode 100644 index 0000000000..ac332a0409 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Configuration/UserFileConfigTests.cs @@ -0,0 +1,113 @@ +// Claude - Fable 5 +using Microsoft.Extensions.Configuration; +using Terminal.Gui.Configuration; + +namespace ConfigurationTests; + +/// +/// Tests for user-file configuration sources: ./.tui/config.json resolves against the +/// current directory (not the app's install directory), malformed files are collected as errors +/// instead of crashing at assembly load, legacy-shaped files are skipped entirely, and +/// picks up file changes. +/// +[Collection ("StaticSettingsTests")] +public class UserFileConfigTests +{ + private static string CreateTuiDirWithConfig (string json) + { + string root = Directory.CreateTempSubdirectory ("tui-test-").FullName; + string tuiDir = Path.Combine (root, TuiConfigurationExtensions.TUI_CONFIG_FOLDER); + Directory.CreateDirectory (tuiDir); + File.WriteAllText (Path.Combine (tuiDir, TuiConfigurationExtensions.CONFIG_FILENAME), json); + + return root; + } + + [Fact] + public void Build_LoadsCurrentDirConfig_FromGivenCurrentDirectory () + { + string root = CreateTuiDirWithConfig ("""{ "TestMarker": { "Value": "42" } }"""); + + try + { + TuiConfigurationBuilder tuiBuilder = new (appName: null, currentDirectory: root); + + Assert.Equal ("42", tuiBuilder.Configuration ["TestMarker:Value"]); + } + finally + { + Directory.Delete (root, recursive: true); + } + } + + [Fact] + public void Reload_PicksUpChangedUserFile () + { + string root = CreateTuiDirWithConfig ("""{ "TestMarker": { "Value": "1" } }"""); + string configPath = Path.Combine (root, TuiConfigurationExtensions.TUI_CONFIG_FOLDER, TuiConfigurationExtensions.CONFIG_FILENAME); + + try + { + TuiConfigurationBuilder tuiBuilder = new (appName: null, currentDirectory: root); + Assert.Equal ("1", tuiBuilder.Configuration ["TestMarker:Value"]); + + File.WriteAllText (configPath, """{ "TestMarker": { "Value": "2" } }"""); + + // The configuration is cached until invalidated. + Assert.Equal ("1", tuiBuilder.Configuration ["TestMarker:Value"]); + + tuiBuilder.Reload (); + + Assert.Equal ("2", tuiBuilder.Configuration ["TestMarker:Value"]); + } + finally + { + Directory.Delete (root, recursive: true); + } + } + + [Fact] + public void Build_MalformedUserFile_DoesNotThrow_AndCollectsError () + { + string root = CreateTuiDirWithConfig ("{ this is not json"); + TuiJsonErrors.Print (); + + try + { + TuiConfigurationBuilder tuiBuilder = new (appName: null, currentDirectory: root); + IConfiguration config = tuiBuilder.Configuration; + + Assert.NotNull (config); + Assert.Contains (TuiJsonErrors.GetErrors (), e => e.Contains (TuiConfigurationExtensions.CONFIG_FILENAME)); + } + finally + { + Directory.Delete (root, recursive: true); + } + } + + [Fact] + public void Build_LegacyShapedUserFile_IsSkippedEntirely () + { + string root = CreateTuiDirWithConfig ( + """ + { + "TestMarker.Value": "42", + "TestSection": { "X": "1" } + } + """); + + try + { + TuiConfigurationBuilder tuiBuilder = new (appName: null, currentDirectory: root); + + // The whole file is skipped — neither the flat dotted key nor the nested section leaks in. + Assert.Null (tuiBuilder.Configuration ["TestMarker.Value"]); + Assert.Null (tuiBuilder.Configuration ["TestSection:X"]); + } + finally + { + Directory.Delete (root, recursive: true); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiComponentFactorySizeMonitorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiComponentFactorySizeMonitorTests.cs index e1f71d6fae..d23a887ccc 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiComponentFactorySizeMonitorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiComponentFactorySizeMonitorTests.cs @@ -1,4 +1,5 @@ using Moq; +using Terminal.Gui.Configuration; namespace DriverTests.Ansi; @@ -32,12 +33,11 @@ public void CreateSizeMonitor_InjectedMonitor_IsReturnedDirectly () } /// - /// When is active (the default), - /// should return an - /// for an argument. + /// When is explicitly selected, + /// the size monitor must queue an ANSI terminal-size query when initialized. /// [Fact] - public void CreateSizeMonitor_AnsiQuery_ReturnsAnsiSizeMonitor () + public void CreateSizeMonitor_AnsiQuery_QueuesAnsiSizeQuery () { SizeDetectionMode saved = Driver.SizeDetection; @@ -50,9 +50,90 @@ public void CreateSizeMonitor_AnsiQuery_ReturnsAnsiSizeMonitor () AnsiComponentFactory factory = new (); - ISizeMonitor result = factory.CreateSizeMonitor (output, Mock.Of ()); + ISizeMonitor monitor = factory.CreateSizeMonitor (output, Mock.Of ()); - Assert.IsType (result); + List queuedRequests = []; + Mock driver = new (); + driver.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (queuedRequests.Add); + + monitor.Initialize (driver.Object); + + Assert.Contains (queuedRequests, + request => request.Request == EscSeqUtils.CSI_ReportWindowSizeInChars.Request); + } + finally + { + Driver.SizeDetection = saved; + } + } + + // Codex - GPT-5 Codex + /// + /// The default ANSI size monitor must observe native size changes without queuing + /// terminal-size queries. + /// + [Fact] + public void CreateSizeMonitor_Default_DetectsNativeResize_WithoutAnsiSizeQuery () + { + SizeDetectionMode saved = Driver.SizeDetection; + + try + { + DriverSettings defaults = new (); + Driver.SizeDetection = defaults.SizeDetection; + + AnsiOutput output = new (); + Size reportedSize = new (80, 25); + + AnsiComponentFactory factory = new (null, output, null, () => reportedSize); + ISizeMonitor monitor = factory.CreateSizeMonitor (output, Mock.Of ()); + + List queuedRequests = []; + Mock driver = new (); + driver.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (queuedRequests.Add); + + List sizeChanges = []; + monitor.SizeChanged += (_, e) => sizeChanges.Add (e); + monitor.Initialize (driver.Object); + + Assert.Equal (reportedSize, output.GetSize ()); + Assert.False (monitor.Poll ()); + + reportedSize = new Size (120, 40); + + Assert.True (monitor.Poll ()); + Assert.Single (sizeChanges); + Assert.Equal (reportedSize, sizeChanges [0].Size); + Assert.DoesNotContain (queuedRequests, + request => request.Request == EscSeqUtils.CSI_ReportWindowSizeInChars.Request); + } + finally + { + Driver.SizeDetection = saved; + } + } + + // Codex - GPT-5 Codex + [Fact] + public void CreateOutput_Default_InitializesNativeSizeBeforeMainLoopSizing () + { + SizeDetectionMode saved = Driver.SizeDetection; + + try + { + DriverSettings defaults = new (); + Driver.SizeDetection = defaults.SizeDetection; + + AnsiOutput output = new (); + Size reportedSize = new (120, 40); + AnsiComponentFactory factory = new (null, output, null, () => reportedSize); + + IOutput createdOutput = factory.CreateOutput (); + + Assert.Same (output, createdOutput); + Assert.Equal (reportedSize, createdOutput.GetSize ()); } finally { @@ -62,12 +143,11 @@ public void CreateSizeMonitor_AnsiQuery_ReturnsAnsiSizeMonitor () /// /// When is active, - /// should return a - /// and configure - /// so that GetSize() queries the OS instead of returning the stale cache. + /// should return an ANSI size monitor + /// configured to query the OS without sending terminal-size requests. /// [Fact] - public void CreateSizeMonitor_Polling_ReturnsSizeMonitorImpl_AndSetsNativeSizeQuery () + public void CreateSizeMonitor_Polling_ReturnsAnsiSizeMonitor_AndSetsNativeSizeQuery () { SizeDetectionMode saved = Driver.SizeDetection; @@ -76,15 +156,13 @@ public void CreateSizeMonitor_Polling_ReturnsSizeMonitorImpl_AndSetsNativeSizeQu Driver.SizeDetection = SizeDetectionMode.Polling; AnsiOutput output = new (); - output.SetSize (80, 25); + Size reportedSize = new (80, 25); - AnsiComponentFactory factory = new (); + AnsiComponentFactory factory = new (null, output, null, () => reportedSize); ISizeMonitor result = factory.CreateSizeMonitor (output, Mock.Of ()); - Assert.IsType (result); - - // NativeSizeQuery must have been wired up so GetSize() can query the OS. + Assert.IsType (result); Assert.NotNull (output.NativeSizeQuery); } finally @@ -93,6 +171,35 @@ public void CreateSizeMonitor_Polling_ReturnsSizeMonitorImpl_AndSetsNativeSizeQu } } + // Codex - GPT-5 Codex + [Fact] + public void CreateSizeMonitor_PollingWithoutNativeSize_FallsBackToAnsiQuery () + { + SizeDetectionMode saved = Driver.SizeDetection; + + try + { + Driver.SizeDetection = SizeDetectionMode.Polling; + AnsiOutput output = new (); + AnsiComponentFactory factory = new (null, output, null, () => null); + ISizeMonitor monitor = factory.CreateSizeMonitor (output, Mock.Of ()); + + List queuedRequests = []; + Mock driver = new (); + driver.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (queuedRequests.Add); + + monitor.Initialize (driver.Object); + + Assert.Contains (queuedRequests, + request => request.Request == EscSeqUtils.CSI_ReportWindowSizeInChars.Request); + } + finally + { + Driver.SizeDetection = saved; + } + } + /// /// In mode the NativeSizeQuery delegate /// causes to return the OS-provided size rather than the diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs index 31a5bf788f..77030ffee7 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs @@ -289,6 +289,31 @@ public void InlineMode_InitialCursorRow_ParsedFromCprResponse () // Copilot Assert.Equal (14, monitor.InitialCursorRow); // 0-indexed } + // Codex - GPT-5 Codex + [Fact] + public void InlineMode_NativeSizeQuery_StillDiscoversInitialCursorRow () + { + AnsiOutput output = new (AppModel.Inline); + output.NativeSizeQuery = () => new Size (80, 50); + + List captured = []; + Mock driverMock = new (); + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (captured.Add); + + AnsiSizeMonitor monitor = new (output, queryTerminalSize: false); + monitor.Initialize (driverMock.Object); + + Assert.DoesNotContain (captured, request => request.Terminator == "t"); + AnsiEscapeSequenceRequest cprRequest = Assert.Single (captured, request => request.Terminator == "R"); + Assert.False (monitor.InitialSizeReceived); + + cprRequest.ResponseReceived! ("[15;1R"); + + Assert.True (monitor.InitialSizeReceived); + Assert.Equal (14, monitor.InitialCursorRow); + } + /// /// In FullScreen mode, sends both size and cursor /// position queries during initialization (cursor position is needed for the startup gate). diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/UnixRawModeHelperTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/UnixRawModeHelperTests.cs index 5194bc10c1..6d138d43f3 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/UnixRawModeHelperTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/UnixRawModeHelperTests.cs @@ -63,9 +63,7 @@ public void Dispose_WithoutTryEnable_IsNoOp_AndDoesNotThrow () [Fact] public void TryEnable_OnNonUnix_ReturnsFalse_AndLeavesNoSavedTermios () { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux) - || RuntimeInformation.IsOSPlatform (OSPlatform.OSX) - || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)) + if (!OperatingSystem.IsWindows ()) { // Behaviour on Unix depends on whether stdin is a tty; covered by // integration tests, not this unit test. diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs index f59752ba16..a98468faf3 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs @@ -15,15 +15,15 @@ public class KittyKeyboardPipelineTests /// private static (List KeyDown, List KeyUp) InjectRawSequence (params string [] sequences) { - return InjectRawSequenceCore (false, sequences); + return InjectRawSequenceCore (TimeSpan.Zero, sequences); } - private static (List KeyDown, List KeyUp) InjectRawSequenceWithKittyEnabled (params string [] sequences) + private static (List KeyDown, List KeyUp) InjectRawSequenceWithDelay (TimeSpan delay, params string [] sequences) { - return InjectRawSequenceCore (true, sequences); + return InjectRawSequenceCore (delay, sequences); } - private static (List KeyDown, List KeyUp) InjectRawSequenceCore (bool kittyKeyboardEnabled, params string [] sequences) + private static (List KeyDown, List KeyUp) InjectRawSequenceCore (TimeSpan delay, params string [] sequences) { VirtualTimeProvider timeProvider = new (); timeProvider.SetTime (new DateTime (2025, 1, 1, 12, 0, 0)); @@ -37,17 +37,23 @@ private static (List KeyDown, List KeyUp) InjectRawSequenceCore (bool app.Keyboard.KeyUp += (_, key) => keyUpEvents.Add (key); IInputProcessor processor = app.Driver?.GetInputProcessor ()!; - ((AnsiInputProcessor)processor).SetKittyKeyboardEnabled (kittyKeyboardEnabled); ConcurrentQueue queue = ((AnsiInputProcessor)processor).InputQueue; - foreach (string seq in sequences) + for (var sequenceIndex = 0; sequenceIndex < sequences.Length; sequenceIndex++) { + string seq = sequences [sequenceIndex]; + foreach (char ch in seq) { queue.Enqueue (ch); } processor.ProcessQueue (); + + if (sequenceIndex < sequences.Length - 1) + { + timeProvider.Advance (delay); + } } return (keyDownEvents, keyUpEvents); @@ -215,9 +221,9 @@ public void Pipeline_F12_CtrlShift_Press () #endregion - #region Mixed Kitty + Legacy Duplicate Input + #region Kitty + Legacy Input - // Copilot + // Codex - GPT-5 Codex [Fact] public void Pipeline_MixedKittyAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown () { @@ -230,27 +236,93 @@ public void Pipeline_MixedKittyAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown ( Assert.Empty (up); } - // Copilot - [Theory] - [InlineData ("«")] - [InlineData ("»")] - [InlineData ("ç")] - [InlineData ("Ç")] - [InlineData ("º")] - [InlineData ("ª")] - public void Pipeline_LegacyPrintable_PortugueseKeys_WhenKittyEnabled_DoesNotRaiseDuplicateKeyDown (string printable) + // Codex - GPT-5 Codex + [Fact] + public void Pipeline_MixedKittyAndLegacyPrintable_AcrossDelay_DoesNotRaiseDuplicateKeyDown () { - // Issue #4918 (PT keyboard): some glyphs may still arrive as duplicated legacy printable input - // even when kitty is enabled. A single keypress should still produce one KeyDown. - string duplicatedInput = printable + printable; - (List down, List up) = InjectRawSequenceWithKittyEnabled (duplicatedInput); + (List down, List up) = InjectRawSequenceWithDelay (TimeSpan.FromMilliseconds (51), "\x1b[97u", "a"); Assert.Single (down); - Assert.Equal (printable, down [0].GetPrintableText ()); + Assert.Equal (Key.A, down [0]); Assert.Empty (up); } - // Copilot + // Codex - GPT-5.6 + [Fact] + public void Pipeline_KittyPortugueseKey_FollowedByTwoLegacyPrintables_SuppressesOnlyFirstFallback () + { + (List down, List up) = InjectRawSequence ("\x1b[231::59;;231u", "ç", "ç"); + + Assert.Equal (2, down.Count); + Assert.All (down, key => Assert.Equal ("ç", key.GetPrintableText ())); + Assert.Empty (up); + } + + // Codex - GPT-5.6 + [Fact] + public void Pipeline_KittyPrintable_FollowedByNonmatchingFallback_ClearsSuppression () + { + (List down, List up) = InjectRawSequence ("\x1b[97u", "b", "a"); + + Assert.Equal ([Key.A, Key.B, Key.A], down); + Assert.Empty (up); + } + + // Codex - GPT-5.6 + [Fact] + public void Pipeline_KittyPrintable_InterveningParsedKey_PreservesLaterMatchingFallbackKey () + { + (List down, List up) = InjectRawSequence ("\x1b[97u\x1b[Aa"); + + Assert.Equal ([Key.A, Key.CursorUp, Key.A], down); + Assert.Empty (up); + } + + // Codex - GPT-5.6 + [Theory] + [InlineData ("\x1b[<0;10;20M")] + [InlineData ("\x1b[?1;2c")] + [InlineData ("\x1b[200~x\x1b[201~")] + public void Pipeline_KittyPrintable_InterveningAnsiInput_PreservesLaterMatchingFallbackKey (string interveningInput) + { + (List down, List up) = InjectRawSequence ($"\x1b[97u{interveningInput}a"); + + Assert.Equal ([Key.A, Key.A], down); + Assert.Empty (up); + } + + // Codex - GPT-5.6 + [Fact] + public void Pipeline_LegacyShiftTabFollowedByTab_RaisesBothKeyDowns () + { + (List down, List up) = InjectRawSequence ("\x1b[Z\t"); + + Assert.Equal ([Key.Tab.WithShift, Key.Tab], down); + Assert.Empty (up); + } + + // Codex - GPT-5.6 + [Fact] + public void Pipeline_KittyTabAndLegacyTab_DoesNotRaiseDuplicateKeyDown () + { + (List down, List up) = InjectRawSequence ("\x1b[9u\t"); + + Assert.Equal ([Key.Tab], down); + Assert.Empty (up); + } + + // Codex - GPT-5 Codex + [Fact] + public void Pipeline_RepeatedLegacyPrintableInput_DoesNotSelfArmSuppression () + { + (List down, List up) = InjectRawSequence ("jjjj"); + + Assert.Equal (4, down.Count); + Assert.Equal ("jjjj", string.Concat (down.Select (key => key.GetPrintableText ()))); + Assert.Empty (up); + } + + // Codex - GPT-5 Codex [Fact] public void Pipeline_MixedKittyAssociatedTextAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown () { diff --git a/Tests/UnitTestsParallelizable/Drivers/Output/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/Output/OutputBaseTests.cs index f8f8f95b0d..a5e19a8849 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Output/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Output/OutputBaseTests.cs @@ -104,6 +104,136 @@ public void Write_WritesDirtyCellsAndClearsDirtyFlags () Assert.False (buffer.Contents! [0, 1].IsDirty); } + // CoPilot - GPT-5 + [Fact] + public void Write_ClearsDirtyLines_AfterFlushing () + { + AnsiOutput output = new (); + OutputBufferImpl buffer = new () { InlineMode = true }; + buffer.SetSize (3, 2); + buffer.AddStr ("X"); + + Assert.True (buffer.DirtyLines [0]); + Assert.False (buffer.DirtyLines [1]); + + output.Write (buffer); + + Assert.All (buffer.DirtyLines, Assert.False); + } + + // Codex - GPT-5 + [Fact] + public void FillRect_InlineMode_WritesDirtyRow () + { + AnsiOutput output = new (); + OutputBufferImpl buffer = new () { InlineMode = true }; + buffer.SetSize (3, 2); + + buffer.FillRect (new Rectangle (1, 1, 1, 1), 'X'); + + Assert.True (buffer.DirtyLines [1]); + + output.Write (buffer); + + Assert.Contains ("X", output.GetLastOutput ()); + Assert.False (buffer.DirtyLines [1]); + } + + // Codex - GPT-5 + [Fact] + public void Write_UnchangedBuffer_DoesNotMoveCursorOrWrite () + { + CountingOutput output = new (); + OutputBufferImpl buffer = new (); + buffer.SetSize (3, 2); + output.Write (buffer); + output.ResetCounters (); + + output.Write (buffer); + + Assert.Equal (0, output.CursorMoves); + Assert.Equal (0, output.Writes); + } + + // CoPilot - GPT-5.6 + [Fact] + public void Write_SparseBorderFrame_MovesCursorOncePerDirtyRun () + { + const int COLS = 270; + const int ROWS = 72; + CountingOutput output = new (); + OutputBufferImpl buffer = new (); + buffer.SetSize (COLS, ROWS); + output.Write (buffer); + output.ResetCounters (); + + for (var row = 0; row < ROWS; row++) + { + buffer.Contents! [row, 0].IsDirty = true; + buffer.Contents [row, COLS - 1].IsDirty = true; + buffer.DirtyLines [row] = true; + } + + output.Write (buffer); + + Assert.Equal (ROWS * 2, output.CursorMoves); + Assert.Equal (ROWS * 2, output.Writes); + + for (var row = 0; row < ROWS; row++) + { + Assert.Equal (new Point (0, row), output.CursorPositions [row * 2]); + Assert.Equal (new Point (COLS - 1, row), output.CursorPositions [row * 2 + 1]); + Assert.False (buffer.Contents! [row, 0].IsDirty); + Assert.False (buffer.Contents [row, COLS - 1].IsDirty); + Assert.False (buffer.DirtyLines [row]); + } + } + + // CoPilot - GPT-5.6 + [Fact] + public void Write_MultipleDirtyRuns_SkipsLeadingInteriorAndTrailingCleanCells () + { + CountingOutput output = new (); + OutputBufferImpl buffer = new (); + buffer.SetSize (10, 1); + output.Write (buffer); + output.ResetCounters (); + + buffer.Contents! [0, 2].IsDirty = true; + buffer.Contents [0, 3].IsDirty = true; + buffer.Contents [0, 8].IsDirty = true; + buffer.DirtyLines [0] = true; + + output.Write (buffer); + + Point [] expectedCursorPositions = [new (0, 0), new (2, 0), new (8, 0)]; + Assert.Equal (expectedCursorPositions, output.CursorPositions); + Assert.Equal (2, output.Writes); + Assert.False (buffer.Contents [0, 2].IsDirty); + Assert.False (buffer.Contents [0, 3].IsDirty); + Assert.False (buffer.Contents [0, 8].IsDirty); + Assert.False (buffer.DirtyLines [0]); + } + + // CoPilot - GPT-5 + [Fact] + public void FillRect_MarksDirtyLine_AfterPreviousFlush () + { + AnsiOutput output = new (); + IOutputBuffer buffer = output.GetLastBuffer ()!; + buffer.SetSize (3, 2); + output.Write (buffer); + + buffer.FillRect (new Rectangle (1, 1, 1, 1), 'X'); + + Assert.False (buffer.DirtyLines [0]); + Assert.True (buffer.DirtyLines [1]); + + output.Write (buffer); + + Assert.False (buffer.DirtyLines [1]); + } + [Theory] [InlineData (true)] [InlineData (false)] @@ -1893,6 +2023,32 @@ public void Write_KittyRasterImage_FragmentedClip_UsesDistinctImageIdsPerFragmen return image; } + private sealed class CountingOutput : OutputBase + { + public List CursorPositions { get; } = []; + + public int CursorMoves { get; private set; } + + public int Writes { get; private set; } + + public void ResetCounters () + { + CursorPositions.Clear (); + CursorMoves = 0; + Writes = 0; + } + + protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) + { + CursorPositions.Add (new Point (screenPositionX, screenPositionY)); + CursorMoves++; + + return true; + } + + protected override void Write (StringBuilder output) => Writes++; + } + // Copilot - Claude Sonnet 4.6 // When Kitty is supported, UseKittyGraphics must be enabled — even if Sixel is also available, // because Kitty is the preferred protocol. diff --git a/Tests/UnitTestsParallelizable/Drivers/PlatformDetectionTests.cs b/Tests/UnitTestsParallelizable/Drivers/PlatformDetectionTests.cs index 34f64ba143..257a362a77 100644 --- a/Tests/UnitTestsParallelizable/Drivers/PlatformDetectionTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/PlatformDetectionTests.cs @@ -13,35 +13,19 @@ public void DetectPlatform_BasedOnOSDescription () if (OperatingSystem.IsWindows ()) { Assert.False (isWSLExpected); - Assert.True (PlatformDetection.IsWindows ()); - Assert.False (PlatformDetection.IsUnixLike ()); - Assert.False (PlatformDetection.IsLinux ()); - Assert.False (PlatformDetection.IsMac ()); } else if (OperatingSystem.IsLinux ()) { Assert.Equal (isWSLExpected, PlatformDetection.IsWSL ()); - Assert.False (PlatformDetection.IsWindows ()); - Assert.True (PlatformDetection.IsUnixLike ()); - Assert.True (PlatformDetection.IsLinux ()); - Assert.False (PlatformDetection.IsMac ()); } else if (OperatingSystem.IsMacOS ()) { Assert.False (isWSLExpected); - Assert.False (PlatformDetection.IsWindows ()); - Assert.True (PlatformDetection.IsUnixLike ()); - Assert.False (PlatformDetection.IsLinux ()); - Assert.True (PlatformDetection.IsMac ()); } else { // Fallback for other Unix-like or unknown systems Assert.False (isWSLExpected); - Assert.False (PlatformDetection.IsWindows ()); - Assert.True (PlatformDetection.IsUnixLike ()); - Assert.False (PlatformDetection.IsLinux ()); - Assert.True (PlatformDetection.IsMac ()); output.WriteLine ($"Unknown OS Description: {RuntimeInformation.OSDescription}"); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/UnixIOHelperTests.cs b/Tests/UnitTestsParallelizable/Drivers/UnixIOHelperTests.cs index 120f0ab0d2..6aeda50505 100644 --- a/Tests/UnitTestsParallelizable/Drivers/UnixIOHelperTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/UnixIOHelperTests.cs @@ -60,7 +60,7 @@ public void TryWriteAll_RetriesUntilAllBytesWritten () // Copilot public void IsInputAvailable_ReturnsTrue_WhenPollReportsReadableData () { - if (!OperatingSystem.IsLinux () && !OperatingSystem.IsMacOS () && !OperatingSystem.IsFreeBSD ()) + if (OperatingSystem.IsWindows ()) { return; } @@ -95,7 +95,7 @@ public void IsInputAvailable_ReturnsTrue_WhenPollReportsReadableData () // Copilot public void IsInputAvailable_ReturnsFalse_WhenPollReportsNonReadableEvent () { - if (!OperatingSystem.IsLinux () && !OperatingSystem.IsMacOS () && !OperatingSystem.IsFreeBSD ()) + if (OperatingSystem.IsWindows ()) { return; } diff --git a/Tests/UnitTestsParallelizable/Drivers/WindowsDriver/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/WindowsDriver/WindowsKeyConverterTests.cs index 4240fd6403..80b961dc8e 100644 --- a/Tests/UnitTestsParallelizable/Drivers/WindowsDriver/WindowsKeyConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/WindowsDriver/WindowsKeyConverterTests.cs @@ -25,7 +25,7 @@ public void ToKey_LetterKeys_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -53,7 +53,7 @@ public void ToKey_NumberKeys_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -87,7 +87,7 @@ public void ToKey_WithModifiers_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -123,7 +123,7 @@ KeyCode expectedKeyCode [InlineData (ConsoleKey.RightArrow, KeyCode.CursorRight)] public void ToKey_SpecialKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -161,7 +161,7 @@ public void ToKey_SpecialKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, Key [InlineData (ConsoleKey.F12, KeyCode.F12)] public void ToKey_FunctionKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, KeyCode expectedKeyCode) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -189,7 +189,7 @@ public void ToKey_FunctionKeys_ReturnsExpectedKeyCode (ConsoleKey consoleKey, Ke [InlineData ('Ω')] // Greek character public void ToKey_VKPacket_Unicode_ReturnsExpectedCharacter (char unicodeChar) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -207,7 +207,7 @@ public void ToKey_VKPacket_Unicode_ReturnsExpectedCharacter (char unicodeChar) [Fact] public void ToKey_VKPacket_ZeroChar_ReturnsNull () { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -225,7 +225,7 @@ public void ToKey_VKPacket_ZeroChar_ReturnsNull () [Fact] public void ToKey_VKPacket_SurrogatePair_DocumentsCurrentLimitation () { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -289,7 +289,7 @@ public void ToKey_OEMKeys_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -319,7 +319,7 @@ public void ToKey_NumPadKeys_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -346,7 +346,7 @@ public void ToKey_NumPadOperators_ReturnsExpectedKeyCode ( KeyCode expectedKeyCode ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -368,7 +368,7 @@ KeyCode expectedKeyCode [Fact] public void ToKey_NullKey_ReturnsEmpty () { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -398,7 +398,7 @@ public void ToKeyInfo_LetterKeys_ReturnsExpectedInputRecord ( char expectedChar ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -427,7 +427,7 @@ public void ToKeyInfo_NumberKeys_ReturnsExpectedInputRecord ( char expectedChar ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -459,7 +459,7 @@ public void ToKeyInfo_SpecialKeys_ReturnsExpectedInputRecord ( char expectedChar ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -488,7 +488,7 @@ char expectedChar [InlineData (KeyCode.CursorRight, ConsoleKey.RightArrow)] public void ToKeyInfo_NavigationKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -510,7 +510,7 @@ public void ToKeyInfo_NavigationKeys_ReturnsExpectedInputRecord (KeyCode keyCode [InlineData (KeyCode.F12, ConsoleKey.F12)] public void ToKeyInfo_FunctionKeys_ReturnsExpectedInputRecord (KeyCode keyCode, ConsoleKey expectedConsoleKey) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -546,7 +546,7 @@ public void ToKeyInfo_WithModifiers_ReturnsExpectedControlKeyState ( WindowsConsole.ControlKeyState expectedState ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -576,7 +576,7 @@ WindowsConsole.ControlKeyState expectedState [InlineData (KeyCode.Home, 71)] public void ToKeyInfo_ScanCodes_ReturnsExpectedScanCode (KeyCode keyCode, ushort expectedScanCode) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -607,7 +607,7 @@ public void ToKeyInfo_ScanCodes_ReturnsExpectedScanCode (KeyCode keyCode, ushort [InlineData (KeyCode.Space)] public void RoundTrip_ToKeyInfo_ToKey_PreservesKeyCode (KeyCode originalKeyCode) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -636,7 +636,7 @@ public void RoundTrip_ToKey_ToKeyInfo_PreservesData ( bool ctrl ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } @@ -686,7 +686,7 @@ public void ToKey_WithCapsLock_ReturnsExpectedKeyCode ( bool capsLock ) { - if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + if (!OperatingSystem.IsWindows ()) { return; } diff --git a/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs b/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs index edd5078303..128f563b68 100644 --- a/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs +++ b/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs @@ -39,7 +39,7 @@ public void TestBasicIcons () var p = new FileSystemIconProvider (); IFileSystem fs = GetMockFileSystem (); - Assert.Equal (IsWindows () ? new Rune ('\\') : new Rune ('/'), p.GetIcon (fs.DirectoryInfo.New (@"c:\"))); + Assert.Equal (OperatingSystem.IsWindows () ? new Rune ('\\') : new Rune ('/'), p.GetIcon (fs.DirectoryInfo.New (@"c:\"))); Assert.Equal ( new Rune (' '), @@ -49,7 +49,7 @@ public void TestBasicIcons () ); } - private string GetFileSystemRoot () { return IsWindows () ? @"c:\" : "/"; } + private string GetFileSystemRoot () { return OperatingSystem.IsWindows () ? @"c:\" : "/"; } private IFileSystem GetMockFileSystem () { @@ -67,6 +67,4 @@ private IFileSystem GetMockFileSystem () return fileSystem; } - - private bool IsWindows () { return RuntimeInformation.IsOSPlatform (OSPlatform.Windows); } } diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/BindTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/BindTests.cs index 1aaf535a8b..9646c8318f 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/BindTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/BindTests.cs @@ -98,12 +98,4 @@ public void Bind_Platform_AllNulls_ReturnsEmpty () Assert.Null (result.Linux); Assert.Null (result.Macos); } - - [Fact] - public void GetCurrentPlatform_ReturnsValidPlatform () - { - TuiPlatform platform = PlatformDetection.GetCurrentPlatform (); - - Assert.True (Enum.IsDefined (platform)); - } } diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/PlatformKeyBindingTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/PlatformKeyBindingTests.cs index 5c9b2e61b5..62ed11d5cb 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/PlatformKeyBindingTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/PlatformKeyBindingTests.cs @@ -75,16 +75,9 @@ public void GetCurrentPlatformKeys_AllOnly_ReturnsAllKeys () [Fact] public void GetCurrentPlatformKeys_PlatformOnly_ReturnsCurrentPlatformKeys () { - // Set the current platform's property (test runs on Windows for this CI) - TuiPlatform current = PlatformDetection.GetCurrentPlatform (); - - PlatformKeyBinding pkb = current switch - { - TuiPlatform.Windows => new () { Windows = [Key.F1] }, - TuiPlatform.Linux => new () { Linux = [Key.F1] }, - TuiPlatform.Macos => new () { Macos = [Key.F1] }, - _ => new () { Linux = [Key.F1] } - }; + PlatformKeyBinding pkb = OperatingSystem.IsWindows () ? new () { Windows = [Key.F1] } : + OperatingSystem.IsMacOS () ? new () { Macos = [Key.F1] } : + new () { Linux = [Key.F1] }; List keys = pkb.GetCurrentPlatformKeys ().ToList (); @@ -95,15 +88,9 @@ public void GetCurrentPlatformKeys_PlatformOnly_ReturnsCurrentPlatformKeys () [Fact] public void GetCurrentPlatformKeys_AllPlusPlatform_Additive () { - TuiPlatform current = PlatformDetection.GetCurrentPlatform (); - - PlatformKeyBinding pkb = current switch - { - TuiPlatform.Windows => new () { All = [Key.Esc], Windows = [Key.Q.WithCtrl] }, - TuiPlatform.Linux => new () { All = [Key.Esc], Linux = [Key.Q.WithCtrl] }, - TuiPlatform.Macos => new () { All = [Key.Esc], Macos = [Key.Q.WithCtrl] }, - _ => new () { All = [Key.Esc], Linux = [Key.Q.WithCtrl] } - }; + PlatformKeyBinding pkb = OperatingSystem.IsWindows () ? new () { All = [Key.Esc], Windows = [Key.Q.WithCtrl] } : + OperatingSystem.IsMacOS () ? new () { All = [Key.Esc], Macos = [Key.Q.WithCtrl] } : + new () { All = [Key.Esc], Linux = [Key.Q.WithCtrl] }; List keys = pkb.GetCurrentPlatformKeys ().ToList (); @@ -115,16 +102,9 @@ public void GetCurrentPlatformKeys_AllPlusPlatform_Additive () [Fact] public void GetCurrentPlatformKeys_OtherPlatformOnly_ReturnsEmpty () { - TuiPlatform current = PlatformDetection.GetCurrentPlatform (); - - // Set a platform that's NOT the current one - PlatformKeyBinding pkb = current switch - { - TuiPlatform.Windows => new () { Linux = [Key.F1] }, - TuiPlatform.Linux => new () { Windows = [Key.F1] }, - TuiPlatform.Macos => new () { Windows = [Key.F1] }, - _ => new () { Windows = [Key.F1] } - }; + PlatformKeyBinding pkb = OperatingSystem.IsWindows () ? new () { Linux = [Key.F1] } : + OperatingSystem.IsMacOS () ? new () { Windows = [Key.F1] } : + new () { Windows = [Key.F1] }; List keys = pkb.GetCurrentPlatformKeys ().ToList (); diff --git a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj index 0b8f506f9a..096f6afe44 100644 --- a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj +++ b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj @@ -26,6 +26,7 @@ + diff --git a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs index cbefc74404..ffd16b24bc 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs @@ -249,7 +249,7 @@ public class OnNewKeyTestView : View public bool CancelVirtualMethods { set; private get; } public bool OnKeyDownCalled { get; set; } public bool OnProcessKeyDownCalled { get; set; } - public override string Text { get; set; } + public new string Text { get; set; } protected override bool OnKeyDown (Key keyEvent) { diff --git a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/ViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/ViewDefaultKeyBindingsTests.cs index f5da0b2c70..7473c67538 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/ViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/ViewDefaultKeyBindingsTests.cs @@ -85,9 +85,9 @@ public void View_DefaultKeyBindings_HasConfigurationPropertyAttribute () Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + System.Attribute? attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); - Assert.NotNull (attr); + Assert.Null (attr); } [Fact] @@ -103,8 +103,8 @@ public void View_ViewKeyBindings_HasConfigurationPropertyAttribute () Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + System.Attribute? attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); - Assert.NotNull (attr); + Assert.Null (attr); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Mouse/ViewDefaultMouseBindingsTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/ViewDefaultMouseBindingsTests.cs index a5e1ba584b..036c2f79fd 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Mouse/ViewDefaultMouseBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Mouse/ViewDefaultMouseBindingsTests.cs @@ -49,9 +49,9 @@ public void View_DefaultMouseBindings_HasConfigurationPropertyAttribute () Assert.NotNull (property); - ConfigurationPropertyAttribute? attr = property.GetCustomAttribute (); + System.Attribute? attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); - Assert.NotNull (attr); + Assert.Null (attr); } [Fact] @@ -64,8 +64,8 @@ public void View_ViewMouseBindings_HasConfigurationPropertyAttribute () Assert.NotNull (property); - ConfigurationPropertyAttribute? attr = property.GetCustomAttribute (); + System.Attribute? attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); - Assert.NotNull (attr); + Assert.Null (attr); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/TextCwpTests.cs b/Tests/UnitTestsParallelizable/ViewBase/TextCwpTests.cs new file mode 100644 index 0000000000..490420824b --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/TextCwpTests.cs @@ -0,0 +1,526 @@ +// Copilot + +using System.ComponentModel; + +namespace ViewBaseTests; + +/// +/// Tests for the CWP-compliant notifications: +/// and . +/// +public class TextCwpTests +{ + [Fact] + public void Text_SetSameValue_NoEventsRaised () + { + View view = new () { Text = "hello" }; + bool changingRaised = false; + bool changedRaised = false; + view.TextChanging += (_, _) => changingRaised = true; + view.TextChanged += (_, _) => changedRaised = true; + + view.Text = "hello"; + + Assert.False (changingRaised); + Assert.False (changedRaised); + } + + [Fact] + public void Text_SetDifferentValue_BothEventsRaised () + { + View view = new () { Text = "old" }; + bool changingRaised = false; + bool changedRaised = false; + view.TextChanging += (_, _) => changingRaised = true; + view.TextChanged += (_, _) => changedRaised = true; + + view.Text = "new"; + + Assert.True (changingRaised); + Assert.True (changedRaised); + } + + [Fact] + public void TextChanging_Cancel_PreventsTextChange () + { + View view = new () { Text = "original" }; + view.TextChanging += (_, e) => e.Cancel = true; + + view.Text = "modified"; + + Assert.Equal ("original", view.Text); + } + + [Fact] + public void TextChanging_Cancel_SuppressesTextChanged () + { + View view = new () { Text = "original" }; + bool changedRaised = false; + view.TextChanging += (_, e) => e.Cancel = true; + view.TextChanged += (_, _) => changedRaised = true; + + view.Text = "modified"; + + Assert.False (changedRaised); + } + + [Fact] + public void TextChanging_RaisedBeforeMutation () + { + View view = new () { Text = "before" }; + string? textDuringChanging = null; + view.TextChanging += (sender, _) => textDuringChanging = ((View)sender!).Text; + + view.Text = "after"; + + Assert.Equal ("before", textDuringChanging); + } + + [Fact] + public void TextChanged_RaisedAfterMutation () + { + View view = new () { Text = "before" }; + string? textDuringChanged = null; + view.TextChanged += (sender, _) => textDuringChanged = ((View)sender!).Text; + + view.Text = "after"; + + Assert.Equal ("after", textDuringChanged); + } + + [Fact] + public void OnTextChanging_Override_CanCancel () + { + CancellingView view = new (); + // Set initial text before enabling cancellation + view.AllowChange = true; + view.Text = "initial"; + view.AllowChange = false; + + view.Text = "blocked"; + + Assert.Equal ("initial", view.Text); + } + + [Fact] + public void OnTextChanged_Override_CalledAfterChange () + { + TrackingView view = new () { Text = "start" }; + + view.Text = "end"; + + Assert.True (view.OnTextChangedCalled); + Assert.Equal ("end", view.TextAtOnTextChanged); + } + + [Fact] + public void TextChanging_EventOrder_ChangingBeforeChanged () + { + View view = new () { Text = "a" }; + List order = []; + view.TextChanging += (_, _) => order.Add ("changing"); + view.TextChanged += (_, _) => order.Add ("changed"); + + view.Text = "b"; + + Assert.Equal (["changing", "changed"], order); + } + + // --- CR feedback tests: these must fail before fixes, pass after --- + + /// + /// CR Issue 1: should raise + /// (CWP pattern: virtual method raises the event). A subclass that calls base.OnTextChanging() + /// should get the event's cancellation result. + /// + [Fact] + public void OnTextChanging_BaseRaisesEvent_SubclassGetsCancel () // Copilot + { + DelegatingView view = new (); + view.TextChanging += (_, e) => e.Cancel = true; + + view.Text = "hello"; + + // base.OnTextChanging() should have raised TextChanging and returned true (cancelled) + Assert.True (view.BaseReturnedCancel); + Assert.Equal (string.Empty, view.Text); + } + + /// + /// CR Issue 2: Setting polymorphically on a + /// should sync the TextField's internal model so that textField.Text returns the new value. + /// + [Fact] + public void TextField_PolymorphicSet_SyncsInternalModel () // Copilot + { + TextField tf = new (); + View v = tf; + + v.Text = "hello"; + + Assert.Equal ("hello", tf.Text); + } + + /// + /// CR Issue 3: Setting polymorphically on a + /// should sync the TextView's internal model so that textView.Text returns the new value. + /// + [Fact] + public void TextView_PolymorphicSet_SyncsInternalModel () // Copilot + { + TextView tv = new (); + View v = tv; + + v.Text = "hello"; + + Assert.Equal ("hello", tv.Text); + } + + // Codex - GPT-5.6 + [Fact] + public void TextView_Text_Set_Survives_Throwing_TextChanged_Subscriber () + { + TextView textView = new (); + EventHandler thrower = (_, _) => throw new InvalidOperationException ("Subscriber failure."); + textView.TextChanged += thrower; + + Assert.Throws (() => textView.Text = "first"); + + textView.TextChanged -= thrower; + ((View)textView).Text = "second"; + + Assert.Equal ("second", textView.Text); + } + + // Codex - GPT-5.6 + [Fact] + public void TextView_Text_Set_Throwing_TextChanged_Subscriber_Clears_History_Baseline () + { + TextView textView = new (); + textView.TextChanged += (_, _) => throw new InvalidOperationException ("Subscriber failure."); + + Assert.Throws (() => textView.Text = "first"); + + Assert.Equal ("first", textView.Text); + Assert.False (textView.IsDirty); + } + + // Codex - GPT-5.6 + [Fact] + public void TextView_TextChanged_ReentrantPolymorphicSet_SyncsInternalModel () + { + TextView textView = new (); + bool reentered = false; + + textView.TextChanged += (_, _) => + { + if (reentered) + { + return; + } + + reentered = true; + ((View)textView).Text = "second"; + }; + + textView.Text = "first"; + + Assert.Equal ("second", ((View)textView).Text); + Assert.Equal ("second", textView.Text); + } + + /// + /// CR Issue 4: A newly constructed should have + /// set to "0%" so that renders the percentage + /// on first draw. + /// + [Fact] + public void ProgressBar_Constructor_TextShowsZeroPercent () // Copilot + { + ProgressBar pb = new (); + + Assert.Equal ("0%", pb.Text); + } + + /// A test subclass that cancels text changes via . + private class CancellingView : View + { + public bool AllowChange { get; set; } + + protected override bool OnTextChanging (string newText) => !AllowChange; + } + + /// A test subclass that tracks calls to . + private class TrackingView : View + { + public bool OnTextChangedCalled { get; private set; } + public string? TextAtOnTextChanged { get; private set; } + + protected override void OnTextChanged () + { + OnTextChangedCalled = true; + TextAtOnTextChanged = Text; + + base.OnTextChanged (); + } + } + + /// + /// A test subclass that delegates to base.OnTextChanging() and records + /// whether the base returned a cancel signal. + /// + private class DelegatingView : View + { + public bool BaseReturnedCancel { get; private set; } + + protected override bool OnTextChanging (string newText) + { + BaseReturnedCancel = base.OnTextChanging (newText); + + return BaseReturnedCancel; + } + } + + // --- CR feedback regression tests --- + + /// + /// Verifies that when TextField's TextChanging subscriber modifies text, and then a + /// subsequent base TextChanging event cancels, the stale modified text does not leak + /// into the next successful text change. + /// + [Fact] + public void TextField_PendingText_ClearedOnBaseCancel () + { + // Copilot + TextField tf = new () { Width = 20, Height = 1 }; + tf.Text = "initial"; + + // First: a subscriber modifies text via ResultEventArgs + tf.TextChanging += (_, args) => + { + if (args.Result == "modified") + { + args.Result = "subscriber_changed"; + } + }; + + tf.Text = "modified"; + Assert.Equal ("subscriber_changed", tf.Text); + + // Now subscribe to base View.TextChanging to cancel the NEXT change + var cancelOnce = true; + + ((View)tf).TextChanging += (_, e) => + { + if (cancelOnce) + { + e.Cancel = true; + cancelOnce = false; + } + }; + + // This should be cancelled by the base event + tf.Text = "blocked"; + Assert.Equal ("subscriber_changed", tf.Text); + + // Next change should succeed with fresh text, NOT stale _pendingText + tf.Text = "final"; + Assert.Equal ("final", tf.Text); + } + + /// + /// Verifies that TextValidateField does not raise View.TextChanged when + /// ValueChanging is cancelled (CWP semantics: cancel suppresses TextChanged). + /// + [Fact] + public void TextValidateField_ValueChangingCancel_SuppressesTextChanged () + { + // Copilot + TextValidateField field = new () { Width = 20, Height = 1 }; + + // Set a provider that accepts any text + field.Provider = new TextRegexProvider (".*"); + field.Text = "initial"; + + // Cancel ValueChanging + field.ValueChanging += (_, args) => args.Handled = true; + + bool textChangedRaised = false; + ((View)field).TextChanged += (_, _) => textChangedRaised = true; + + field.Text = "blocked"; + + Assert.False (textChangedRaised, "TextChanged should not fire when ValueChanging cancels"); + Assert.Equal ("initial", field.Text); + } + + /// + /// Verifies that DatePicker rejects invalid (unparseable) text: Text should not + /// persist an invalid string that cannot round-trip through DateTime. + /// + [Fact] + public void DatePicker_InvalidText_DoesNotPersist () + { + // Copilot + DatePicker dp = new () { Width = 20, Height = 1 }; + DateTime originalValue = dp.Value; + string originalText = dp.Text; + + // Set invalid date text + dp.Text = "not-a-date"; + + // Value should remain unchanged + Assert.Equal (originalValue, dp.Value); + + // Text should NOT hold the invalid string — it should revert or be rejected + Assert.NotEqual ("not-a-date", dp.Text); + } + + /// + /// Verifies that ColorPicker rejects invalid (unparseable) text: Text should not + /// persist an invalid string that cannot round-trip through Color. + /// + [Fact] + public void ColorPicker_InvalidText_DoesNotPersist () + { + // Copilot + ColorPicker cp = new () { Width = 20, Height = 3 }; + cp.SelectedColor = new Color (255, 0, 0); + string originalText = cp.Text; + + // Set invalid color text + cp.Text = "not-a-color"; + + // SelectedColor should remain unchanged + Assert.Equal (new Color (255, 0, 0), cp.SelectedColor); + + // Text should NOT hold the invalid string — it should revert or be rejected + Assert.NotEqual ("not-a-color", cp.Text); + } + + /// + /// Verifies that setting View.Text on a word-wrapped TextView via a View reference + /// does not corrupt or redundantly re-process the model. + /// + [Fact] + public void TextView_WordWrap_PolymorphicSet_DoesNotCorruptModel () + { + // Copilot + TextView tv = new () { Width = 10, Height = 5, WordWrap = true }; + tv.Text = "Hello World this wraps"; + + // Set via polymorphic View reference + View viewRef = tv; + viewRef.Text = "Short"; + + Assert.Equal ("Short", tv.Text); + Assert.Equal ("Short", viewRef.Text); + } + + /// + /// Verifies that setting a valid date string via Text updates DatePicker.Value accordingly. + /// Ensures Text↔Value consistency on the happy path. + /// + [Fact] + public void DatePicker_ValidText_UpdatesValue () + { + // Copilot + DatePicker dp = new () { Width = 20, Height = 1 }; + DateTime target = new (2025, 12, 25); + + // Use the same short-date format the picker uses internally + string formatted = target.ToShortDateString (); + dp.Text = formatted; + + Assert.Equal (target, dp.Value); + Assert.Equal (formatted, dp.Text); + } + + /// + /// Verifies that setting DatePicker.Value updates Text to the formatted representation. + /// Ensures Value→Text consistency. + /// + [Fact] + public void DatePicker_ValueSet_UpdatesText () + { + // Copilot + DatePicker dp = new () { Width = 20, Height = 1 }; + DateTime target = new (2024, 7, 4); + + dp.Value = target; + + Assert.Equal (target, dp.Value); + + // Text should be parseable back to the same date + Assert.True (DateTime.TryParse (dp.Text, out DateTime roundTrip)); + Assert.Equal (target, roundTrip); + } + + /// + /// Verifies that setting a valid color string via Text updates ColorPicker.SelectedColor. + /// Ensures Text↔Value consistency on the happy path. + /// + [Fact] + public void ColorPicker_ValidText_UpdatesSelectedColor () + { + // Copilot + ColorPicker cp = new () { Width = 20, Height = 3 }; + cp.SelectedColor = new Color (0, 0, 0); + + // StandardColorsNameResolver accepts StandardColor enum names + cp.Text = "Red"; + + // SelectedColor should have changed from black to red + Assert.NotEqual (new Color (0, 0, 0), cp.SelectedColor); + Assert.Equal ("Red", cp.Text); + } + + /// + /// Verifies that setting ColorPicker.SelectedColor updates Text to the color string. + /// Ensures Value→Text consistency. + /// + [Fact] + public void ColorPicker_SelectedColorSet_UpdatesText () + { + // Copilot + ColorPicker cp = new () { Width = 20, Height = 3 }; + + cp.SelectedColor = new Color (0, 0, 255); + + Assert.Equal (new Color (0, 0, 255).ToString (), cp.Text); + } + + /// + /// Verifies that TextValidateField accepts valid text and updates Value accordingly. + /// Ensures Text↔Value consistency on the happy path. + /// + [Fact] + public void TextValidateField_ValidText_UpdatesValue () + { + // Copilot + TextValidateField tvf = new () { Provider = new TextRegexProvider ("^[0-9]+$") }; + tvf.Text = "123"; + + tvf.Text = "456"; + + Assert.Equal ("456", tvf.Text); + } + + /// + /// Verifies that when TextValidateField.ValueChanging cancels, both Text and the + /// provider's internal value remain at the old value (no divergence). + /// + [Fact] + public void TextValidateField_ValueChangingCancel_TextAndProviderStayConsistent () + { + // Copilot + TextValidateField tvf = new () { Provider = new TextRegexProvider ("^[0-9]+$") }; + tvf.Text = "111"; + tvf.ValueChanging += (_, e) => e.Handled = true; + + tvf.Text = "222"; + + // Both must stay at original value + Assert.Equal ("111", tvf.Text); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs b/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs index d60517b8b1..c3392d898d 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs @@ -1279,6 +1279,13 @@ public void Button_Implements_IAcceptTarget () Assert.IsAssignableFrom (button); } + // Copilot + [Fact] + public void IAcceptTarget_Is_In_Input_Namespace () + { + Assert.Equal ("Terminal.Gui.Input", typeof (IAcceptTarget).Namespace); + } + // CoPilot - ChatGPT o1 [Fact] public void NonIAcceptTarget_Handled_Does_Not_Redirect () diff --git a/Tests/UnitTestsParallelizable/Views/CustomTextRendererTests.cs b/Tests/UnitTestsParallelizable/Views/CustomTextRendererTests.cs new file mode 100644 index 0000000000..b6f8052e54 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/CustomTextRendererTests.cs @@ -0,0 +1,77 @@ +using UnitTests; + +namespace ViewsTests; + +public class CustomTextRendererTests : TestDriverBase +{ + // CoPilot - GPT-5.6 + [Fact] + public void ColorPicker_SuppressesGenericTextRendering () + { + ColorPicker colorPicker = new () { Width = 32, Height = 5, Value = Color.BrightRed }; + + AssertGenericTextRenderingIsSuppressed (colorPicker); + } + + // CoPilot - GPT-5.6 + [Fact] + public void LinearRangeViewBase_SuppressesGenericTextRendering () + { + LinearSelector linearSelector = new () { Width = 20, Height = 3, Text = "One,Two,Three" }; + + AssertGenericTextRenderingIsSuppressed (linearSelector); + } + + // CoPilot - GPT-5.6 + [Fact] + public void ProgressBar_SuppressesGenericTextRendering () + { + ProgressBar progressBar = new () { Width = 10, Height = 3, Fraction = 0.5F }; + + AssertGenericTextRenderingIsSuppressed (progressBar); + } + + // CoPilot - GPT-5.6 + [Fact] + public void TextView_SuppressesGenericTextRendering () + { + TextView textView = new () { Width = 10, Height = 3, Text = "custom text" }; + + AssertGenericTextRenderingIsSuppressed (textView); + } + + // CoPilot - GPT-5.6 + [Fact] + public void Code_SuppressesGenericTextRendering () + { + Code code = new () { Width = 10, Height = 3, Language = "cs", Text = "int answer = 42;" }; + + AssertGenericTextRenderingIsSuppressed (code); + } + + private void AssertGenericTextRenderingIsSuppressed (View view) + { + using (view) + using (IDriver driver = CreateTestDriver (40, 8)) + { + driver.Clip = new (driver.Screen); + view.Driver = driver; + + bool drawingTextRaised = false; + bool drewTextRaised = false; + view.DrawingText += (_, _) => drawingTextRaised = true; + view.DrewText += (_, _) => drewTextRaised = true; + + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + Assert.NotEmpty (view.Text); + + view.Draw (); + + Assert.False (drawingTextRaised); + Assert.False (drewTextRaised); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Views/DateEditorDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/DateEditorDefaultKeyBindingsTests.cs index cd5e32a423..4a22efef78 100644 --- a/Tests/UnitTestsParallelizable/Views/DateEditorDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DateEditorDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void DateEditor_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttrib Assert.NotNull (property); - var attr = property!.GetCustomAttribute (); + var attr = property!.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/DropDownListDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/DropDownListDefaultKeyBindingsTests.cs index c9b55283b6..4f32dbb885 100644 --- a/Tests/UnitTestsParallelizable/Views/DropDownListDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DropDownListDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void DropDownList_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttr Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/DropDownListTests.cs b/Tests/UnitTestsParallelizable/Views/DropDownListTests.cs index 95152eb5d7..216d687c7c 100644 --- a/Tests/UnitTestsParallelizable/Views/DropDownListTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DropDownListTests.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.ObjectModel; +using System.Text; using UnitTests; namespace ViewsTests; @@ -1223,8 +1224,213 @@ public void DroppedList_UsesBackgroundDistinctFromSuperView () app.End (token!); } + // CoPilot - Grok 4.6 + /// + /// Regression for https://github.com/tui-cs/Terminal.Gui/issues/5635. + /// Keyboard Enter with Accepting Handled and a focus move must not leave the dropped + /// list's text in cells the popover previously covered. + /// + [Fact] + public void Enter_HandledAccepting_DoesNotLeaveStalePopoverCells () + { + using HandledAcceptRepro repro = HandledAcceptRepro.Create (); + + repro.DropDown.SetFocus (); + repro.App.InjectKey (Key.F4); + repro.App.LayoutAndDraw (); + + IPopoverView? popover = FindDropDownPopover (repro.App); + Assert.NotNull (popover); + Assert.True (popover.Visible); + + string openBuffer = ReadScreen (repro.App); + output.WriteLine ("Open:\n{0}", openBuffer); + Assert.Contains ("Equipment", openBuffer, StringComparison.Ordinal); + + repro.App.InjectKey (Key.CursorDown); + Assert.False (repro.CoveredLabel.NeedsDraw); + Assert.False (repro.Dialog.NeedsDraw); + + repro.App.InjectKey (Key.Enter); + + Assert.Equal ("Fuel/Energy", repro.DropDown.Text); + Assert.True (repro.NextView.HasFocus); + Assert.False (popover.Visible); + Assert.NotEqual (popover, repro.App.Popovers?.GetActivePopover ()); + Assert.True (repro.Dialog.NeedsDraw, "Hide must invalidate the dialog so covered cells are repainted."); + Assert.True (repro.CoveredLabel.NeedsDraw, "The label the popover covered must be invalidated."); + + repro.App.LayoutAndDraw (); + + string closedBuffer = ReadScreen (repro.App); + output.WriteLine ("Closed:\n{0}", closedBuffer); + Assert.Contains ("COVERED-AREA-3", closedBuffer, StringComparison.Ordinal); + Assert.DoesNotContain ("Equipment", closedBuffer, StringComparison.Ordinal); + } + + // CoPilot - Grok 4.6 + /// + /// Regression for https://github.com/tui-cs/Terminal.Gui/issues/5635. + /// Mouse accept with Accepting Handled must still repaint the covered region. + /// + [Fact] + public void MouseAccept_HandledAccepting_DoesNotLeaveStalePopoverCells () + { + using HandledAcceptRepro repro = HandledAcceptRepro.Create (); + + repro.DropDown.SetFocus (); + repro.App.InjectKey (Key.F4); + repro.App.LayoutAndDraw (); + + Popover? popover = FindDropDownPopover (repro.App) as Popover; + Assert.NotNull (popover); + Assert.True (popover.Visible); + Assert.NotNull (popover.ContentView); + + Rectangle listFrame = popover.ContentView.FrameToScreen (); + Point clickPos = new (listFrame.X + 1, listFrame.Y + 1); + + DateTime baseTime = new (2025, 1, 1, 12, 0, 0); + InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct }; + IInputInjector injector = repro.App.GetInputInjector (); + + injector.InjectMouse (new Mouse { ScreenPosition = clickPos, Flags = MouseFlags.LeftButtonPressed, Timestamp = baseTime }, options); + injector.InjectMouse (new Mouse { ScreenPosition = clickPos, Flags = MouseFlags.LeftButtonReleased, Timestamp = baseTime.AddMilliseconds (50) }, options); + + // Mouse selection uses Activate, not Accept. The Accepting handler may not move focus. + // The covered region must still be invalidated. + Assert.Equal ("Fuel/Energy", repro.DropDown.Text); + Assert.False (popover.Visible); + Assert.NotEqual (popover, repro.App.Popovers?.GetActivePopover ()); + Assert.True (repro.Dialog.NeedsDraw, "Hide must invalidate the dialog so covered cells are repainted."); + Assert.True (repro.CoveredLabel.NeedsDraw, "The label the popover covered must be invalidated."); + + repro.App.LayoutAndDraw (); + + string closedBuffer = ReadScreen (repro.App); + output.WriteLine ("Closed:\n{0}", closedBuffer); + Assert.Contains ("COVERED-AREA-3", closedBuffer, StringComparison.Ordinal); + Assert.DoesNotContain ("Equipment", closedBuffer, StringComparison.Ordinal); + } + // Helper to find the DropDownList popover (excludes the context menu popover) private static IPopoverView? FindDropDownPopover (IApplication app) => app.Popovers?.Popovers.OfType> ().FirstOrDefault (); + + private static string ReadScreen (IApplication app) + { + Cell [,]? contents = app.Driver!.GetOutputBuffer ().Contents; + Assert.NotNull (contents); + + int rows = contents.GetLength (0); + int cols = contents.GetLength (1); + StringBuilder builder = new (); + + for (var row = 0; row < rows; row++) + { + for (var col = 0; col < cols; col++) + { + builder.Append (contents [row, col].Grapheme); + } + + builder.AppendLine (); + } + + return builder.ToString (); + } + + /// + /// Dialog repro for handled Accepting: read-only DropDownList over a Label, ListView as the next focus target. + /// + private sealed class HandledAcceptRepro : IDisposable + { + private readonly SessionToken? _token; + + private HandledAcceptRepro (IApplication app, Dialog dialog, DropDownList dropDown, ListView nextView, Label coveredLabel, SessionToken? token) + { + App = app; + Dialog = dialog; + DropDown = dropDown; + NextView = nextView; + CoveredLabel = coveredLabel; + _token = token; + } + + public IApplication App { get; } + + public Dialog Dialog { get; } + + public DropDownList DropDown { get; } + + public ListView NextView { get; } + + public Label CoveredLabel { get; } + + public static HandledAcceptRepro Create () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (40, 15); + + Dialog dialog = new () + { + X = 0, + Y = 0, + Width = 40, + Height = 12, + Title = "Repro", + ShadowStyle = ShadowStyles.None + }; + + ObservableCollection items = ["Labor", "Fuel/Energy", "Equipment"]; + + DropDownList dropDown = new () + { + X = 0, + Y = 0, + Width = 20, + ReadOnly = true, + Source = new ListWrapper (items), + Text = "Labor" + }; + + Label covered1 = new () { X = 0, Y = 1, Width = 20, Text = "COVERED-AREA-1" }; + Label covered2 = new () { X = 0, Y = 2, Width = 20, Text = "COVERED-AREA-2" }; + Label covered3 = new () { X = 0, Y = 3, Width = 20, Text = "COVERED-AREA-3" }; + + ListView nextView = new () + { + X = 0, + Y = 5, + Width = 20, + Height = 3, + Source = new ListWrapper (new ObservableCollection (["NextA", "NextB", "NextC"])) + }; + + dropDown.Accepting += (_, e) => + { + e.Handled = true; + nextView.SetFocus (); + }; + + dialog.Add (dropDown, covered1, covered2, covered3, nextView); + + SessionToken? token = app.Begin (dialog); + app.LayoutAndDraw (); + + return new HandledAcceptRepro (app, dialog, dropDown, nextView, covered3, token); + } + + public void Dispose () + { + if (_token is { }) + { + App.End (_token); + } + + Dialog.Dispose (); + App.Dispose (); + } + } } public class DropDownListGenericTests diff --git a/Tests/UnitTestsParallelizable/Views/HexViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/HexViewDefaultKeyBindingsTests.cs index 604a30d04d..5c18c84fe4 100644 --- a/Tests/UnitTestsParallelizable/Views/HexViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/HexViewDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void HexView_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribute Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/LinearRange/LinearRangeDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/LinearRange/LinearRangeDefaultKeyBindingsTests.cs index fe195f98ed..dc3d20bb82 100644 --- a/Tests/UnitTestsParallelizable/Views/LinearRange/LinearRangeDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/LinearRange/LinearRangeDefaultKeyBindingsTests.cs @@ -49,7 +49,7 @@ public void LinearRange_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttri Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/ListViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewDefaultKeyBindingsTests.cs index 4f54542f93..8b08ba0d6e 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void ListView_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribut Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/MenuBarDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/MenuBarDefaultKeyBindingsTests.cs index 5a42382ec1..0d1295623f 100644 --- a/Tests/UnitTestsParallelizable/Views/MenuBarDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MenuBarDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void MenuBar_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribute Assert.NotNull (property); - var attr = property!.GetCustomAttribute (); + var attr = property!.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/NumericUpDownDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/NumericUpDownDefaultKeyBindingsTests.cs index bd130bacd3..925eafbac5 100644 --- a/Tests/UnitTestsParallelizable/Views/NumericUpDownDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/NumericUpDownDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void NumericUpDown_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAtt Assert.NotNull (property); - var attr = property!.GetCustomAttribute (); + var attr = property!.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs index 195a829661..e4d5f2ee49 100644 --- a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs +++ b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs @@ -646,4 +646,94 @@ public void CanFocus_Is_True_By_Default () numericUpDown.Dispose (); } + + // GitHub Copilot + // Tests that editable mode parses text, keeps invalid text visible, and resyncs on increment + [Fact] + public void CanEdit_Parses_Valid_Text_And_Resyncs_Invalid_Text_On_Increment () + { + NumericUpDown numericUpDown = new () + { + CanEdit = true, + Value = 12.5m, + Increment = 0.25m, + Format = "{0:0.00}" + }; + + TextField editor = Assert.IsType (numericUpDown.SubViews.OfType ().Single ()); + + Assert.Equal ("12.50", editor.Text); + Assert.Equal (12.5m, numericUpDown.Value); + + editor.Text = "3"; + + // The parsed value is applied, but the editor text is NOT reformatted while the user is typing. + Assert.Equal (3m, numericUpDown.Value); + Assert.Equal ("3", editor.Text); + + editor.Text = "3a.00"; + + Assert.Equal (3m, numericUpDown.Value); + Assert.Equal ("3a.00", editor.Text); + + // Increment resyncs the editor text to the formatted value. + numericUpDown.InvokeCommand (Command.Up); + + Assert.Equal (3.25m, numericUpDown.Value); + Assert.Equal ("3.25", editor.Text); + + numericUpDown.Dispose (); + } + + // Claude - Opus 4.8 + // Editable mode must keep the value editor between the down and up buttons so keyboard Tab order matches + // the visual left-to-right layout (down, value, up). + [Fact] + public void CanEdit_Editor_Preserves_SubView_Order_Between_Buttons () + { + NumericUpDown numericUpDown = new () { CanEdit = true }; + + TextField editor = numericUpDown.SubViews.OfType ().Single (); + + Assert.Equal (3, numericUpDown.SubViews.Count); + Assert.Same (editor, numericUpDown.SubViews.ElementAt (1)); + + numericUpDown.Dispose (); + } + + // Claude - Opus 4.8 + // Typing into the editor must update Value without reformatting the in-progress text, so partial input + // such as a trailing decimal point is preserved. Only Up/Down (or an external Value change) resyncs the text. + [Fact] + public void CanEdit_Typing_Does_Not_Reformat_Editor_Text_Until_Increment () + { + NumericUpDown numericUpDown = new () + { + CanEdit = true, + Value = 0m, + Increment = 0.25m, + Format = "{0:0.00}" + }; + + TextField editor = numericUpDown.SubViews.OfType ().Single (); + + // Simulate mid-typing a fractional value; the trailing "." must not be wiped out by reformatting. + editor.Text = "3."; + + Assert.Equal (3m, numericUpDown.Value); + Assert.Equal ("3.", editor.Text); + + editor.Text = "3.5"; + + Assert.Equal (3.5m, numericUpDown.Value); + Assert.Equal ("3.5", editor.Text); + + // An external Value change resyncs the editor to the formatted display. + numericUpDown.InvokeCommand (Command.Up); + + Assert.Equal (3.75m, numericUpDown.Value); + Assert.Equal ("3.75", editor.Text); + + numericUpDown.Dispose (); + } } diff --git a/Tests/UnitTestsParallelizable/Views/PopoverMenuDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/PopoverMenuDefaultKeyBindingsTests.cs index f5513a523a..4bab91beae 100644 --- a/Tests/UnitTestsParallelizable/Views/PopoverMenuDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/PopoverMenuDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void PopoverMenu_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttri Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutDrawingTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutDrawingTests.cs new file mode 100644 index 0000000000..94f6dcd586 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/ShortcutDrawingTests.cs @@ -0,0 +1,31 @@ +using UnitTests; + +namespace ViewsTests; + +public class ShortcutDrawingTests (ITestOutputHelper output) : TestDriverBase +{ + // CoPilot - GPT-5.6 + [Fact] + public void HelpText_Does_Not_Draw_Over_CommandView () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 1); + + Runnable runnable = new () { Width = 20, Height = 1 }; + app.Begin (runnable); + + Shortcut shortcut = new () + { + Title = "_New", + HelpText = "New file", + Width = 20, + Height = 1 + }; + + runnable.Add (shortcut); + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre (" New New file", output, app.Driver); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs b/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs index b368361511..c9fd372338 100644 --- a/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs @@ -289,12 +289,9 @@ public void Diagnostic_ActiveTabScroll_DrawEvents_OnEachTab () output.WriteLine ($"Sum of inactive DrawingContent: {inactiveContentDraws}"); output.WriteLine ($"Sum of inactive DrawingText: {inactiveTextDraws}"); - // Issue #5358 fix: inactive tabs' NeedsDraw is no longer set as a side-effect - // of the parent entering Draw(). DrawingText is the right metric here because - // it's gated on NeedsDraw inside DoDrawText — and Code does NOT override - // OnDrawingText (unlike OnClearingViewport / OnDrawingContent which Code - // intercepts, suppressing those events). DrawComplete fires unconditionally - // for clip-exclusion bookkeeping, so it is informational only. + // Code owns text rendering, so the generic DrawingText event must not fire + // for either the active or inactive tabs. + Assert.Equal (0, activeCounts.DrawingText); Assert.Equal (0, inactiveTextDraws); // Issue #5358 fix: the Tabs container itself no longer clears its viewport @@ -349,28 +346,19 @@ public void Diagnostic_TabbedFanOut_ComparedTo_SingleViewBaseline () ViewActivityCounters.Counts tabActiveCounts = tabCounters.Get (active); output.WriteLine (tabCounters.Report ("Tabbed scenario (5 scrolls of active tab):")); - int totalTabTextDraws = tabActiveCounts.DrawingText; int totalTabLayouts = tabActiveCounts.SubViewsLaidOut; for (var i = 1; i < TabCount; i++) { - totalTabTextDraws += tabCounters.Get (codes [i]).DrawingText; totalTabLayouts += tabCounters.Get (codes [i]).SubViewsLaidOut; } - double textDrawFanOut = singleCounts.DrawingText == 0 ? double.NaN : (double)totalTabTextDraws / singleCounts.DrawingText; double layoutFanOut = singleCounts.SubViewsLaidOut == 0 ? double.NaN : (double)totalTabLayouts / singleCounts.SubViewsLaidOut; - output.WriteLine ($"Tabbed total text draws / single text draws = {totalTabTextDraws} / {singleCounts.DrawingText} = {textDrawFanOut:F2}"); output.WriteLine ($"Tabbed total layouts / single layouts = {totalTabLayouts} / {singleCounts.SubViewsLaidOut} = {layoutFanOut:F2}"); - Assert.True (singleCounts.DrawingText > 0, "Single-Code baseline must record text-draw activity (NeedsDraw-gated)."); - Assert.True (tabActiveCounts.DrawingText > 0, "Active tab in tabbed scenario must record text-draw activity."); - - // Issue #5358 fix: tabbed text-draw fan-out is now at parity with the single-Code baseline. - // DrawingText is gated on NeedsDraw, so it only fires on tabs whose NeedsDraw was set — - // which is just the active tab after the fix. - Assert.Equal (1.0, textDrawFanOut); + Assert.Equal (0, singleCounts.DrawingText); + Assert.Equal (0, tabActiveCounts.DrawingText); Assert.Equal (1.0, layoutFanOut); @@ -417,8 +405,7 @@ public void Diagnostic_TransparentInactiveTab_StillObservable () { ViewActivityCounters.Counts c = counters.Get (codes [i]); - // Issue #5358 fix: transparent inactive peers also stay out of NeedsDraw-gated work. - // DrawingText is the right per-Code metric (Code intercepts ClearedViewport / DrawingContent). + // Code suppresses the generic text pass regardless of transparency. Assert.Equal (0, c.DrawingText); } diff --git a/Tests/UnitTestsParallelizable/Views/TableViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TableViewDefaultKeyBindingsTests.cs index c7e32f5aff..095a442ffb 100644 --- a/Tests/UnitTestsParallelizable/Views/TableViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TableViewDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void TableView_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribu Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/TableViewTests.cs b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs index ed66ea6589..785307e808 100644 --- a/Tests/UnitTestsParallelizable/Views/TableViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs @@ -229,6 +229,32 @@ public void TableView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () Assert.Equal (5, tableView.Value!.SelectedCell.Y); } + // Copilot + [Fact] + public void TableView_ContentSize_UsesWideCellContent_WhenContentExceedsViewport () + { + DataTable dt = new (); + dt.Columns.Add ("A"); + dt.Columns.Add ("Description"); + dt.Columns.Add ("C"); + dt.Rows.Add ("x", new string ('d', 120), "y"); + + TableView tableView = new () + { + Frame = new Rectangle (0, 0, 20, 5), + ViewportSettings = ViewportSettingsFlags.HasHorizontalScrollBar, + Table = new DataTableSource (dt) + }; + + tableView.BeginInit (); + tableView.EndInit (); + + Size contentSize = tableView.GetContentSize (); + + Assert.True (contentSize.Width > tableView.Viewport.Width); + Assert.True (tableView.HorizontalScrollBar.Visible); + } + [Fact] public void TableView_CollectionNavigatorMatcher_HotKey_Finds_Item () { @@ -863,9 +889,8 @@ public void Test_CalculateMaxCellWidth_UsesGraphemeWidth () } // Copilot - // Verifies fix for #5072: a column with very wide content must not consume all viewport space - // and push later columns off-screen. Each subsequent visible column should be reserved at least - // its header width. + // Verifies fix for #5072: a wide column should be allowed to grow based on its content so that + // the table can establish horizontal overflow and keep later columns reachable via scrolling. [Fact] public void Calculate_WideColumn_DoesNotStarveLaterColumns () { @@ -888,7 +913,7 @@ public void Calculate_WideColumn_DoesNotStarveLaterColumns () Assert.Equal (3, columns.Length); - // Description must be clamped so that Status and Owner fit + // The wide column should be allowed to extend beyond the viewport so the table can scroll horizontally. TableView.ColumnToRender description = columns [0]; TableView.ColumnToRender status = columns [1]; TableView.ColumnToRender owner = columns [2]; @@ -896,14 +921,8 @@ public void Calculate_WideColumn_DoesNotStarveLaterColumns () Assert.True (description.X >= 0); Assert.True (status.X > description.X); Assert.True (owner.X > status.X); - - // Every column's right edge must lie within the viewport - Assert.True (description.X + description.Width - 1 < tableView.Viewport.Width, - $"Description right edge {description.X + description.Width - 1} exceeds viewport {tableView.Viewport.Width}"); - Assert.True (status.X + status.Width - 1 < tableView.Viewport.Width, - $"Status right edge {status.X + status.Width - 1} exceeds viewport {tableView.Viewport.Width}"); - Assert.True (owner.X + owner.Width - 1 < tableView.Viewport.Width, - $"Owner right edge {owner.X + owner.Width - 1} exceeds viewport {tableView.Viewport.Width}"); + Assert.True (description.X + description.Width - 1 >= tableView.Viewport.Width, + $"Description right edge {description.X + description.Width - 1} should exceed viewport {tableView.Viewport.Width}"); // Status and Owner each must have at least header-width room (excluding separator) Assert.True (status.Width - 1 >= "Status".Length, $"Status got width {status.Width - 1}"); diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldDefaultKeyBindingsTests.cs index 4dfdf59452..c7441de911 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void TextField_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribu Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldDrawingTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldDrawingTests.cs new file mode 100644 index 0000000000..f912a7301a --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/TextFieldDrawingTests.cs @@ -0,0 +1,107 @@ +using System.Text; +using UnitTests; + +namespace ViewsTests; + +public class TextFieldDrawingTests : TestDriverBase +{ + private const string LongText = "C/W Liners, Fabrics, Pipe Connections, 150mm Dia Perf Drain Pipe, Drain Works"; + + // CoPilot - GPT-5.6 + [Fact] + public void DefaultHeight_LongText_HasOneRowOfContent () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 5); + + using Runnable runnable = new () { Width = 30, Height = 5 }; + TextField textField = new () + { + Width = 30, + Text = LongText, + BorderStyle = LineStyle.Single + }; + + runnable.Add (textField); + SessionToken token = app.Begin (runnable)!; + + try + { + app.LayoutAndDraw (); + + Assert.Equal (1, textField.Viewport.Height); + Assert.Equal (3, textField.Frame.Height); + } + finally + { + app.End (token); + } + } + + // CoPilot - GPT-5.6 + [Fact] + public void LongText_DoesNotOverdrawFollowingView () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 3); + + using Runnable runnable = new () { Width = 30, Height = 3 }; + TextField textField = new () { Width = 30, Text = LongText }; + Label nextRow = new () { Y = 1, Text = "NEXT ROW" }; + + runnable.Add (textField, nextRow); + SessionToken token = app.Begin (runnable)!; + + try + { + app.LayoutAndDraw (); + + Assert.Equal ("NEXT ROW", GetRow (app.Driver, 1, 30).TrimEnd ()); + } + finally + { + app.End (token); + } + } + + // CoPilot - GPT-5.6 + [Fact] + public void ExplicitHeight_DoesNotDrawWrappedBaseText () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (5, 3); + + using Runnable runnable = new () { Width = 5, Height = 3 }; + TextField textField = new () { Width = 5, Height = 3, Text = "abcdefghij" }; + + runnable.Add (textField); + SessionToken token = app.Begin (runnable)!; + + try + { + app.LayoutAndDraw (); + + Assert.Empty (GetRow (app.Driver, 1, 5).TrimEnd ()); + Assert.Empty (GetRow (app.Driver, 2, 5).TrimEnd ()); + } + finally + { + app.End (token); + } + } + + private static string GetRow (IDriver driver, int row, int width) + { + StringBuilder builder = new (); + + for (int column = 0; column < width; column++) + { + builder.Append (driver.Contents! [row, column]!.Grapheme); + } + + return builder.ToString (); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs index 70a0de42a8..2954373160 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -1740,7 +1740,7 @@ public void Paste_Replacing_Selection_With_Identical_Text_Clears_Selection_And_R [Fact] public void UnifiedKeyBindings_NonWindows_Undo_Redo () { - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { return; // non-Windows-only bindings are not added on Windows } diff --git a/Tests/UnitTestsParallelizable/Views/TextValidateFieldDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TextValidateFieldDefaultKeyBindingsTests.cs index eab955e0f9..04ac908989 100644 --- a/Tests/UnitTestsParallelizable/Views/TextValidateFieldDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextValidateFieldDefaultKeyBindingsTests.cs @@ -46,7 +46,7 @@ public void TextValidateField_DefaultKeyBindings_DoesNotHaveConfigurationPropert Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/TextValidateFieldDrawingTests.cs b/Tests/UnitTestsParallelizable/Views/TextValidateFieldDrawingTests.cs new file mode 100644 index 0000000000..bc44fa9c6a --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/TextValidateFieldDrawingTests.cs @@ -0,0 +1,78 @@ +using System.Text; +using UnitTests; + +namespace ViewsTests; + +public class TextValidateFieldDrawingTests : TestDriverBase +{ + private const string LongText = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + // CoPilot - GPT-5.6 + [Fact] + public void DefaultHeight_LongText_HasOneRowAndDoesNotOverdrawFollowingView () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 3); + + TextRegexProvider provider = new (".*") { Text = LongText, ValidateOnInput = false }; + TextValidateField field = new () { Width = 20, Provider = provider }; + Label nextRow = new () { Y = 1, Text = "NEXT ROW" }; + using Runnable runnable = new () { Width = 20, Height = 3 }; + + runnable.Add (field, nextRow); + SessionToken token = app.Begin (runnable)!; + + try + { + app.LayoutAndDraw (); + + Assert.Equal (1, field.Viewport.Height); + Assert.Equal ("NEXT ROW", GetRow (app.Driver, 1, 20).TrimEnd ()); + } + finally + { + app.End (token); + } + } + + // CoPilot - GPT-5.6 + [Fact] + public void ExplicitHeight_DoesNotDrawWrappedBaseText () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (5, 3); + + TextRegexProvider provider = new (".*") { Text = LongText, ValidateOnInput = false }; + TextValidateField field = new () { Width = 5, Height = 3, Provider = provider }; + using Runnable runnable = new () { Width = 5, Height = 3 }; + + runnable.Add (field); + SessionToken token = app.Begin (runnable)!; + + try + { + app.LayoutAndDraw (); + + Assert.Empty (GetRow (app.Driver, 1, 5).TrimEnd ()); + Assert.Empty (GetRow (app.Driver, 2, 5).TrimEnd ()); + } + finally + { + app.End (token); + } + } + + private static string GetRow (IDriver driver, int row, int width) + { + StringBuilder builder = new (); + + for (int column = 0; column < width; column++) + { + builder.Append (driver.Contents! [row, column]!.Grapheme); + } + + return builder.ToString (); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs index 674fc9ded9..72f4768217 100644 --- a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs @@ -787,4 +787,48 @@ public void CtrlKey_With_AssociatedText_Does_Not_Insert_Into_TextValidateField ( Assert.Equal (before, field.Text); } + + // CoPilot - GPT-5 + [Fact] + public void Draw_FocusedCursor_PreservesEditableColors_AndAddsUnderline () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (4, 1); + + Attribute editableAttribute = new (Color.BrightGreen, Color.Blue, TextStyle.Bold); + Scheme scheme = new () + { + Editable = editableAttribute, + Focus = new (Color.BrightRed, Color.Magenta) + }; + + TextRegexProvider provider = new ("^[0-9]+$") + { + ValidateOnInput = false + }; + TextValidateField field = new () + { + Provider = provider, + Width = 4, + Height = 1 + }; + field.Text = "12"; + field.SetScheme (scheme); + + using Runnable top = new (); + top.Add (field); + + app.Begin (top); + field.SetFocus (); + app.LayoutAndDraw (); + + Cell cursorCell = app.Driver.Contents! [0, 0]; + Attribute cursorAttribute = Assert.IsType (cursorCell.Attribute); + + Assert.Equal ("1", cursorCell.Grapheme); + Assert.Equal (editableAttribute.Foreground, cursorAttribute.Foreground); + Assert.Equal (editableAttribute.Background, cursorAttribute.Background); + Assert.Equal (editableAttribute.Style | TextStyle.Underline, cursorAttribute.Style); + } } diff --git a/Tests/UnitTestsParallelizable/Views/TextViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewDefaultKeyBindingsTests.cs index ea1106631b..971ba1ea4d 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void TextView_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribut Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/TextViewMouseSelectionBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewMouseSelectionBindingsTests.cs index da7a4eff5d..54a784c69a 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewMouseSelectionBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewMouseSelectionBindingsTests.cs @@ -9,11 +9,11 @@ public void TextView_SelectionStartMouseBindings_UsePlatformDefaults () { TextView textView = new (); - MouseFlags expectedStartSelection = PlatformDetection.GetCurrentPlatform () == TuiPlatform.Windows + MouseFlags expectedStartSelection = OperatingSystem.IsWindows () ? MouseFlags.LeftButtonPressed | MouseFlags.Alt : MouseFlags.LeftButtonPressed | MouseFlags.Shift; - MouseFlags expectedStartRectangleSelection = PlatformDetection.GetCurrentPlatform () == TuiPlatform.Windows + MouseFlags expectedStartRectangleSelection = OperatingSystem.IsWindows () ? MouseFlags.LeftButtonPressed | MouseFlags.Alt | MouseFlags.Ctrl : MouseFlags.LeftButtonPressed | MouseFlags.Shift | MouseFlags.Ctrl; diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs index b92d298cc0..d2eda3ad5e 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs @@ -3846,7 +3846,7 @@ public void UnifiedKeyBindings_CtrlV_Is_Paste_Not_PageDown () [Fact] public void UnifiedKeyBindings_NonWindows_Undo_Redo () { - if (PlatformDetection.IsWindows ()) + if (OperatingSystem.IsWindows ()) { return; // non-Windows-only bindings are not added on Windows } diff --git a/Tests/UnitTestsParallelizable/Views/TimeEditorDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TimeEditorDefaultKeyBindingsTests.cs index 2adbd1125a..090f404903 100644 --- a/Tests/UnitTestsParallelizable/Views/TimeEditorDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TimeEditorDefaultKeyBindingsTests.cs @@ -45,7 +45,7 @@ public void TimeEditor_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttrib Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tests/UnitTestsParallelizable/Views/TreeViewDefaultKeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Views/TreeViewDefaultKeyBindingsTests.cs index e7ed65db7b..5589311836 100644 --- a/Tests/UnitTestsParallelizable/Views/TreeViewDefaultKeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TreeViewDefaultKeyBindingsTests.cs @@ -46,7 +46,7 @@ public void TreeView_DefaultKeyBindings_DoesNotHaveConfigurationPropertyAttribut Assert.NotNull (property); - var attr = property.GetCustomAttribute (); + var attr = property.GetCustomAttributes ().FirstOrDefault (a => a.GetType ().Name == "ConfigurationPropertyAttribute"); Assert.Null (attr); } diff --git a/Tools/MigrateConfig/ConfigJsonMigrator.cs b/Tools/MigrateConfig/ConfigJsonMigrator.cs new file mode 100644 index 0000000000..778bafe534 --- /dev/null +++ b/Tools/MigrateConfig/ConfigJsonMigrator.cs @@ -0,0 +1,144 @@ +// Grok - grok-4.6 +using System.Text.Json.Nodes; + +namespace Terminal.Gui.Tools.MigrateConfig; + +/// +/// Transforms a pre-MEC Terminal.Gui config.json (flat-key, array-themes shape) +/// into the nested MEC-native shape consumed by TuiConfigurationBuilder. +/// +public static class ConfigJsonMigrator +{ + /// + /// Migrates a top-level configuration object. Nested objects are walked recursively. + /// + public static JsonObject MigrateObject (JsonObject src) + { + JsonObject result = []; + + foreach (KeyValuePair pair in src) + { + JsonNode? value = pair.Value is null ? null : Clone (pair.Value); + JsonNode? migratedValue = MigrateValue (pair.Key, value); + + MergeDottedKey (result, pair.Key, migratedValue); + } + + return result; + } + + internal static JsonNode? MigrateValue (string keyName, JsonNode? value) + { + if (value is JsonObject obj) + { + return MigrateObject (obj); + } + + if (value is JsonArray arr && IsArrayDictKey (keyName) && (arr.Count == 0 || IsArrayOfSingleKeyObjects (arr))) + { + JsonObject dict = []; + + foreach (JsonNode? item in arr) + { + if (item is not JsonObject itemObj) + { + continue; + } + + foreach (KeyValuePair entry in itemObj) + { + JsonNode? entryValue = entry.Value is null ? null : MigrateValue (entry.Key, Clone (entry.Value)); + dict [entry.Key] = entryValue; + } + } + + return dict; + } + + return value; + } + + private static bool IsArrayDictKey (string keyName) => + keyName is "Themes" or "Schemes"; + + private static bool IsArrayOfSingleKeyObjects (JsonArray arr) + { + if (arr.Count == 0) + { + return false; + } + + foreach (JsonNode? item in arr) + { + if (item is not JsonObject obj || obj.Count != 1) + { + return false; + } + } + + return true; + } + + private static void MergeDottedKey (JsonObject target, string key, JsonNode? value) + { + if (!key.Contains ('.')) + { + SetOrMerge (target, key, value); + + return; + } + + string [] parts = key.Split ('.'); + JsonObject cursor = target; + + for (var i = 0; i < parts.Length - 1; i++) + { + string part = parts [i]; + + if (cursor [part] is not JsonObject next) + { + next = []; + cursor [part] = next; + } + + cursor = next; + } + + SetOrMerge (cursor, parts [^1], value); + } + + /// + /// Assigns to , deep-merging when both the existing + /// entry and the value are objects. Legacy CM accepted mixed dotted and nested keys for the same + /// section; overwriting would drop settings depending on key order. + /// + private static void SetOrMerge (JsonObject target, string key, JsonNode? value) + { + if (target [key] is JsonObject existing && value is JsonObject incoming) + { + DeepMerge (existing, incoming); + + return; + } + + target [key] = value; + } + + private static void DeepMerge (JsonObject target, JsonObject source) + { + foreach (KeyValuePair pair in source) + { + if (target [pair.Key] is JsonObject existing && pair.Value is JsonObject incoming) + { + DeepMerge (existing, incoming); + + continue; + } + + target [pair.Key] = pair.Value is null ? null : Clone (pair.Value); + } + } + + private static JsonNode Clone (JsonNode node) => + JsonNode.Parse (node.ToJsonString ())!; +} diff --git a/Tools/MigrateConfig/MigrateConfig.csproj b/Tools/MigrateConfig/MigrateConfig.csproj new file mode 100644 index 0000000000..7e312839d0 --- /dev/null +++ b/Tools/MigrateConfig/MigrateConfig.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + Terminal.Gui.Tools.MigrateConfig + migrate-tui-config + + false + + + diff --git a/Tools/MigrateConfig/Program.cs b/Tools/MigrateConfig/Program.cs new file mode 100644 index 0000000000..84606a94b5 --- /dev/null +++ b/Tools/MigrateConfig/Program.cs @@ -0,0 +1,111 @@ +// Migrates a pre-MEC Terminal.Gui config.json (flat-key, array-themes shape) +// to the nested MEC-native shape consumed by TuiConfigurationBuilder. +// +// Transforms applied (recursively): +// 1. Any object property name containing '.' is split into nested objects. +// "Button.DefaultShadow": "Opaque" -> "Button": { "DefaultShadow": "Opaque" } +// 2. "Themes" as an array of single-key objects becomes a dictionary. +// "Themes": [ { "Dark": { ... } } ] -> "Themes": { "Dark": { ... } } +// 3. "Schemes" inside a theme follows the same array -> dictionary collapse. +// +// Usage: +// migrate-tui-config [output.json] +// +// If output.json is omitted, the migrated JSON is written to stdout. +// Exit codes: 0 success, 1 usage error, 2 I/O or JSON parse error. + +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Terminal.Gui.Tools.MigrateConfig; + +internal static class Program +{ + private static int Main (string [] args) + { + if (args.Length is < 1 or > 2) + { + Console.Error.WriteLine ("Usage: migrate-tui-config [output.json]"); + Console.Error.WriteLine ("If output.json is omitted, the result is written to stdout."); + + return 1; + } + + string inputPath = args [0]; + string? outputPath = args.Length == 2 ? args [1] : null; + + string text; + + try + { + text = File.ReadAllText (inputPath); + } + catch (Exception ex) + { + Console.Error.WriteLine ($"Could not read \"{inputPath}\": {ex.Message}"); + + return 2; + } + + JsonNodeOptions nodeOpts = new () { PropertyNameCaseInsensitive = false }; + + JsonDocumentOptions docOpts = new () + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + JsonNode? root; + + try + { + root = JsonNode.Parse (text, nodeOpts, docOpts); + } + catch (JsonException ex) + { + Console.Error.WriteLine ($"Could not parse \"{inputPath}\" as JSON: {ex.Message}"); + + return 2; + } + + if (root is not JsonObject rootObj) + { + Console.Error.WriteLine ("Top-level JSON value is not an object; cannot migrate."); + + return 2; + } + + JsonObject migrated = ConfigJsonMigrator.MigrateObject (rootObj); + + JsonSerializerOptions writeOpts = new () + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + string output = migrated.ToJsonString (writeOpts); + + if (outputPath is null) + { + Console.WriteLine (output); + } + else + { + try + { + File.WriteAllText (outputPath, output); + Console.Error.WriteLine ($"Migrated \"{inputPath}\" -> \"{outputPath}\"."); + } + catch (Exception ex) + { + Console.Error.WriteLine ($"Could not write \"{outputPath}\": {ex.Message}"); + + return 2; + } + } + + return 0; + } + +} diff --git a/Tools/MigrateConfig/README.md b/Tools/MigrateConfig/README.md new file mode 100644 index 0000000000..1c7cf6d05e --- /dev/null +++ b/Tools/MigrateConfig/README.md @@ -0,0 +1,79 @@ +# migrate-tui-config + +Migrates a pre-MEC Terminal.Gui `config.json` (flat-key, array-themes +shape) to the nested shape consumed by `TuiConfigurationBuilder`. + +## When you need this + +Terminal.Gui v2.x originally used a flat-key `config.json` format: + +```json +{ + "Button.DefaultShadow": "Opaque", + "Themes": [ + { "Dark": { "Glyphs.CheckStateChecked": "☑" } } + ] +} +``` + +Starting in the release that ships PR #5416, the library only reads +the nested MEC-native shape: + +```json +{ + "Button": { "DefaultShadow": "Opaque" }, + "Themes": { + "Dark": { "Glyphs": { "CheckStateChecked": "☑" } } + } +} +``` + +Files in the old shape are detected at load time and ignored with a +`WARN` log; their settings fall through to defaults. This tool produces +a migrated copy you can drop in place. + +## Usage + +```bash +# Write to a new file +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json ./.tui/config.migrated.json + +# Or pipe to stdout +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json +``` + +Exit codes: + +- `0` — success +- `1` — usage error (wrong number of arguments) +- `2` — I/O or JSON parse error + +## What it does + +Three transforms applied recursively: + +1. Property names containing `.` are split into nested objects. + `"Button.DefaultShadow": "Opaque"` becomes + `"Button": { "DefaultShadow": "Opaque" }`. +2. `"Themes"` arrays of single-key objects become dictionaries. + `"Themes": [{"Dark": {...}}]` becomes `"Themes": {"Dark": {...}}`. +3. `"Schemes"` arrays inside a theme get the same collapse. + +JSON comments and trailing commas in the input are tolerated but +**not** preserved in the output — re-add hand-edited comments after +migration. + +## What it doesn't do + +- It does not validate that the resulting JSON binds cleanly to the + v2.x Settings POCOs. Run your app afterwards and watch for any + `OptionsValidationException`. +- It does not preserve property order beyond what `System.Text.Json` + guarantees for a `JsonObject`. + +## Lifecycle + +This tool is not part of the shipping `Terminal.Gui` library and is +not included in `Terminal.slnx`. It exists to ease the one-time +migration in the release that drops the legacy shape. It will be +removed in a future release once the upgrade window is past. diff --git a/Tools/README.md b/Tools/README.md new file mode 100644 index 0000000000..66494a5333 --- /dev/null +++ b/Tools/README.md @@ -0,0 +1,28 @@ +# Tools/ + +Standalone utilities that are **not** part of the shipping `Terminal.Gui` +library and **not** part of the `Examples/` set users learn the library +from. + +## What lives here + +| Tool | Purpose | Shipped? | +|------|---------|----------| +| [`MigrateConfig/`](./MigrateConfig/) | Converts a pre-MEC flat-key `config.json` to the nested MEC schema | No — standalone console app, run on demand | + +## Conventions + +- Each tool gets its own folder + `.csproj` + `README.md`. +- Tool csprojs are **not** added to `Terminal.slnx`. They are built + on demand (`dotnet run --project Tools//`). +- Tools are not user-facing examples. If you want to demonstrate + Terminal.Gui idioms, add to `Examples/` instead. +- Tools may be deleted in any release without a deprecation cycle. + +## Why a separate folder? + +Without this folder, one-off utilities tend to land either inside +`Terminal.Gui.dll` (bloating AOT output) or inside `Examples/` (where +they mislead newcomers into thinking they're recommended app patterns). +The `Tools/` convention is also used by `dotnet/roslyn`, +`dotnet/runtime`, and `dotnet/aspnetcore`. diff --git a/ai-v2-primer.md b/ai-v2-primer.md index f1f4a5c2fd..42e2594927 100644 --- a/ai-v2-primer.md +++ b/ai-v2-primer.md @@ -37,7 +37,7 @@ most of what you "know" about Terminal.Gui is **wrong**. The API has fundamental | `view.Bounds` | `view.Viewport` | | `LayoutStyle.Computed` | Removed — all layout is declarative via `Pos`/`Dim` | | `new RadioGroup (...)` | `new OptionSelector { ... }` | -| `Colors.ColorSchemes ["name"]` | `Schemes.Resolve ("name")` or use `Scheme` directly | +| `Colors.ColorSchemes ["name"]` | `SchemeManager.GetScheme ("name")` or use `Scheme` directly | | `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | | `Pos.At (n)` / `Pos.Left (v)` | Assign integers directly: `X = 5;` (implicit conversion) | @@ -88,7 +88,7 @@ public sealed class MainWindow : Runnable | `Terminal.Gui.Drawing` | `Color`, `Attribute`, `Scheme`, `LineCanvas`, `Glyphs` | | `Terminal.Gui.Input` | `Key`, `KeyCode`, `Command`, `KeyBindings`, `MouseBindings` | | `Terminal.Gui.Text` | `TextFormatter`, `TextDirection` | -| `Terminal.Gui.Configuration` | `ConfigurationManager`, themes | +| `Terminal.Gui.Configuration` | `TuiConfigurationBuilder`, Settings POCOs, themes | --- diff --git a/docfx/README.md b/docfx/README.md index f4826d1e02..9d45237772 100644 --- a/docfx/README.md +++ b/docfx/README.md @@ -58,7 +58,7 @@ grep "ClassName" docfx/_site/xrefmap.yml | grep "^- uid:" | `Command`, `CommandRouting`, `ICommandContext`, `CommandContext`, `ICommandBinding`, `CommandBridge` | `Terminal.Gui.Input.` | | `View` | `Terminal.Gui.ViewBase.View` | | All view classes (`Button`, `CheckBox`, `Dialog`, etc.) | `Terminal.Gui.Views.` | -| `IAcceptTarget` | `Terminal.Gui.` | +| `IAcceptTarget` | `Terminal.Gui.Input.` | ### Syntax @@ -85,7 +85,7 @@ grep "ClassName" docfx/_site/xrefmap.yml | grep "^- uid:" | `ICommandContext` | `Terminal.Gui.Input.ICommandContext` | | `CommandContext` | `Terminal.Gui.Input.CommandContext` | | `CommandBridge` | `Terminal.Gui.Input.CommandBridge` | -| `IAcceptTarget` | `Terminal.Gui.IAcceptTarget` | +| `IAcceptTarget` | `Terminal.Gui.Input.IAcceptTarget` | | `View` | `Terminal.Gui.ViewBase.View` | | `View.CommandsToBubbleUp` | `Terminal.Gui.ViewBase.View.CommandsToBubbleUp` | | `View.DefaultAcceptView` | `Terminal.Gui.ViewBase.View.DefaultAcceptView` | diff --git a/docfx/apispec/namespace-configuration.md b/docfx/apispec/namespace-configuration.md index d912ee345b..612bd9f153 100644 --- a/docfx/apispec/namespace-configuration.md +++ b/docfx/apispec/namespace-configuration.md @@ -3,46 +3,28 @@ uid: Terminal.Gui.Configuration summary: Configuration management, themes, and persistent settings. --- -The `Configuration` namespace provides comprehensive configuration management for Terminal.Gui applications. +The `Configuration` namespace loads Terminal.Gui settings from JSON using Microsoft.Extensions.Configuration. ## Key Types -- **ConfigurationManager** - Central system for loading and applying configuration -- **ConfigurationPropertyAttribute** - Marks properties as configurable -- **Scope** - Configuration contexts (Settings, Themes, AppSettings) -- **ThemeManager** - Theme loading and application - -## Configuration Scopes - -| Scope | Purpose | -|-------|---------| -| Settings | Runtime behavior settings | -| Themes | Visual styling and color schemes | -| AppSettings | Application-specific settings | - -## Configuration Sources - -Configuration is loaded from multiple locations in priority order: - -1. Application directory (`appSettings.json`) -2. User directory (`~/.tui/`) -3. Environment variables -4. Code-based defaults +- **TuiConfigurationBuilder** - Builds the source chain and applies values to Settings POCOs +- **TuiConfigurationExtensions** - `AddTuiLibraryDefaults`, `AddTuiUserFiles`, `AddTuiRuntimeConfig` +- **IThemeManager** / **MecThemeManager** - Theme names and `SwitchTheme` +- **ISchemeManager** / **SchemeManager** - Named schemes +- **ThemeChanges** - Process-wide `ThemeChanged` observer +- **\*Settings** records (`ButtonSettings`, `GlyphSettings`, …) - Bind targets; theme-scoped types expose `Current` ## Example ```csharp -// Enable configuration from all sources -ConfigurationManager.Enable (ConfigLocations.All); - -// Access theme -ThemeManager.Theme = "Dark"; +TuiConfigurationBuilder builder = new ("MyApp"); +builder.RuntimeConfig = """{ "Theme": "Dark" }"""; +builder.ApplyToStaticFacades (); -// Mark a property as configurable -[ConfigurationProperty (Scope = typeof (SettingsScope))] -public static bool MyFeature { get; set; } = true; +builder.ThemeManager.SwitchTheme ("Dark"); ``` ## See Also - [Configuration Deep Dive](~/docs/config.md) +- [Migrating ConfigurationManager](~/docs/migrate-cm-to-mec.md) diff --git a/docfx/docs/breaking-changes-2.5.0.md b/docfx/docs/breaking-changes-2.5.0.md new file mode 100644 index 0000000000..3e16386d21 --- /dev/null +++ b/docfx/docs/breaking-changes-2.5.0.md @@ -0,0 +1,35 @@ +# Terminal.Gui 2.5.0 Breaking Changes + +Terminal.Gui 2.5.0 is a minor bump that includes planned API breaks. Apps that compiled against 2.4.17 may need source changes. + +The GitHub Release notes list for this train lives on [#5630](https://github.com/tui-cs/Terminal.Gui/issues/5630). This page covers the four breaks that belong in the conceptual docs. + +## `View.Text` is no longer virtual + + is a non-virtual property. To validate or cancel a change, override `OnTextChanging(string)` or subscribe to . To react after a change, override `OnTextChanged` or subscribe to . + +`TextField` uses `View.Text` directly (no `new` hider). + +See [Cancellable Work Pattern](cancellable-work-pattern.md) and [Events](events.md). + +## `IAcceptTarget` moved to `Terminal.Gui.Input` + + now lives in `Terminal.Gui.Input`. To compile against 2.5.0, add `using Terminal.Gui.Input;`. + +See [Command](command.md). + +## `ConfigurationManager` and `[ConfigurationProperty]` removed + +2.5.0 deletes the legacy `ConfigurationManager` type and the `[ConfigurationProperty]` attribute that 2.4.17 marked `[Obsolete]`. + +To load themes and settings, use and Settings POCOs such as `ButtonSettings.Current`. + +See [Configuration](config.md) and [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md). + +## `config.json` is nested only + +A pre-2.5.0 file with dotted keys (`"Button.DefaultShadow"`) or array-shaped `Themes` / `Schemes` is not applied. A `WARN` log names the file. + +To convert a legacy file, run `Tools/MigrateConfig`. Nested JSON is the supported contract. + +See [Configuration](config.md) and [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md). diff --git a/docfx/docs/cancellable-work-pattern.md b/docfx/docs/cancellable-work-pattern.md index a31de6a640..87f95dcd8c 100644 --- a/docfx/docs/cancellable-work-pattern.md +++ b/docfx/docs/cancellable-work-pattern.md @@ -270,10 +270,10 @@ public class CommandExecutor ### Example 4: Property Changes (Cancellable) -A `ConfigurationManager` class manages configuration properties with cancellable change notifications. +A settings facade manages configuration properties with cancellable change notifications. ```csharp -public class ConfigurationManager +public class ThemeSettingsExample { private string _theme = "Default"; diff --git a/docfx/docs/command-diagrams.md b/docfx/docs/command-diagrams.md index 84e1f8ec18..3948c7beab 100644 --- a/docfx/docs/command-diagrams.md +++ b/docfx/docs/command-diagrams.md @@ -82,7 +82,7 @@ flowchart TD - `DispatchDown` creates a new context with `Routing = DispatchingDown`, suppressing re-bubbling in the target and preventing infinite recursion. - `TextField.DefaultAcceptHandler` skips its own redirect because `acceptWillBubble = true` — this prevents double-handling. - is a property on each view that returns the first `IAcceptTarget { IsDefault: true }` SubView (typically a ). It is not inherited from the SuperView. -- returns `true` from `DefaultAcceptHandler` because it implements . +- returns `true` from `DefaultAcceptHandler` because it implements . ### Level 3: Complete Flow with MenuBarItem, Menu, and MenuItem diff --git a/docfx/docs/command.md b/docfx/docs/command.md index ff3b9c2787..93996e1bee 100644 --- a/docfx/docs/command.md +++ b/docfx/docs/command.md @@ -324,7 +324,7 @@ Bound to `Key.Enter`. 4. If not handled, redirects to via `DispatchDown` (unless Accept will also bubble to an ancestor — prevents double-path) 5. For `BubblingUp` with a dispatch target, calls 6. Calls -7. Returns `true` if: redirected, will bubble to ancestor, routing is `BubblingUp`, or view is +7. Returns `true` if: redirected, will bubble to ancestor, routing is `BubblingUp`, or view is ### `DefaultHotKeyHandler` () @@ -404,7 +404,7 @@ Called by , : handles + redirect logic +3. For : handles + redirect logic 4. If command is in `SuperView.CommandsToBubbleUp` → invoke on SuperView with `Routing = BubblingUp` 5. Handles `Padding` edge cases (checks Padding's parent) @@ -420,14 +420,14 @@ protected bool? DispatchDown (View target, ICommandContext? ctx) Creates a with `Routing = CommandRouting.DispatchingDown` and invokes on the target. checks for `DispatchingDown` and skips bubbling, preventing infinite recursion. -### and +### and identifies the SubView that receives when no other handles it. Defaults to the first `IAcceptTarget { IsDefault: true }` SubView (typically a ). - affects flow in three ways: + affects flow in three ways: 1. **Resolution**: searches for `IAcceptTarget { IsDefault: true }` -2. **Return value**: `DefaultAcceptHandler` returns `true` for views -3. **Redirect**: Non-default sources bubble up when a exists +2. **Return value**: `DefaultAcceptHandler` returns `true` for views +3. **Redirect**: Non-default sources bubble up when a exists ## CommandBridge @@ -650,7 +650,7 @@ When an inner activates (via click/space), th 1. ** base**: Space → , Enter → , `LeftButtonReleased` → . Subclasses override or extend. -2. ****: Implements . All interactions map to . +2. ****: Implements . All interactions map to . 3. **Selector views**: Use `ConsumeDispatch=true`. Inner commands are consumed; don't propagate to SuperView. diff --git a/docfx/docs/config.md b/docfx/docs/config.md index c73bc7a34f..7d8e33e8ae 100644 --- a/docfx/docs/config.md +++ b/docfx/docs/config.md @@ -1,1156 +1,157 @@ -# Configuration Management Deep Dive +# Configuration Deep Dive -Terminal.Gui provides a comprehensive configuration system that allows users and developers to customize application behavior and appearance through JSON configuration files. The [ConfigurationManager](~/api/Terminal.Gui.Configuration.ConfigurationManager.yml) enables persistent settings, themes, and application-specific preferences. +Terminal.Gui loads themes, glyphs, key bindings, and view defaults from JSON using [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/core/extensions/configuration) via . -## Table of Contents +The legacy `ConfigurationManager` type was removed in 2.5.0. To convert a pre-2.5.0 `config.json`, see [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md). For the other 2.5.0 API breaks, see [2.5.0 Breaking Changes](breaking-changes-2.5.0.md). -- [Overview](#overview) -- [Getting Started](#getting-started) -- [Configuration Scopes](#configuration-scopes) -- [Configuration Locations and Precedence](#configuration-locations-and-precedence) -- [Themes and Schemes](#themes-and-schemes) -- [Defining Configuration Properties](#defining-configuration-properties) -- [Loading and Applying Configuration](#loading-and-applying-configuration) -- [Events](#events) -- [What Can Be Configured](#what-can-be-configured) -- [Configuration File Format](#configuration-file-format) -- [Best Practices](#best-practices) +## Quick start ---- - -## Overview - -The [ConfigurationManager](~/api/Terminal.Gui.Configuration.ConfigurationManager.yml) provides: - -- **Persistent Settings** - User preferences stored in JSON files -- **Theme System** - Named collections of visual settings -- **Scheme Management** - Color and text style definitions -- **Configuration Precedence** - Layered configuration from multiple sources -- **Runtime Configuration** - In-memory configuration without files -- **AOT Compatible** - Works with Native AOT compilation - -### Key Features - -- JSON-based configuration with schema validation -- Multiple configuration locations (user home, app directory, resources) -- Process-wide settings using static properties -- Built-in themes (Default, Dark, Light, etc.) -- Custom glyphs and Unicode characters -- Event-driven configuration changes - ---- - -## Getting Started - -### Enabling Configuration - -**ConfigurationManager is disabled by default** and must be explicitly enabled: - -```csharp -using Terminal.Gui.Configuration; - -class Program -{ - static void Main() - { - // Enable configuration with all sources - ConfigurationManager.Enable(ConfigLocations.All); - - using IApplication app = Application.Create(); - app.Init(); - // ... rest of app - } -} -``` - -### Quick Example - -```csharp -// Enable configuration -ConfigurationManager.Enable(ConfigLocations.All); - -// Listen for configuration changes -ConfigurationManager.Applied += (sender, e) => -{ - Console.WriteLine("Configuration applied!"); -}; - -// Switch themes -ThemeManager.Theme = "Dark"; -ConfigurationManager.Apply(); -``` - -To customize syntax highlighting colors, override the `Code*` visual roles on a scheme. See -[tui-cs/Examples](https://github.com/tui-cs/Examples) for theme configuration examples. - ---- - -## Configuration Scopes - -Terminal.Gui uses three configuration scopes, each serving a different purpose: - -### 1. SettingsScope - -System-level settings that affect Terminal.Gui behavior. Only Terminal.Gui library developers can define [SettingsScope](~/api/Terminal.Gui.Configuration.SettingsScope.yml) properties. +Library defaults are applied when `Terminal.Gui.dll` loads. To overlay runtime JSON or switch themes: ```csharp -[ConfigurationProperty(Scope = typeof(SettingsScope))] -public static bool Force16Colors { get; set; } = false; -``` +TuiConfigurationBuilder builder = new ("MyApp"); +builder.RuntimeConfig = """{ "Theme": "Dark" }"""; +builder.ApplyToStaticFacades (); -**Examples:** -- `Application.DefaultKeyBindings` (e.g. `Command.Quit`) - Default keys for application-level commands -- `Driver.Force16Colors` - Force 16-color mode -- `Key.Separator` - Character separating keys in key combinations - -### 2. ThemeScope - -Visual appearance settings that can be themed. Only Terminal.Gui library developers can define [ThemeScope](~/api/Terminal.Gui.Configuration.ThemeScope.yml) properties. - -```csharp -[ConfigurationProperty(Scope = typeof(ThemeScope))] -public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; +builder.ThemeManager.SwitchTheme ("Dark"); ``` -**Examples:** -- `Window.DefaultBorderStyle` - Default border style for windows -- `Dialog.DefaultShadow` - Default shadow style for dialogs -- - Color schemes for the theme +Views read static facades such as `Button.DefaultShadow` (backed by `ButtonSettings.Current`). -### 3. AppSettingsScope (Default) +## Sources and precedence -Application-specific settings. Application developers can define [AppSettingsScope](~/api/Terminal.Gui.Configuration.AppSettingsScope.yml) properties for their apps. +`TuiConfigurationBuilder.Build` loads sources lowest-to-highest: -```csharp -[ConfigurationProperty] // AppSettingsScope is default -public static string MyAppSetting { get; set; } = "default value"; -``` +1. Hard-coded Settings POCO `init` defaults +2. Library embedded `Terminal.Gui.Resources.config.json` +3. App embedded `config.json` +4. `~/.tui/config.json` and `./.tui/config.json` +5. `~/.tui/{appName}.config.json` and `./.tui/{appName}.config.json` +6. `TUI_CONFIG` environment variable (inline JSON) +7. `TuiConfigurationBuilder.RuntimeConfig` -**Important:** -- App developers **cannot** define or properties -- AppSettings property names must be globally unique (automatically prefixed with class name) +Later sources override earlier ones property-by-property. ---- +`./.tui/` paths resolve against the process's current directory (never the app's install directory). -## Configuration Locations and Precedence +A bad source never crashes the app. A malformed source (invalid JSON, an unreadable file, an invalid scheme value) is skipped; the error is collected in `TuiJsonErrors` and logged at shutdown. A legacy-shaped source (dotted keys or array `Themes`/`Schemes`) is skipped with a `WARN` log — convert it with `Tools/MigrateConfig`. -Configuration is loaded from multiple locations with increasing precedence (higher numbers override lower): +## JSON shape -### ConfigLocations Enum - -[ConfigLocations](~/api/Terminal.Gui.Configuration.ConfigLocations.yml) specifies where configuration can be loaded from: - -1. **[ConfigLocations.HardCoded](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** (Lowest Precedence) - - Default values in code (static property initializers) - - Always available, even when ConfigurationManager is disabled - -2. **[ConfigLocations.LibraryResources](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - Settings in `Terminal.Gui.dll` resources (`Terminal.Gui.Resources.config.json`) - - Defines default themes and settings for the library - -3. **[ConfigLocations.AppResources](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - App-specific resources (`MyApp.Resources.config.json` or `Resources/config.json`) - - Embedded in the application assembly - -4. **[ConfigLocations.GlobalHome](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - Global file in user's home directory (`~/.tui/config.json`) - -5. **[ConfigLocations.GlobalCurrent](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - Global file in current directory (`./.tui/config.json`) - -6. **[ConfigLocations.AppHome](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - App-specific file in user's home directory (`~/.tui/MyApp.config.json`) - -7. **[ConfigLocations.AppCurrent](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - App-specific file in current directory (`./.tui/MyApp.config.json`) - -8. **[ConfigLocations.Env](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** - - Settings from the `TUI_CONFIG` environment variable - - Useful for container environments and CI/CD pipelines - -9. **[ConfigLocations.Runtime](~/api/Terminal.Gui.Configuration.ConfigLocations.yml)** (Highest Precedence) - - Settings in 's `RuntimeConfig` string property - - In-memory configuration without files - -### Precedence Diagram - -```mermaid -graph TD - A[1. Hard-coded Defaults] --> B[2. Library Resources] - B --> C[3. App Resources] - C --> D[4. Global Home Directory] - D --> E[5. Global Current Directory] - E --> F[6. App Home Directory] - F --> G[7. App Current Directory] - G --> H[8. Environment Variable TUI_CONFIG] - H --> I[9. Runtime Config - Highest Priority] - - style A fill:#f9f9f9 - style I fill:#90EE90 -``` - -### File Locations - -**Global Settings** (`config.json`): -- Windows: `C:\Users\username\.tui\config.json` -- macOS/Linux: `~/.tui/config.json` or `./.tui/config.json` - -**App-Specific Settings** (`AppName.config.json`): -- Windows: `C:\Users\username\.tui\UICatalog.config.json` -- macOS/Linux: `~/.tui/UICatalog.config.json` or `./.tui/UICatalog.config.json` - -**Environment Variable** (`TUI_CONFIG`): -```bash -# Linux/macOS -export TUI_CONFIG='{"Application.DefaultKeyBindings": {"Quit": {"All": ["Ctrl+Q"]}}}' - -# Windows PowerShell -$env:TUI_CONFIG='{"Application.DefaultKeyBindings": {"Quit": {"All": ["Ctrl+Q"]}}}' -``` - ---- - -## Themes and Schemes - -### Theme System - -A **Theme** is a named collection of visual settings bundled together. Terminal.Gui includes several built-in themes. - -#### Built-in Themes - -- **Default** - The default Terminal.Gui theme (matches hard-coded defaults) -- **Dark** - Dark color scheme with heavy borders -- **Light** - Light color scheme -- **TurboPascal 5** - Classic Turbo Pascal IDE colors -- **And more** - See `Terminal.Gui/Resources/config.json` for all built-in themes - -#### Using Themes - -```csharp -// Get current theme -ThemeScope currentTheme = ThemeManager.GetCurrentTheme(); - -// Get all available themes. Before ConfigurationManager is initialized, -// this falls back to the hard-coded Default theme. -ConcurrentDictionary themes = ThemeManager.Themes!; - -// Get theme names -ImmutableList themeNames = ThemeManager.GetThemeNames(); - -// Switch themes -ThemeManager.Theme = "Dark"; -ConfigurationManager.Apply(); - -// Listen for theme changes -ThemeManager.ThemeChanged += (sender, e) => -{ - // e.Value is the new theme name - // Update UI based on new theme -}; -``` - -### Scheme System - -A **Scheme** defines the colors and text styles for a specific UI context (e.g., Dialog, Menu, Accent). - -See the [Scheme Deep Dive](scheme.md) for complete details on the scheme system. - -#### Built-in Schemes - -[Schemes](~/api/Terminal.Gui.Drawing.Schemes.yml) enum defines the standard schemes: - -- **Base** - Default for most views -- **Accent** - Secondary/alternate scheme for visual distinction (opaque, derived from Base) -- **Dialog** - Dialogs and message boxes -- **Menu** - Menus and status bars -- **Error** - Error messages and dialogs - -#### Working with Schemes - -```csharp -// Get all schemes for current theme -Dictionary schemes = SchemeManager.GetSchemesForCurrentTheme(); - -// Get specific scheme -Scheme dialogScheme = SchemeManager.GetScheme(Schemes.Dialog); - -// Get scheme names -ImmutableList schemeNames = SchemeManager.GetSchemeNames(); - -// Add custom scheme -SchemeManager.AddScheme("MyScheme", new Scheme -{ - Normal = new Attribute(Color.White, Color.Blue), - Focus = new Attribute(Color.Black, Color.Cyan) -}); -``` - -#### Custom Schemes for Individual Views - -Any view can be given a named scheme by setting `View.SchemeName`. When set, `GetScheme()` looks up that name in the active theme and uses it if found. If the name is not found in the current theme, it falls back through the normal resolution chain (SuperView → `"Base"` → hard-coded `"Base"`) rather than throwing. - -```csharp -// 1. Register the custom scheme (call before Application.Init or after ConfigurationManager.Apply) -SchemeManager.AddScheme("Highlight", new Scheme -{ - Normal = new Attribute(Color.Black, Color.BrightYellow), - Focus = new Attribute(Color.White, Color.BrightYellow) -}); - -// 2. Assign the scheme name to any view -Label warningLabel = new () { Text = "Warning!" }; -warningLabel.SchemeName = "Highlight"; -``` - -Custom schemes can also be defined in a config JSON file so they are theme-aware: +Settings are **nested objects**, not dotted keys. The JSON Schema is [`docfx/schemas/tui-config-schema.json`](../schemas/tui-config-schema.json), hosted at `https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json` after docs publish. ```json { - "Themes": [ - { - "Default": { - "Schemes": [ - { - "Highlight": { - "Normal": { "Foreground": "Black", "Background": "BrightYellow" }, - "Focus": { "Foreground": "White", "Background": "BrightYellow" } - } - } - ] - } - } - ] -} -``` - -When the active theme changes, any view with `SchemeName = "Highlight"` automatically picks up the new theme's definition of that scheme. - -#### Scheme Resolution Order - -`View.GetScheme()` resolves the scheme for a view using the following priority order: - -```mermaid -flowchart TD - A([GetScheme called]) --> B{HasScheme?\nexplicit Scheme set} - B -- Yes --> C([Return explicit Scheme]) - B -- No --> D{SchemeName set?} - D -- No --> E{SuperView exists?} - D -- Yes --> F{TryGetScheme\nSchemeName found?} - F -- Yes --> G([Return named Scheme]) - F -- No --> H[/Logging.Warning emitted/] - H --> E - E -- Yes --> I([Return SuperView.GetScheme]) - E -- No --> J{TryGetScheme\n'Base' found?} - J -- Yes --> K([Return 'Base' from active theme]) - J -- No --> L([Return hard-coded 'Base']) -``` - -| Priority | Condition | Result | -|----------|-----------|--------| -| 1 | `HasScheme` is true | Explicit `Scheme` instance used as-is | -| 2 | `SchemeName` is set and found in active theme | Named scheme returned | -| 3 | `SchemeName` is set but **not** found | `Logging.Warning` emitted; fallback continues | -| 4 | `SuperView` exists | `SuperView.GetScheme()` (recursive) | -| 5 | `"Base"` exists in active theme | Active theme's `"Base"` scheme | -| 6 | *(last resort)* | Hard-coded `"Base"` — always available | - -When writing code that looks up a scheme by name, prefer `SchemeManager.TryGetScheme()` over `SchemeManager.GetScheme(string)` — the `Try` variant returns `false` instead of throwing `KeyNotFoundException` when the name is not found. - -```csharp -if (SchemeManager.TryGetScheme("Highlight", out Scheme? scheme)) -{ - // use scheme -} -``` - -#### Scheme Structure - -Each [Scheme](~/api/Terminal.Gui.Drawing.Scheme.yml) maps [VisualRole](~/api/Terminal.Gui.Drawing.VisualRole.yml) to [Attribute](~/api/Terminal.Gui.Drawing.Attribute.yml): - -```json -{ - "Accent": { - "Normal": { - "Foreground": "BrightGreen", - "Background": "Black", - "Style": "None" - }, - "Focus": { - "Foreground": "White", - "Background": "Cyan", - "Style": "Bold" - }, - "HotNormal": { - "Foreground": "Yellow", - "Background": "Black" - }, - "HotFocus": { - "Foreground": "Blue", - "Background": "Cyan", - "Style": "Underline" - }, - "Active": { - "Foreground": "White", - "Background": "DarkCyan" - }, - "HotActive": { - "Foreground": "Yellow", - "Background": "DarkCyan" - }, - "Highlight": { - "Foreground": "Black", - "Background": "BrightGreen" - }, - "Editable": { - "Foreground": "White", - "Background": "DarkBlue" - }, - "ReadOnly": { - "Foreground": "Gray", - "Background": "Black" - }, - "Disabled": { - "Foreground": "DarkGray", - "Background": "Black", - "Style": "Faint" - } - } -} -``` - ---- - -## Defining Configuration Properties - -### Basic Property Definition - -Application developers define settings using the [ConfigurationPropertyAttribute](~/api/Terminal.Gui.Configuration.ConfigurationPropertyAttribute.yml): - -```csharp -public class MyApp -{ - [ConfigurationProperty] - public static string MySetting { get; set; } = "Default Value"; - - [ConfigurationProperty] - public static int MaxItems { get; set; } = 100; -} -``` - -**Requirements:** -- Must be `public` or `internal` -- Must be `static` -- Must be a property (not a field) -- Must have a default value - -### Property Naming - -AppSettings properties are automatically prefixed with the class name to ensure global uniqueness: - -```csharp -// Code -public class MyApp -{ - [ConfigurationProperty] - public static string MySetting { get; set; } = "value"; -} - -// JSON -{ - "AppSettings": { - "MyApp.MySetting": "value" - } -} -``` - -### Scope Specification - -Use the `Scope` parameter to specify non-default scopes (Terminal.Gui library only): - -```csharp -// SettingsScope - Library-wide settings -[ConfigurationProperty(Scope = typeof(SettingsScope))] -public static bool Force16Colors { get; set; } = false; - -// ThemeScope - Visual settings -[ConfigurationProperty(Scope = typeof(ThemeScope))] -public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; - -// AppSettingsScope - Application settings (default) -[ConfigurationProperty] // or explicitly: Scope = typeof(AppSettingsScope) -public static string MyAppSetting { get; set; } = "default"; -``` - -### Omit Class Name (Advanced) - -For library developers only, use `OmitClassName = true` for cleaner JSON: - -```csharp -[ConfigurationProperty(Scope = typeof(ThemeScope), OmitClassName = true)] -public static Dictionary Schemes { get; set; } = new(); -``` - ---- - -## Loading and Applying Configuration - -### Enable with Load and Apply - -The simplest approach - enable and load in one call: - -```csharp -ConfigurationManager.Enable(ConfigLocations.All); -``` - -This: -1. Enables ConfigurationManager -2. Loads configuration from all locations -3. Applies settings to the application - -### Configuration Gotchas - -- `ConfigurationManager.Initialize()` is internal and runs automatically when the module loads. Do not call it directly. -- Initialization discovers configuration properties and caches hard-coded defaults, but it does **not** enable resource-backed configuration. -- To load `Resources/config.json`, home/current-directory config files, `TUI_CONFIG`, or `RuntimeConfig`, call `ConfigurationManager.Enable(...)`. -- `ThemeManager.Themes` falls back to the hard-coded `Default` theme before configuration is initialized. After initialization, it expects a `Themes` entry in settings and can throw if that entry is unavailable. - -### Granular Control - -For more control, use 's `Load` and `Apply()` separately: - -```csharp -// Enable without loading -ConfigurationManager.Enable(ConfigLocations.None); - -// Load from specific locations -ConfigurationManager.Load(ConfigLocations.GlobalHome | ConfigLocations.AppResources); - -// Apply settings -ConfigurationManager.Apply(); -``` - -### Runtime Configuration - -Set configuration directly in code without files: - -```csharp -ConfigurationManager.RuntimeConfig = @" -{ - ""Application.DefaultKeyBindings.Quit"": ""Ctrl+Q"", - ""Application.Force16Colors"": true -}"; - -ConfigurationManager.Enable(ConfigLocations.Runtime); -``` - -### Reset to Defaults - -Reset all settings to hard-coded defaults: - -```csharp -ConfigurationManager.ResetToHardCodedDefaults(); -``` - ---- - -## Events - -The ConfigurationManager provides events to track configuration changes: - -### Applied Event - -Raised after configuration is applied to the application: - -```csharp -ConfigurationManager.Applied += (_, _) => -{ - // Configuration has been applied - // Update UI or refresh views -}; -``` - -### Updated Event - -Raised after configuration is loaded from a source or reset (before `Apply()` is called): - -```csharp -ConfigurationManager.Updated += (_, _) => -{ - // Configuration has been loaded or reset - // Inspect ConfigurationManager.Settings if needed -}; -``` - -### ThemeChanged Event - -Raised when the active theme changes: - -```csharp -ThemeManager.ThemeChanged += (_, e) => -{ - // e.Value is the new theme name - // Refresh all views to use new theme - // From within a View, use: App?.Current?.SetNeedsDraw(); - // Or access via IApplication instance: app.Current?.SetNeedsDraw(); -}; -``` - ---- - -## What Can Be Configured - -### Application Settings - -System-wide settings from [SettingsScope](~/api/Terminal.Gui.Configuration.SettingsScope.yml): - -```json -{ - "Application.DefaultKeyBindings.Quit": "Esc", - "Driver.Force16Colors": false, - "Application.IsMouseDisabled": false, - "Application.DefaultKeyBindings.Arrange": "Ctrl+F5", - "Application.DefaultKeyBindings.NextTabStop": "Tab", - "Application.DefaultKeyBindings.PreviousTabStop": "Shift+Tab", - "Application.DefaultKeyBindings.NextTabGroup": "F6", - "Application.DefaultKeyBindings.PreviousTabGroup": "Shift+F6", - "Key.Separator": "+" -} -``` - -### Key Binding Settings - -Configurable key bindings use the `PlatformKeyBinding` format to support platform-aware defaults. See [Key Binding Overrides](#key-binding-overrides) for the full JSON format. - -```json -{ - "Application.DefaultKeyBindings": { - "Quit": { "All": ["Esc"] }, - "Suspend": { "Linux": ["Ctrl+Z"], "Macos": ["Ctrl+Z"] }, - "Arrange": { "All": ["Ctrl+F5"] } + "Theme": "Dark", + "Application": { + "IsMouseDisabled": false }, - "View.DefaultKeyBindings": { - "Copy": { "All": ["Ctrl+C"] }, - "Undo": { "All": ["Ctrl+Z"], "Linux": ["Ctrl+/"], "Macos": ["Ctrl+/"] } + "Button": { + "DefaultShadow": "Opaque" }, - "View.ViewKeyBindings": { - "TextField": { - "CutToEndOfLine": { "All": ["Ctrl+K"] } - } - } -} -``` - -### View-Specific Settings - -Settings for individual View types from [ThemeScope](~/api/Terminal.Gui.Configuration.ThemeScope.yml): - -```json -{ - "Window.DefaultBorderStyle": "Single", - "Window.DefaultShadow": "None", - "Dialog.DefaultBorderStyle": "Heavy", - "Dialog.DefaultShadow": "Transparent", - "Dialog.DefaultButtonAlignment": "End", - "FrameView.DefaultBorderStyle": "Rounded", - "Button.DefaultShadow": "None", - "PopoverMenu.DefaultKey": "Shift+F10", - "FileDialog.MaxSearchResults": 10000 -} -``` - -### Glyphs - -Customize the Unicode characters used for drawing: - -```json -{ - "Glyphs.RightArrow": "►", - "Glyphs.LeftArrow": "U+25C4", - "Glyphs.DownArrow": "\\u25BC", - "Glyphs.UpArrow": 965010, - "Glyphs.LeftBracket": "[", - "Glyphs.RightBracket": "]", - "Glyphs.Checked": "☑", - "Glyphs.UnChecked": "☐", - "Glyphs.Selected": "◉", - "Glyphs.UnSelected": "○" -} -``` - -Glyphs can be specified as: -- Unicode character: `"►"` -- U+ format: `"U+25C4"` -- UTF-16 format: `"\\u25BC"` -- Decimal codepoint: `965010` - -### Key Binding Overrides - -Key bindings for Application-level commands, base View commands, and per-view commands can all be overridden in configuration. See [Keyboard Deep Dive - Configurable Key Bindings](keyboard.md#configurable-key-bindings) for the full architecture. - -**Override Application-level key bindings** (e.g., change the Quit key): - -```json -{ - "Application.DefaultKeyBindings": { - "Quit": { "All": ["Ctrl+Q"] }, - "Suspend": { "Linux": ["Ctrl+Z"], "Macos": ["Ctrl+Z"] } - } -} -``` - -**Override base View key bindings** (affects all views that support those commands): - -```json -{ - "View.DefaultKeyBindings": { - "Copy": { "All": ["Ctrl+C", "Ctrl+Insert"] }, - "Paste": { "All": ["Ctrl+V", "Shift+Insert"] }, - "Undo": { "All": ["Ctrl+Z"], "Linux": ["Ctrl+/"], "Macos": ["Ctrl+/"] } - } -} -``` - -**Override per-view key bindings** using `View.ViewKeyBindings` (maps view type name to command overrides): - -```json -{ - "View.ViewKeyBindings": { - "TextField": { - "Undo": { "All": ["Ctrl+Z"] }, - "CutToEndOfLine": { "All": ["Ctrl+K"] } - }, - "Editor": { - "Redo": { "All": ["Ctrl+Shift+Z"], "Windows": ["Ctrl+Y"] } + "Glyphs": { + "CheckStateChecked": "☑" + }, + "Themes": { + "Dark": { + "Button": { "DefaultShadow": "None" }, + "Glyphs": { "LeftBracket": "[", "RightBracket": "]" } } } } ``` -Each entry uses the `PlatformKeyBinding` format with optional `All`, `Windows`, `Linux`, and `Macos` string arrays. `All` keys apply on every platform; platform-specific arrays add additional bindings for that OS. - -### Discovering Configuration Properties - -To find all available configuration properties: - -```csharp -// Get hard-coded configuration as a JSON string -string hardCodedJson = ConfigurationManager.GetHardCodedConfig(); -Console.WriteLine(hardCodedJson); - -// Or get an empty configuration skeleton -string emptyJson = ConfigurationManager.GetEmptyConfig(); -``` +`Themes` is a dictionary of theme name → overlay. Each overlay uses the same nested section names as the root (`Button`, `Glyphs`, `Dialog`, …). Properties omitted from an overlay keep the root value. -Or search the source code for `[ConfigurationProperty]` attributes. +A pre-MEC file with `"Button.DefaultShadow": "None"` or `"Themes": [ { "Dark": { } } ]` is **not** applied; a warning is logged. Convert it with `Tools/MigrateConfig`. ---- +## Settings POCOs -## Themes and Schemes +| Kind | Types | Storage | +|-------|--------|---------| +| Theme-scoped | `ButtonSettings`, `DialogSettings`, `GlyphSettings`, `MenuSettings`, … | Immutable `record` with `Current` swapped atomically | +| Process-wide | `ApplicationSettings`, `DriverSettings`, `KeySettings`, `ThemeSettings`, … | Mutable `Defaults` instance | -### Theme Structure +Theme overlays apply only to theme-scoped POCOs. The selected theme name is `ThemeSettings.Defaults.Theme` (JSON scalar `"Theme"`). -A theme is a named collection bundling visual settings and schemes: +Key bindings overlay hard-coded defaults from nested JSON: ```json { - "Themes": [ - { - "Dark": { - "Dialog.DefaultBorderStyle": "Heavy", - "Dialog.DefaultShadow": "Transparent", - "Window.DefaultBorderStyle": "Single", - "Button.DefaultShadow": "Opaque", - "Schemes": [ - { - "Accent": { - "Normal": { "Foreground": "BrightGreen", "Background": "Black" }, - "Focus": { "Foreground": "White", "Background": "Cyan" } - }, - "Dialog": { - "Normal": { "Foreground": "Black", "Background": "Gray" } - } - } - ] - } + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } } - ] -} -``` - -### Creating Custom Themes - -Custom themes can be defined in configuration files: - -```json -{ - "Themes": [ - { - "MyCustomTheme": { - "Window.DefaultBorderStyle": "Double", - "Dialog.DefaultShadow": "Opaque", - "Schemes": [ - { - "Base": { - "Normal": { - "Foreground": "Cyan", - "Background": "Black", - "Style": "Bold" - } - } - } - ] - } - } - ] -} -``` - -Then activate the theme: - -```csharp -ThemeManager.Theme = "MyCustomTheme"; -ConfigurationManager.Apply(); -``` - -### Theme Inheritance - -Themes only override specified properties. To build on an existing theme: - -```csharp -// Start with default theme -ThemeManager.Theme = "Default"; -ConfigurationManager.Apply(); - -// Apply custom theme (overrides only what's specified) -ThemeManager.Theme = "MyCustomTheme"; -ConfigurationManager.Apply(); -``` - -### TextStyle in Schemes - -Each [Attribute](~/api/Terminal.Gui.Drawing.Attribute.yml) in a scheme now includes [TextStyle](~/api/Terminal.Gui.Drawing.TextStyle.yml): - -```json -{ - "Normal": { - "Foreground": "White", - "Background": "Blue", - "Style": "Bold, Underline" - } -} -``` - -Available styles (combinable): -- `None` -- `Bold` -- `Faint` -- `Italic` -- `Underline` -- `Blink` -- `Reverse` -- `Strikethrough` - ---- - -## Configuration File Format - -### Schema - -All configuration files must conform to the JSON schema: - -**Schema URL:** https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json - -### Root Structure - -```json -{ - "$schema": "https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json", - - // SettingsScope properties - "Application.DefaultKeyBindings.Quit": "Esc", - "Application.Force16Colors": false, - - // Current theme name - "Theme": "Dark", - - // Theme definitions - "Themes": [ - { - "Dark": { - // ThemeScope properties - "Window.DefaultBorderStyle": "Single", - // Schemes - "Schemes": [ ... ] + }, + "View": { + "ViewKeyBindings": { + "TextField": { + "CutToEndOfLine": { "All": ["Ctrl+K"] } } } - ], - - // AppSettings - "AppSettings": { - "MyApp.MySetting": "value" } } ``` -## Best Practices - -### For Application Developers - -**1. Enable Early** - -Enable ConfigurationManager at the start of `Main()`, before creating the application: - -```csharp -static void Main() -{ - ConfigurationManager.Enable(ConfigLocations.All); - using IApplication app = Application.Create(); - app.Init(); - // ... -} -``` +Unmentioned commands keep their compile-time bindings. See [Keyboard](keyboard.md#user-overrides-via-configuration). -**2. Use AppSettings for App Configuration** +To change a theme-scoped default in code: ```csharp -public class MyApp -{ - [ConfigurationProperty] - public static bool ShowWelcomeMessage { get; set; } = true; - - [ConfigurationProperty] - public static string DefaultDirectory { get; set; } = ""; -} +ButtonSettings.Current = ButtonSettings.Current with { DefaultShadow = ShadowStyles.None }; ``` -**3. Ship Default Configuration as Resource** +## Themes and schemes -Include a `Resources/config.json` file in your app: - -```xml - - - -``` - -**4. Handle Configuration Changes** +- (`TuiConfigurationBuilder.ThemeManager`) lists theme names and calls `SwitchTheme`. +- .`ThemeChanged` is the process-wide observer for theme switches. +- Schemes live inside each theme's `Schemes` dictionary and are applied through . ```csharp -ConfigurationManager.Applied += (sender, e) => +builder.ThemeManager.ThemeChanged += (_, args) => { - // Refresh UI when configuration changes - RefreshAllViews(); + // args.Value is the new theme name }; -``` - -### For Library Developers - -**1. Use Appropriate Scopes** - -- - For system-wide behavior -- - For visual appearance that should be themeable -- Don't use `AppSettingsScope` in library code - -**2. Provide Meaningful Defaults** - -```csharp -[ConfigurationProperty(Scope = typeof(ThemeScope))] -public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; -``` - -**3. Document Configuration Properties** - -```csharp -/// -/// Gets or sets the default border style for all Windows. -/// -[ConfigurationProperty(Scope = typeof(ThemeScope))] -public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; -``` - -### Process-Wide Settings - -> [!IMPORTANT] -> Configuration settings are applied at the **process level**. -> -> Since configuration properties are static, changes affect all applications in the same process. This is typically not an issue for normal applications, but can affect scenarios with: -> - Multiple Terminal.Gui apps in the same process -> - Unit tests running in parallel -> - Hot reload scenarios - ---- - -## Advanced Topics - -### JSON Error Handling - -Control how JSON parsing errors are handled: - -```json -{ - "ConfigurationManager.ThrowOnJsonErrors": true -} -``` - -- `false` (default) - Silent failures, errors logged -- `true` - Throws exceptions on JSON parsing errors - -### Get Configuration as JSON - -Retrieve the current hard-coded defaults as a JSON string: -```csharp -// Get the hard-coded configuration as JSON -string json = ConfigurationManager.GetHardCodedConfig(); -File.WriteAllText("my-config.json", json); - -// Get an empty configuration skeleton (just the $schema tag) -string empty = ConfigurationManager.GetEmptyConfig(); +ThemeChanges.ThemeChanged += (_, _) => view.SetNeedsDraw (); ``` -### Disable ConfigurationManager +## App settings -Disable and optionally reset to defaults: +To bind an application-specific POCO: ```csharp -// Disable but keep current settings -ConfigurationManager.Disable(resetToHardCodedDefaults: false); - -// Disable and reset to hard-coded defaults -ConfigurationManager.Disable(resetToHardCodedDefaults: true); -``` - -### File System Watching - -Watch for configuration file changes: - -```csharp -var watcher = new FileSystemWatcher( - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".tui")); -watcher.Filter = "*.json"; -watcher.Changed += (s, e) => +public class MyAppSettings { - ConfigurationManager.Load(ConfigLocations.GlobalHome); - ConfigurationManager.Apply(); -}; -watcher.EnableRaisingEvents = true; -``` - -See UICatalog's `ConfigurationEditor` scenario for a complete example. - ---- - -## Examples - -### Example 1: Simple Theme Switching - -```csharp -using Terminal.Gui; -using Terminal.Gui.Configuration; - -ConfigurationManager.Enable(ConfigLocations.All); - -using IApplication app = Application.Create(); -app.Init(); - -OptionSelector themeSelector = new () -{ - X = 1, - Y = 1, - Labels = ThemeManager.GetThemeNames() -}; -themeSelector.ValueChanged += (_, e) => -{ - IReadOnlyList? labels = themeSelector.Labels; - if (labels is null || e.NewValue is null) - { - return; - } - - ThemeManager.Theme = labels[e.NewValue.Value]; - ConfigurationManager.Apply(); -}; - -Window win = new () { Title = "Theme Demo" }; -win.Add(themeSelector); -app.Run(win); -``` - -### Example 2: Custom Application Settings - -```csharp -public class MyApp -{ - [ConfigurationProperty] - public static string LastOpenedFile { get; set; } = ""; - - [ConfigurationProperty] - public static int WindowWidth { get; set; } = 80; - - [ConfigurationProperty] - public static int WindowHeight { get; set; } = 25; + public string Title { get; set; } = "My App"; + public static MyAppSettings Defaults { get; set; } = new (); } -// Enable and use -ConfigurationManager.Enable(ConfigLocations.All); +TuiConfigurationBuilder builder = new ("MyApp"); +builder.BindAppSettings ("MyApp", s => MyAppSettings.Defaults = s); +builder.ApplyToStaticFacades (); +``` -// Settings are automatically loaded and applied -var window = new Window -{ - Width = MyApp.WindowWidth, - Height = MyApp.WindowHeight -}; +Corresponding JSON: -// Later, retrieve the hard-coded config as JSON -string json = ConfigurationManager.GetHardCodedConfig(); -// Could save to file here +```json +{ "MyApp": { "Title": "Demo" } } ``` -### Example 3: Runtime Configuration +## Custom sources ```csharp -ConfigurationManager.RuntimeConfig = @" -{ - ""Application.DefaultKeyBindings"": { - ""Quit"": { ""All"": [""Ctrl+Q""] } - }, - ""Driver.Force16Colors"": true, - ""Theme"": ""Dark"" -}"; - -ConfigurationManager.Enable(ConfigLocations.Runtime); - -// Settings are now applied -// Quit key is Ctrl+Q -// 16-color mode is forced -// Dark theme is active +IConfiguration config = new ConfigurationBuilder () + .AddTuiLibraryDefaults () + .AddTuiUserFiles ("MyApp") + .AddJsonFile ("custom-settings.json", optional: true) + .Build (); ``` ---- - -## See Also - -- **[Scheme Deep Dive](scheme.md)** - Color scheme details -- **[Drawing Deep Dive](drawing.md)** - Color and attribute system -- **[View Deep Dive](View.md)** - View configuration properties -- **[Theme Schema](https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json)** - JSON schema for validation -- **[Default Config](https://raw.githubusercontent.com/tui-cs/Terminal.Gui/develop/Terminal.Gui/Resources/config.json)** - Complete default configuration - -### UICatalog Examples - -The UICatalog application demonstrates configuration management: - -- **Configuration Editor** - Interactive editor for configuration files -- **Themes** - Theme viewer and selector -- **File System Watcher** - Automatic reload on configuration file changes - -### API Reference +## See also -- [ConfigurationManager](~/api/Terminal.Gui.Configuration.ConfigurationManager.yml) -- [ConfigLocations](~/api/Terminal.Gui.Configuration.ConfigLocations.yml) -- [SettingsScope](~/api/Terminal.Gui.Configuration.SettingsScope.yml) -- [ThemeScope](~/api/Terminal.Gui.Configuration.ThemeScope.yml) -- [AppSettingsScope](~/api/Terminal.Gui.Configuration.AppSettingsScope.yml) -- [ThemeManager](~/api/Terminal.Gui.Configuration.ThemeManager.yml) -- [SchemeManager](~/api/Terminal.Gui.Configuration.SchemeManager.yml) -- [ConfigurationPropertyAttribute](~/api/Terminal.Gui.Configuration.ConfigurationPropertyAttribute.yml) +- [2.5.0 Breaking Changes](breaking-changes-2.5.0.md) +- [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md) +- [Configuration JSON Schema](../schemas/tui-config-schema.json) +- [Scheme Deep Dive](scheme.md) +- [Menus](menus.md) diff --git a/docfx/docs/drawing.md b/docfx/docs/drawing.md index cfea8dbebc..b22a3ea9b1 100644 --- a/docfx/docs/drawing.md +++ b/docfx/docs/drawing.md @@ -250,7 +250,7 @@ Terminal.Gui supports text formatting using class defines the common set of glyphs used to draw checkboxes, lines, borders, etc... The default glyphs can be changed per-ThemeScope via . +The class defines the common set of glyphs used to draw checkboxes, lines, borders, etc... The default glyphs can be changed per-theme via and . ## LineCanvas Deep Dive diff --git a/docfx/docs/drivers.md b/docfx/docs/drivers.md index 199ceead81..cdd828e592 100644 --- a/docfx/docs/drivers.md +++ b/docfx/docs/drivers.md @@ -25,7 +25,7 @@ Terminal.Gui provides three console driver implementations: | **Output Model** | Pure ANSI escape sequences | Managed .NET + ANSI sequences (when VT mode enabled) | Direct character output via Win32 API with double buffering | | **Unix Write APIs** | `write()` syscall to stdout (fd 1) | N/A (uses .NET `Console.Write()` which internally delegates to platform APIs) | N/A (Windows-only) | | **Windows Write APIs** | P/Invokes `WriteFile()` | N/A (uses .NET `Console.Write()` which internally delegates to platform APIs) | P/Invokes `WriteConsoleW()`, `CreateConsoleScreenBuffer()`/`SetConsoleActiveScreenBuffer()` for double buffering, `SetConsoleTextAttribute()` | -| **Screen Model** | Configurable via `Driver.SizeDetection`. Default (`AnsiQuery`): ANSI `CSI 18t` query, throttled to 500 ms. `Polling`: `ioctl(TIOCGWINSZ)` on Unix, Console API on Windows. | Polling-based re-size: `Console.WindowWidth`/`Console.WindowHeight` queried periodically. Falls back to 80x25 on `IOException`. | Event-based re-size: `WINDOW_BUFFER_SIZE_EVENT` received in input stream via `ReadConsoleInputW()`. Immediate resize notification. `GetConsoleScreenBufferInfoEx()` queries dimensions. | +| **Screen Model** | Configurable via `Driver.SizeDetection`. Default (`Polling`): `ioctl(TIOCGWINSZ)` on Unix, Console API on Windows. `AnsiQuery`: ANSI `CSI 18t` query, throttled to 500 ms. | Polling-based re-size: `Console.WindowWidth`/`Console.WindowHeight` queried periodically. Falls back to 80x25 on `IOException`. | Event-based re-size: `WINDOW_BUFFER_SIZE_EVENT` received in input stream via `ReadConsoleInputW()`. Immediate resize notification. `GetConsoleScreenBufferInfoEx()` queries dimensions. | | **Cursor Handling** | ANSI sequences: DECTCEM (`CSI ? 25 h/l`) for show/hide, DECSCUSR (`CSI Ps SP q`) for style. Full `CursorStyle` support. | ANSI sequences (same as ansi driver). Falls back to `Console.SetCursorPosition()` on Windows. | • Legacy mode: Win32 `CONSOLE_CURSOR_INFO` (size percentage only, no blinking control).
• Modern VT mode: ANSI sequences (same as ansi driver). Full `CursorStyle` support. | | **Advantages** | • Cross-platform (all platforms)
• Pure, clean implementation
• Perfect for testing/CI
• Virtual time support
• Deterministic behavior
• Configurable size detection | • Cross-platform (all platforms)
• Maximum compatibility
• Simple implementation
• No P/Invoke; Works with .NET BCL
| • Highest performance on Windows
• Immediate resize detection
| | **Disadvantages** | • Requires proper ANSI support | • Lower performance (managed overhead)
• Limited feature set
• `System.ReadKey` has bugs on Windows
• Polling-based resize | • Windows-only
• More complex P/Invoke code | @@ -41,11 +41,13 @@ The appropriate driver is automatically selected based on the platform when .ForceDriver: -- It can be set through the configuration system (e.g., `config.json`) +- Set it in nested JSON (`Application: { ForceDriver: "ansi" }`) or on `ApplicationSettings.Defaults` - Changes raise the `ForceDriverChanged` event -- It persists across application instances when using the static class +- Library defaults load when `Terminal.Gui.dll` loads; overlay with `TuiConfigurationBuilder.RuntimeConfig` ```csharp // Subscribe to driver changes @@ -357,23 +359,23 @@ The main driver interface that the framework uses internally. `IDriver` is organ #### Size Detection (ANSI Driver) -The ANSI driver's terminal-size detection strategy is controlled by `Driver.SizeDetection` (a `[ConfigurationProperty]`): +The ANSI driver's terminal-size detection strategy is controlled by `Driver.SizeDetection` (.SizeDetection): | Mode | Mechanism | When to use | |---|---|---| -| `AnsiQuery` (default) | Sends `CSI 18t`, parses `ESC[8;h;wt` response. Async, ~500 ms throttle. | Most terminals. Works everywhere ANSI is supported. | -| `Polling` | `ioctl(TIOCGWINSZ)` on Unix, `Console.WindowWidth/Height` on Windows. Synchronous. | When the ANSI response does not reflect the actual terminal size (e.g., some SSH configurations). | +| `Polling` (default) | `ioctl(TIOCGWINSZ)` on Unix, `Console.WindowWidth/Height` on Windows. Falls back to `AnsiQuery` when the native query is unavailable. | Supported local platforms. Avoids terminal size-query traffic when the native query is available. | +| `AnsiQuery` | Sends `CSI 18t`, parses `ESC[8;h;wt` response. Async, ~500 ms throttle. | Explicit fallback for transports where the native query is unavailable or does not report the correct size. | Set via JSON configuration: ```json -{ "Driver.SizeDetection": "Polling" } +{ "Driver": { "SizeDetection": "AnsiQuery" } } ``` Or programmatically before `Init()`: ```csharp -Driver.SizeDetection = SizeDetectionMode.Polling; +Driver.SizeDetection = SizeDetectionMode.AnsiQuery; ``` #### Content Buffer diff --git a/docfx/docs/index.md b/docfx/docs/index.md index 2b75a6b2ff..f9ddc0784a 100644 --- a/docfx/docs/index.md +++ b/docfx/docs/index.md @@ -12,6 +12,7 @@ Welcome to the Terminal.Gui documentation! This comprehensive guide covers every - [Getting Started](~/docs/getting-started.md) - Quick start guide to create your first Terminal.Gui application - [Migrating from v1 to v2](~/docs/migratingfromv1.md) - Complete guide for upgrading existing applications - [What's New in v2](~/docs/newinv2.md) - Overview of new features and improvements +- [2.5.0 Breaking Changes](~/docs/breaking-changes-2.5.0.md) - `View.Text` CWP, `IAcceptTarget` namespace, `ConfigurationManager` removal, nested `config.json` - [Showcase](~/docs/showcase.md) - Showcase of TUI apps built with Terminal.Gui - [Lexicon & Taxonomy](~/docs/lexicon.md) - Terminology and concepts used throughout Terminal.Gui - [Views](~/docs/views.md) - Comprehensive list of built-in views and controls diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index d6da717ceb..5fb7cf9fca 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -11,7 +11,7 @@ Tenets higher in the list have precedence over tenets lower in the list. -* **Users Have Control** - *Terminal.Gui* provides default key bindings consistent with these tenets, but those defaults are configurable by the user. For example, allows users to redefine key bindings for the system, a user, or an application. +* **Users Have Control** - *Terminal.Gui* provides default key bindings consistent with these tenets, but those defaults are configurable by the user. For example, allows users to redefine key bindings for the system, a user, or an application. * **More Editor than Command Line** - Once a *Terminal.Gui* app starts, the user is no longer using the command line. Users expect keyboard idioms in TUI apps to be consistent with GUI apps (such as VS Code, Vim, and Emacs). For example, in almost all GUI apps, `Ctrl+V` is `Paste`. But the Linux shells often use `Shift+Insert`. *Terminal.Gui* binds `Ctrl+V` by default. @@ -393,17 +393,17 @@ injector.InjectKey(Key.F1, options); ## Configurable Key Bindings -Terminal.Gui uses a layered, platform-aware key binding architecture. All default key bindings are defined declaratively using `PlatformKeyBinding` records and can be overridden via [ConfigurationManager](config.md). +Terminal.Gui uses a layered, platform-aware key binding architecture. All default key bindings are defined declaratively using `PlatformKeyBinding` records and can be overridden via [TuiConfigurationBuilder](config.md). ### Three Layers Key bindings are organized in three layers, applied from lowest to highest priority: -1. **`Application.DefaultKeyBindings`** - Application-wide bindings for commands like Quit, Suspend, Arrange, and tab navigation. This is a `[ConfigurationProperty]` and can be overridden via configuration. +1. **`Application.DefaultKeyBindings`** - Application-wide bindings for commands like Quit, Suspend, Arrange, and tab navigation. Overridable in nested JSON under `Application.DefaultKeyBindings`. -2. **`View.DefaultKeyBindings`** - Shared base bindings for all views, covering navigation (cursor keys, Home, End), clipboard (Copy, Cut, Paste), and editing (Undo, Redo, Delete). This is also a `[ConfigurationProperty]`. +2. **`View.DefaultKeyBindings`** - Shared base bindings for all views, covering navigation (cursor keys, Home, End), clipboard (Copy, Cut, Paste), and editing (Undo, Redo, Delete). Overridable under `View.DefaultKeyBindings`. -3. **Per-view `DefaultKeyBindings`** - View-specific bindings that layer on top of the base. For example, `TextField.DefaultKeyBindings` adds Emacs-style navigation (`Ctrl+B`, `Ctrl+F`), word movement (`Ctrl+CursorLeft`), and kill commands (`Ctrl+K`). These are plain static properties, not configurable via ConfigurationManager. +3. **Per-view `DefaultKeyBindings`** - View-specific bindings that layer on top of the base. For example, `TextField.DefaultKeyBindings` adds Emacs-style navigation (`Ctrl+B`, `Ctrl+F`), word movement (`Ctrl+CursorLeft`), and kill commands (`Ctrl+K`). Each view's constructor calls `ApplyKeyBindings (View.DefaultKeyBindings, .DefaultKeyBindings)` to combine the layers. Only commands that the view actually supports (via `GetSupportedCommands ()`) are bound. Keys already bound by a lower layer are not overwritten by a higher layer. @@ -436,44 +436,45 @@ The `Bind` helper class provides factory methods: | `Bind.NonWindows (...)` | Keys that apply only on Linux and macOS | | `Bind.Platform (windows, linux, macos)` | Fully platform-specific, no shared keys | -### User Overrides via Configuration +### User overrides via configuration -Users can override key bindings for any view type using `View.ViewKeyBindings` in a configuration file. The outer key is the view type name; the inner dictionary maps command names to `PlatformKeyBinding` objects: +Unmentioned commands keep the hard-coded defaults. JSON entries overlay by command name: ```json { - "View.ViewKeyBindings": { - "TextField": { - "Undo": { "All": ["Ctrl+Z"] }, - "CutToEndOfLine": { "All": ["Ctrl+K"] } + "Application": { + "DefaultKeyBindings": { + "Quit": { "All": ["Esc", "Ctrl+Q"] } + } + }, + "View": { + "DefaultKeyBindings": { + "Undo": { "All": ["Ctrl+Z"], "Linux": ["Ctrl+/"], "Macos": ["Ctrl+/"] } }, - "TextView": { - "Redo": { "All": ["Ctrl+Shift+Z"], "Windows": ["Ctrl+Y"] } + "ViewKeyBindings": { + "TextField": { + "CutToEndOfLine": { "All": ["Ctrl+K"] } + } } } } ``` -`ViewKeyBindings` overrides are applied last (highest priority), after both `View.DefaultKeyBindings` and per-view `DefaultKeyBindings`. - -Application-level defaults can also be overridden: +Or in code before views are constructed: -```json -{ - "Application.DefaultKeyBindings": { - "Quit": { "All": ["Ctrl+Q"] }, - "Suspend": { "Linux": ["Ctrl+Z"], "Macos": ["Ctrl+Z"] } - } -} +```csharp +Application.SetDefaultKeyBinding (Command.Quit, Bind.All (Key.Q.WithCtrl)); ``` +`View.ViewKeyBindings` is applied last (highest priority) after `View.DefaultKeyBindings` and per-view `DefaultKeyBindings`. The outer key is the view type name (`TextField`, `TreeView`, …). + ### Resolution Order When a view is created, key bindings are resolved in this order: 1. `View.DefaultKeyBindings` (base layer - navigation, clipboard, editing) 2. Per-view `DefaultKeyBindings` (e.g., `TextField.DefaultKeyBindings`) -3. `View.ViewKeyBindings` user overrides (from configuration) +3. `View.ViewKeyBindings` user overrides (from configuration and/or code) At each layer, only commands supported by the view are bound, and keys already bound by a previous layer are skipped. This means user overrides take effect because they are applied last, after the default layers have established their bindings. @@ -484,13 +485,13 @@ For debugging keyboard event flow, use the `Trace` class from the `Terminal.Gui. ```csharp using Terminal.Gui.Tracing; -Trace.KeyboardEnabled = true; +Trace.EnabledCategories |= TraceCategory.Keyboard; ``` When enabled, keyboard events are logged via `Logging.Trace` showing the flow from Driver → Application → View. Enable via: -- **Code**: `Trace.KeyboardEnabled = true;` -- **Config**: `"Trace.KeyboardEnabled": true` +- **Code**: `Trace.EnabledCategories |= TraceCategory.Keyboard;` +- **Config**: `{ "Trace": { "EnabledCategories": "Keyboard" } }` - **UICatalog**: Logging menu → Keyboard Trace See [Logging - View Event Tracing](logging.md#view-event-tracing) for more details. diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md index bf4aadc3c4..eb6b84c8b7 100644 --- a/docfx/docs/logging.md +++ b/docfx/docs/logging.md @@ -272,7 +272,9 @@ When tracing is enabled, output automatically goes to `Logging.Trace` via the `L ```json { - "Trace.EnabledCategories": ["Command", "Mouse"] + "Trace": { + "EnabledCategories": "Command,Mouse" + } } ``` @@ -280,16 +282,13 @@ To enable configuration tracing (useful when diagnosing why a property isn't app ```json { - "Trace.EnabledCategories": ["Configuration"] + "Trace": { + "EnabledCategories": "Configuration" + } } ``` -Single values and legacy numeric formats are also supported: - -```json -{ "Trace.EnabledCategories": "Command" } -{ "Trace.EnabledCategories": 6 } -``` +`EnabledCategories` is a flags value: a comma-separated name list (`"Command,Mouse"`) or a single name (`"Configuration"`). **Via UICatalog:** Toggle in **Logging** menu → **Command Trace** / **Mouse Trace** / **Keyboard Trace** diff --git a/docfx/docs/menus.md b/docfx/docs/menus.md index 73bf93d137..1c94f248e7 100644 --- a/docfx/docs/menus.md +++ b/docfx/docs/menus.md @@ -677,16 +677,12 @@ For menus specifically: Menu appearance can be customized via themes: ```csharp -// Set default border style for menus -Menu.DefaultBorderStyle = LineStyle.Single; +// Menu appearance is configured via Settings POCOs (or config.json). +MenuSettings.Current = MenuSettings.Current with { DefaultBorderStyle = LineStyle.Single }; +MenuBarSettings.Current = MenuBarSettings.Current with { DefaultBorderStyle = LineStyle.None }; -// Set default border style for menu bars -MenuBar.DefaultBorderStyle = LineStyle.None; - -// Set default activation key for menu bars +// Activation keys remain settable on the view facades: MenuBar.DefaultKey = Key.F10; - -// Set default activation key for popover menus PopoverMenu.DefaultKey = Key.F10.WithShift; ``` @@ -694,15 +690,17 @@ These can also be configured in `config.json`: ```json { + "MenuBar": { + "DefaultKey": "F10" + }, + "PopoverMenu": { + "DefaultKey": "Shift+F10" + }, "Themes": { "Default": { - "Menu.DefaultBorderStyle": "Single", - "MenuBar.DefaultBorderStyle": "None" + "Menu": { "DefaultBorderStyle": "Single" }, + "MenuBar": { "DefaultBorderStyle": "None" } } - }, - "Settings": { - "MenuBar.DefaultKey": "F10", - "PopoverMenu.DefaultKey": "Shift+F10" } } ``` diff --git a/docfx/docs/migrate-cm-to-mec.md b/docfx/docs/migrate-cm-to-mec.md new file mode 100644 index 0000000000..c6eb3d2259 --- /dev/null +++ b/docfx/docs/migrate-cm-to-mec.md @@ -0,0 +1,31 @@ +# Migrating ConfigurationManager to TuiConfigurationBuilder + +Terminal.Gui 2.5.0 removes the legacy `ConfigurationManager` API that 2.4.17 marked `[Obsolete]`. Configuration is now Microsoft.Extensions.Configuration (MEC) via `TuiConfigurationBuilder`. + +To convert a pre-2.5.0 `config.json`, run: + +```bash +dotnet run --project Tools/MigrateConfig -- ./.tui/config.json ./.tui/config.json +``` + +The tool: + +- Splits top-level dotted keys (`Button.DefaultShadow`) into nested objects (`Button: { DefaultShadow }`) +- Splits `Application.DefaultKeyBindings`, `View.DefaultKeyBindings`, and `View.ViewKeyBindings` into nested `Application` / `View` sections (still applied in 2.5 — overlay by command; unmentioned commands keep hard-coded defaults) +- Collapses `Themes` / `Schemes` arrays of single-key objects into dictionaries +- Treats empty `Themes` / `Schemes` arrays as empty dictionaries (`{}`) + +The library does **not** apply the legacy shape. A `WARN` log names the file and points here so you can convert it. Nested JSON is the supported contract. Point `$schema` at `https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json` (nested MEC, 2.5+). + +## Code + +| 2.4.x (removed) | 2.5.0 | +|---|---| +| `ConfigurationManager.Enable (ConfigLocations.All)` | `new TuiConfigurationBuilder ().ApplyToStaticFacades ()` (already runs at assembly load) | +| `ConfigurationManager.RuntimeConfig = json` | `builder.RuntimeConfig = json; builder.ApplyToStaticFacades ()` (nested JSON only) | +| `ConfigurationManager.Applied += ...` | `ThemeChanges.ThemeChanged += ...` | +| `ThemeManager.Theme = "Dark"` | `builder.ThemeManager.SwitchTheme ("Dark")` | +| `Button.DefaultShadow = ShadowStyles.None` | `ButtonSettings.Current = ButtonSettings.Current with { DefaultShadow = ShadowStyles.None }` | +| `[ConfigurationProperty]` | Settings POCO property + `config.json` section | + +See [Configuration](config.md) for the full contract. diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index ff3032d7b8..ef4a1c84b6 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -8,6 +8,7 @@ For detailed breaking change documentation, check out this Discussion: https://g - [Overview of Major Changes](#overview-of-major-changes) - [Application Architecture](#application-architecture) +- [Configuration](#configuration) - [View Construction and Initialization](#view-construction-and-initialization) - [Layout System Changes](#layout-system-changes) - [Color and Attribute Changes](#color-and-attribute-changes) @@ -166,6 +167,16 @@ public class MyView : View --- +## Configuration + +v1 exposed `ConfigurationManager` (typically `ConfigurationManager.Enable`). That API is gone in 2.5.0. + +To load themes and settings in v2, use and Settings POCOs. Do not call `ConfigurationManager.Enable`. + +To convert a pre-2.5.0 `config.json`, see [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md). For the other 2.5.0 API breaks, see [2.5.0 Breaking Changes](breaking-changes-2.5.0.md). + +--- + ## View Construction and Initialization ### Constructors → Initializers @@ -1093,6 +1104,9 @@ While migration requires some effort, the result is a more robust, performant, a For more details, see: - [Application Deep Dive](application.md) +- [Configuration Deep Dive](config.md) +- [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md) +- [2.5.0 Breaking Changes](breaking-changes-2.5.0.md) - [Keyboard Deep Dive](keyboard.md) - [Mouse Deep Dive](mouse.md) - [Layout Deep Dive](layout.md) diff --git a/docfx/docs/mouse.md b/docfx/docs/mouse.md index 49418feddd..dcd8c6b9ff 100644 --- a/docfx/docs/mouse.md +++ b/docfx/docs/mouse.md @@ -870,13 +870,13 @@ For debugging mouse event flow, use the `Trace` class from the `Terminal.Gui.Tra ```csharp using Terminal.Gui.Tracing; -Trace.MouseEnabled = true; +Trace.EnabledCategories |= TraceCategory.Mouse; ``` When enabled, mouse events are logged via `Logging.Trace` showing the flow from Driver → Application → View. Enable via: -- **Code**: `Trace.MouseEnabled = true;` -- **Config**: `"Trace.MouseEnabled": true` +- **Code**: `Trace.EnabledCategories |= TraceCategory.Mouse;` +- **Config**: `{ "Trace": { "EnabledCategories": "Mouse" } }` - **UICatalog**: Logging menu → Mouse Trace See [Logging - View Event Tracing](logging.md#view-event-tracing) for more details. diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index adf79660ec..14952dc8bf 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -4,6 +4,8 @@ This document provides an in-depth overview of the new features, improvements, a **For migration guidance**, see the [v1 To v2 Migration Guide](migratingfromv1.md). +For the 2.5.0 API breaks (`View.Text`, `IAcceptTarget`, `ConfigurationManager`, nested `config.json`), see [2.5.0 Breaking Changes](breaking-changes-2.5.0.md). + ## Table of Contents - [Overview](#overview) @@ -316,14 +318,15 @@ See the [Configuration Deep Dive](config.md) and [Scheme Deep Dive](scheme.md) f v2 adds comprehensive theme support: -- **ConfigurationManager**: Loads/saves color schemes from files +- **TuiConfigurationBuilder**: Loads themes and settings from nested JSON files - **Schemes**: Applied per-view or globally via [Scheme](~/api/Terminal.Gui.Drawing.Scheme.yml) - **Text Styles**: [TextStyle](~/api/Terminal.Gui.Drawing.TextStyle.yml) supports Bold, Italic, Underline, Strikethrough, Blink, Reverse, Faint - **User Customization**: End-users can personalize without code changes ```csharp // Apply a theme -ConfigurationManager.Themes.Theme = "Dark"; +TuiConfigurationBuilder builder = new (); +builder.ThemeManager.SwitchTheme ("Dark"); // Customize text style view.Scheme.Normal = new ( @@ -639,28 +642,24 @@ See the [Mouse Deep Dive](mouse.md) for complete details. See the [Configuration Deep Dive](config.md) for complete details. -### ConfigurationManager +### TuiConfigurationBuilder -[ConfigurationManager](~/api/Terminal.Gui.Configuration.ConfigurationManager.yml) provides: + provides: -- JSON-based persistence -- Theme management -- Key binding customization -- View property persistence -- [SettingsScope](~/api/Terminal.Gui.Configuration.SettingsScope.yml) - User, Application, Machine levels -- [ConfigLocations](~/api/Terminal.Gui.Configuration.ConfigLocations.yml) - Where to search for configs +- JSON-based persistence via Microsoft.Extensions.Configuration +- Theme management (`IThemeManager.SwitchTheme`) +- Key binding and view-default overlays +- App-specific Settings POCOs (`BindAppSettings`) ```csharp -// Enable configuration -ConfigurationManager.Enable (ConfigLocations.All); - -// Load a theme -ConfigurationManager.Themes.Theme = "Dark"; - -// Save current configuration -ConfigurationManager.Save (); +TuiConfigurationBuilder builder = new ("MyApp"); +builder.RuntimeConfig = """{ "Theme": "Dark" }"""; +builder.ApplyToStaticFacades (); +builder.ThemeManager.SwitchTheme ("Dark"); ``` +See [Migrating ConfigurationManager to TuiConfigurationBuilder](migrate-cm-to-mec.md) if you still have 2.4.x `ConfigurationManager` calls or a flat-key `config.json`. See [2.5.0 Breaking Changes](breaking-changes-2.5.0.md) for the other 2.5.0 API breaks. + **User Customization:** - End-users can personalize themes, colors, text styles - Key bindings can be remapped diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml index 84df95b59c..2de7f8ea6e 100644 --- a/docfx/docs/toc.yml +++ b/docfx/docs/toc.yml @@ -7,7 +7,9 @@ - name: What's new in v2 href: newinv2.md - name: v1 To v2 Migration - href: migratingfromv1.md + href: migratingfromv1.md +- name: 2.5.0 Breaking Changes + href: breaking-changes-2.5.0.md - name: Lexicon & Taxonomy href: lexicon.md - name: Views @@ -30,6 +32,8 @@ href: command-diagrams.md - name: Configuration href: config.md +- name: Migrating ConfigurationManager + href: migrate-cm-to-mec.md - name: Cursor href: cursor.md - name: Dim.Auto diff --git a/docfx/includes/config-lexicon.md b/docfx/includes/config-lexicon.md index 2dd848a90a..b235380a28 100644 --- a/docfx/includes/config-lexicon.md +++ b/docfx/includes/config-lexicon.md @@ -1,16 +1,10 @@ | Term | Meaning | |:-----|:--------| -| **AppSettings** | Application-specific settings stored in the application's resources. | -| **Apply** | Apply the configuration to the application; copies settings from configuration properties to corresponding `static` `[ConfigProperty]` properties. | -| **ConfigProperty** | A property decorated with `[ConfigProperty]` that can be configured via the configuration system. | -| **Configuration** | A collection of settings defining application behavior and appearance. | -| **ConfigurationManager** | System that loads and manages application runtime settings from external sources. | -| **Load** | Load configuration from given location(s), updating with new values. Loading doesn't apply settings automatically. | -| **Location** | Storage location for configuration (e.g., user's home directory, application directory). | -| **Reset** | Reset configuration to current values or hard-coded defaults. Does not load configuration. | -| **Scope** | Defines the context where configuration applies (Settings, Theme, or AppSettings). | -| **Settings** | Runtime options including both system settings and application-specific settings. | -| **Sources** | Set of locations where configuration can be stored (@Terminal.Gui.ConfigLocations enum). | -| **Theme** | Named instance containing specific appearance settings. | -| **ThemeInheritance** | Mechanism where themes can inherit and override settings from other themes. | -| **Themes** | Collection of named Theme definitions bundling visual and layout settings. | +| **TuiConfigurationBuilder** | Builds the Microsoft.Extensions.Configuration source chain and applies settings to static facades. | +| **Settings POCO** | Bind target for a nested JSON section (`ButtonSettings`, `GlyphSettings`, `ApplicationSettings`, …). | +| **Theme-scoped settings** | Immutable `*Settings` records with `Current` swapped on theme switch (`ButtonSettings`, `GlyphSettings`, …). | +| **Process-wide settings** | Mutable `*Settings.Defaults` bound once at apply (`ApplicationSettings`, `DriverSettings`, `KeySettings`, `TraceSettings`, …). | +| **Theme** | Named overlay under `Themes:{name}` that property-level-merges onto root sections. | +| **ThemeChanges** | Process-wide `ThemeChanged` observer for views that cannot take `IThemeManager`. | +| **Sources** | MEC provider chain: hard-coded POCO defaults, library `config.json`, app `config.json`, `~/.tui` / `./.tui` files, `TUI_CONFIG`, `RuntimeConfig`. | +| **RuntimeConfig** | Highest-priority in-memory JSON string on `TuiConfigurationBuilder`. Nested MEC shape only. | diff --git a/docfx/includes/scheme-overview.md b/docfx/includes/scheme-overview.md index b51e26e8f4..20cfbf871b 100644 --- a/docfx/includes/scheme-overview.md +++ b/docfx/includes/scheme-overview.md @@ -16,4 +16,4 @@ A Scheme defines how Views look based on their semantic purpose. The following s Scheme dialogScheme = SchemeManager.GetScheme (Schemes.Dialog); ``` -[ConfigurationManager](~/docs/config.md) can be used to override the default values for these schemes and add additional schemes. +[TuiConfigurationBuilder](~/docs/config.md) can be used to override the default values for these schemes and add additional schemes. diff --git a/docfx/schemas/tui-config-schema.json b/docfx/schemas/tui-config-schema.json index 46b5c06177..6da718854e 100644 --- a/docfx/schemas/tui-config-schema.json +++ b/docfx/schemas/tui-config-schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Terminal.Gui Configuration", - "description": "Configuration settings for Terminal.Gui applications. This schema defines settings for the application behavior, appearance, themes, key bindings, and component-specific options.", + "description": "Nested MEC configuration for Terminal.Gui 2.5+. Sections match Settings POCOs (Application, Button, Glyphs, …). Themes is a dictionary of overlays. Convert pre-2.5 flat-key files with Tools/MigrateConfig.", "type": "object", "properties": { "$schema": { @@ -9,205 +9,403 @@ "format": "uri", "description": "The URI of the schema that this configuration file adheres to. Optional but recommended for validation." }, - "ConfigurationManager.ThrowOnJsonErrors": { - "type": "boolean", - "default": false, - "description": "If true, JSON parsing errors encountered during configuration loading will throw exceptions. If false, errors are typically logged, and default values may be used." - }, - "Key.Separator": { + "Theme": { "type": "string", - "default": "+", - "description": "The character used to separate modifiers and keys in key string representations (e.g., 'Ctrl+Q')." + "default": "Default", + "description": "Name of the theme applied at startup. Must match a key in Themes." }, - "Application.DefaultKeyBindings": { + "Application": { "$ref": "#/definitions/applicationSettings" }, + "View": { "$ref": "#/definitions/viewSettings" }, + "Driver": { "$ref": "#/definitions/driverSettings" }, + "Key": { "$ref": "#/definitions/keySettings" }, + "FileDialog": { "$ref": "#/definitions/fileDialogSettings" }, + "FileDialogStyle": { "$ref": "#/definitions/fileDialogStyleSettings" }, + "Trace": { "$ref": "#/definitions/traceSettings" }, + "Button": { "$ref": "#/definitions/buttonSettings" }, + "CheckBox": { "$ref": "#/definitions/checkBoxSettings" }, + "CharMap": { "$ref": "#/definitions/charMapSettings" }, + "Dialog": { "$ref": "#/definitions/dialogSettings" }, + "FrameView": { "$ref": "#/definitions/frameViewSettings" }, + "HexView": { "$ref": "#/definitions/hexViewSettings" }, + "LinearRange": { "$ref": "#/definitions/linearRangeSettings" }, + "Menu": { "$ref": "#/definitions/menuSettings" }, + "MenuBar": { "$ref": "#/definitions/menuBarSettings" }, + "MessageBox": { "$ref": "#/definitions/messageBoxSettings" }, + "NerdFonts": { "$ref": "#/definitions/nerdFontsSettings" }, + "PopoverMenu": { "$ref": "#/definitions/popoverMenuSettings" }, + "SelectorBase": { "$ref": "#/definitions/selectorBaseSettings" }, + "StatusBar": { "$ref": "#/definitions/statusBarSettings" }, + "TextField": { "$ref": "#/definitions/textFieldSettings" }, + "TextView": { "$ref": "#/definitions/textViewSettings" }, + "Window": { "$ref": "#/definitions/windowSettings" }, + "Glyphs": { "$ref": "#/definitions/glyphSettings" }, + "Themes": { "type": "object", - "description": "A dictionary mapping Command names to PlatformKeyBinding objects. Each PlatformKeyBinding can specify keys for All platforms or per-platform (Windows, Linux, Macos). Commands include Quit, Arrange, NextTabStop, PreviousTabStop, NextTabGroup, PreviousTabGroup.", + "description": "Dictionary of theme name to overlay. Each overlay uses the same nested section names as the root (Button, Glyphs, Dialog, Schemes, …). Properties omitted from an overlay keep the root value.", "additionalProperties": { - "type": "object", - "properties": { - "All": { - "type": "array", - "items": { "type": "string" }, - "description": "Key combinations applied on all platforms." - }, - "Windows": { - "type": "array", - "items": { "type": "string" }, - "description": "Key combinations applied only on Windows." - }, - "Linux": { - "type": "array", - "items": { "type": "string" }, - "description": "Key combinations applied only on Linux." - }, - "Macos": { - "type": "array", - "items": { "type": "string" }, - "description": "Key combinations applied only on macOS." - } - } + "$ref": "#/definitions/themeSettings" } + } + }, + "additionalProperties": true, + "definitions": { + "lineStyle": { + "type": "string", + "enum": [ + "None", + "Single", + "Dashed", + "Dotted", + "Double", + "Heavy", + "HeavyDashed", + "HeavyDotted", + "Rounded", + "RoundedDashed", + "RoundedDotted" + ] }, - "Application.Force16Colors": { - "type": "boolean", - "default": false, - "description": "If true, forces the console driver to output only the basic 16 ANSI colors, even if the terminal supports more (e.g., 256 colors or TrueColor)." + "shadowStyles": { + "type": "string", + "enum": [ "None", "Opaque", "Transparent" ] + }, + "alignment": { + "type": "string", + "enum": [ "Start", "End", "Center", "Fill" ] + }, + "alignmentModes": { + "type": "string", + "description": "Comma-separated AlignmentModes flags: StartToEnd, EndToStart, AddSpaceBetweenItems." }, - "Application.ForceDriver": { - "type": [ - "string", - "null" - ], + "cursorStyle": { + "type": "string", "enum": [ - "", - "ansi", - "dotent", - "unix", - "windows", - null - ], - "default": "", - "description": "Forces the use of a specific console driver. If empty or null, Terminal.Gui will attempt to auto-detect the best driver. Options: 'ansi', 'unix', 'dotnet', 'windows'." - }, - "Application.IsMouseDisabled": { - "type": "boolean", - "default": false, - "description": "Disables or enables mouse support for the application. If true, mouse events will be ignored." - }, - "PopoverMenu.DefaultKey": { + "Default", + "BlinkingBlock", + "SteadyBlock", + "BlinkingUnderline", + "SteadyUnderline", + "BlinkingBar", + "SteadyBar", + "Hidden" + ] + }, + "mouseState": { "type": "string", - "default": "Shift+F10", - "description": "Default key to open the context menu (PopoverMenu)." + "description": "Comma-separated MouseState flags: None, In, Pressed, PressedOutside." }, - "FileDialog.MaxSearchResults": { - "type": "integer", - "default": 10000, - "description": "Maximum number of search results to display in the FileDialog." + "applicationSettings": { + "type": "object", + "description": "Application-level settings (SettingsScope).", + "properties": { + "AppModel": { + "type": "string", + "enum": [ "FullScreen", "Inline" ], + "default": "FullScreen", + "description": "FullScreen uses the alternate screen buffer; Inline renders in the primary buffer." + }, + "ForceDriver": { + "type": [ "string", "null" ], + "enum": [ "", "ansi", "dotnet", "unix", "windows", null ], + "default": "", + "description": "Forces a console driver. Empty or null auto-detects." + }, + "IsMouseDisabled": { + "type": "boolean", + "default": false, + "description": "When true, mouse events are ignored." + }, + "DefaultKeyBindings": { + "$ref": "#/definitions/commandKeyBindings", + "description": "Overlays Application.DefaultKeyBindings. Unmentioned commands keep hard-coded defaults." + } + }, + "additionalProperties": false }, - "FileDialogStyle.DefaultUseColors": { - "type": "boolean", - "default": false, - "description": "Whether the FileDialog should use colors by default to differentiate file/directory types." + "viewSettings": { + "type": "object", + "description": "Shared view key-binding overlays (process-wide).", + "properties": { + "DefaultKeyBindings": { + "$ref": "#/definitions/commandKeyBindings", + "description": "Overlays View.DefaultKeyBindings. Unmentioned commands keep hard-coded defaults." + }, + "ViewKeyBindings": { + "type": "object", + "description": "Per-view-type overlays. Outer key is the type name (e.g. TextField). Applied last in ApplyKeyBindings.", + "additionalProperties": { + "$ref": "#/definitions/commandKeyBindings" + } + } + }, + "additionalProperties": false }, - "FileDialogStyle.DefaultUseUnicodeCharacters": { - "type": "boolean", - "default": false, - "description": "Whether the FileDialog should use Unicode characters for icons by default." + "commandKeyBindings": { + "type": "object", + "description": "Map of Command name to PlatformKeyBinding.", + "additionalProperties": { + "$ref": "#/definitions/platformKeyBinding" + } }, - "Colors16": { + "platformKeyBinding": { "type": "object", - "description": "Defines the 3-byte/6-character hexadecimal string values for the 16 legacy ANSI color names (ColorName16). These definitions can be overridden by the user to customize the base 16 color palette.", + "description": "Keys for one command. All applies on every platform; Windows/Linux/Macos are additive.", "properties": { - "Black": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Black (typically #000000)." - }, - "Blue": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Blue (typically #000080, a dark blue like Navy)." - }, - "Green": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Green (typically #008000, a dark green)." - }, - "Cyan": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Cyan (typically #008080, a dark cyan like Teal)." - }, - "Red": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Red (typically #800000, a dark red like Maroon)." - }, - "Magenta": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Magenta (typically #800080, a dark magenta like Purple)." - }, - "Yellow": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Yellow (typically #808000, a dark yellow like Olive)." - }, - "Gray": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.Gray (typically #C0C0C0, a light gray like Silver)." - }, - "DarkGray": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.DarkGray (typically #808080, a medium gray)." - }, - "BrightBlue": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightBlue (typically #0000FF)." - }, - "BrightGreen": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightGreen (typically #00FF00, like Lime)." + "All": { "type": "array", "items": { "type": "string" } }, + "Windows": { "type": "array", "items": { "type": "string" } }, + "Linux": { "type": "array", "items": { "type": "string" } }, + "Macos": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + }, + "driverSettings": { + "type": "object", + "description": "Driver settings (SettingsScope).", + "properties": { + "Force16Colors": { + "type": "boolean", + "default": false, + "description": "When true, the driver outputs only the 16 ANSI colors." }, - "BrightCyan": { + "SizeDetection": { "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightCyan (typically #00FFFF, like Aqua)." - }, - "BrightRed": { + "enum": [ "AnsiQuery", "Polling" ], + "default": "Polling", + "description": "How the ANSI driver detects terminal size." + } + }, + "additionalProperties": false + }, + "keySettings": { + "type": "object", + "description": "Key parsing settings (SettingsScope).", + "properties": { + "Separator": { "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightRed (typically #FF0000)." - }, - "BrightMagenta": { + "minLength": 1, + "default": "+", + "description": "Character that separates modifiers and keys (e.g. Ctrl+Q)." + } + }, + "additionalProperties": false + }, + "fileDialogSettings": { + "type": "object", + "properties": { + "MaxSearchResults": { + "type": "integer", + "default": 10000, + "description": "Maximum search results shown in FileDialog." + } + }, + "additionalProperties": false + }, + "fileDialogStyleSettings": { + "type": "object", + "properties": { + "DefaultUseColors": { + "type": "boolean", + "default": true, + "description": "Whether FileDialog uses colors to distinguish file types." + }, + "DefaultUseUnicodeCharacters": { + "type": "boolean", + "default": false, + "description": "Whether FileDialog uses Unicode icons by default." + } + }, + "additionalProperties": false + }, + "traceSettings": { + "type": "object", + "properties": { + "EnabledCategories": { "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightMagenta (typically #FF00FF, like Fuchsia)." - }, - "BrightYellow": { + "description": "Comma-separated TraceCategory flags: None, Lifecycle, Command, Mouse, Keyboard, Navigation, Configuration, Draw, All." + } + }, + "additionalProperties": false + }, + "buttonSettings": { + "type": "object", + "properties": { + "DefaultShadow": { "$ref": "#/definitions/shadowStyles", "default": "Opaque" } + }, + "additionalProperties": false + }, + "checkBoxSettings": { + "type": "object", + "properties": { + "DefaultMouseHighlightStates": { "$ref": "#/definitions/mouseState" } + }, + "additionalProperties": false + }, + "charMapSettings": { + "type": "object", + "properties": { + "DefaultCursorStyle": { "$ref": "#/definitions/cursorStyle", "default": "BlinkingBlock" } + }, + "additionalProperties": false + }, + "dialogSettings": { + "type": "object", + "properties": { + "DefaultShadow": { "$ref": "#/definitions/shadowStyles", "default": "Transparent" }, + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "Heavy" }, + "DefaultButtonAlignment": { "$ref": "#/definitions/alignment", "default": "End" }, + "DefaultButtonAlignmentModes": { "$ref": "#/definitions/alignmentModes" } + }, + "additionalProperties": false + }, + "frameViewSettings": { + "type": "object", + "properties": { + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "Rounded" } + }, + "additionalProperties": false + }, + "hexViewSettings": { + "type": "object", + "properties": { + "DefaultCursorStyle": { "$ref": "#/definitions/cursorStyle", "default": "BlinkingBlock" } + }, + "additionalProperties": false + }, + "linearRangeSettings": { + "type": "object", + "properties": { + "DefaultCursorStyle": { "$ref": "#/definitions/cursorStyle", "default": "BlinkingBlock" } + }, + "additionalProperties": false + }, + "menuSettings": { + "type": "object", + "properties": { + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "None" } + }, + "additionalProperties": false + }, + "menuBarSettings": { + "type": "object", + "properties": { + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "None" }, + "DefaultKey": { "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.BrightYellow (typically #FFFF00)." - }, - "White": { + "default": "F10", + "description": "Default key that opens the MenuBar." + } + }, + "additionalProperties": false + }, + "messageBoxSettings": { + "type": "object", + "properties": { + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "Heavy" }, + "DefaultButtonAlignment": { "$ref": "#/definitions/alignment", "default": "Center" } + }, + "additionalProperties": false + }, + "nerdFontsSettings": { + "type": "object", + "properties": { + "Enable": { + "type": "boolean", + "default": false, + "description": "When true, Nerd Font glyphs are used where available." + } + }, + "additionalProperties": false + }, + "popoverMenuSettings": { + "type": "object", + "properties": { + "DefaultKey": { "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$", - "description": "Hex string for ColorName16.White (typically #FFFFFF)." + "default": "Shift+F10", + "description": "Default key that opens a PopoverMenu (context menu)." } }, "additionalProperties": false }, - "Theme": { - "type": "string", - "default": "Default", - "description": "The name of the theme to be applied at application startup. This name must match one of the theme names defined in the 'Themes' array." + "selectorBaseSettings": { + "type": "object", + "properties": { + "DefaultMouseHighlightStates": { "$ref": "#/definitions/mouseState" } + }, + "additionalProperties": false }, - "Themes": { - "type": "array", - "description": "A list of theme definitions. Each theme provides a distinct look and feel for the application.", - "items": { - "type": "object", - "description": "A single theme definition. The object should have exactly one property, where the key is the unique name of the theme (e.g., 'Default', 'Dark'), and the value contains the theme's settings.", - "maxProperties": 1, - "minProperties": 1, - "patternProperties": { - "^[\\w\\s&\\(\\)-]+$": { - "$ref": "#/definitions/themeSettings" - } - }, - "additionalProperties": false + "statusBarSettings": { + "type": "object", + "properties": { + "DefaultSeparatorLineStyle": { "$ref": "#/definitions/lineStyle", "default": "Single" } + }, + "additionalProperties": false + }, + "textFieldSettings": { + "type": "object", + "properties": { + "DefaultCursorStyle": { "$ref": "#/definitions/cursorStyle", "default": "BlinkingBar" } + }, + "additionalProperties": false + }, + "textViewSettings": { + "type": "object", + "properties": { + "DefaultCursorStyle": { "$ref": "#/definitions/cursorStyle", "default": "BlinkingBar" } + }, + "additionalProperties": false + }, + "windowSettings": { + "type": "object", + "properties": { + "DefaultShadow": { "$ref": "#/definitions/shadowStyles", "default": "None" }, + "DefaultBorderStyle": { "$ref": "#/definitions/lineStyle", "default": "Single" } + }, + "additionalProperties": false + }, + "glyphSettings": { + "type": "object", + "description": "Glyph characters used to draw UI chrome. Keys match GlyphSettings property names (LeftArrow, HLine, CheckStateChecked, …).", + "additionalProperties": { + "type": "string", + "minLength": 1, + "description": "A single glyph (Unicode character or grapheme)." } - } - }, - "additionalProperties": true, - "definitions": { + }, + "themeSettings": { + "type": "object", + "description": "Overlay for one named theme. Nested sections match root ThemeScope POCOs. Omitted properties keep the root value.", + "properties": { + "Button": { "$ref": "#/definitions/buttonSettings" }, + "CheckBox": { "$ref": "#/definitions/checkBoxSettings" }, + "CharMap": { "$ref": "#/definitions/charMapSettings" }, + "Dialog": { "$ref": "#/definitions/dialogSettings" }, + "FrameView": { "$ref": "#/definitions/frameViewSettings" }, + "HexView": { "$ref": "#/definitions/hexViewSettings" }, + "LinearRange": { "$ref": "#/definitions/linearRangeSettings" }, + "Menu": { "$ref": "#/definitions/menuSettings" }, + "MenuBar": { "$ref": "#/definitions/menuBarSettings" }, + "MessageBox": { "$ref": "#/definitions/messageBoxSettings" }, + "NerdFonts": { "$ref": "#/definitions/nerdFontsSettings" }, + "PopoverMenu": { "$ref": "#/definitions/popoverMenuSettings" }, + "SelectorBase": { "$ref": "#/definitions/selectorBaseSettings" }, + "StatusBar": { "$ref": "#/definitions/statusBarSettings" }, + "TextField": { "$ref": "#/definitions/textFieldSettings" }, + "TextView": { "$ref": "#/definitions/textViewSettings" }, + "Window": { "$ref": "#/definitions/windowSettings" }, + "Glyphs": { "$ref": "#/definitions/glyphSettings" }, + "Schemes": { + "type": "object", + "description": "Named schemes for this theme (Base, Menu, Error, …).", + "additionalProperties": { + "$ref": "#/definitions/scheme" + } + } + }, + "additionalProperties": true + }, "Color": { - "description": "One of the standard color names (parsed case-insensitively; (e.g. 'BrightBlue'), an rgb(r,g,b) tuple, or a hex color string in the format #RRGGBB.", - "$schema": "http://json-schema.org/draft-07/schema#", + "description": "One of the standard color names (parsed case-insensitively; e.g. 'BrightBlue'), an rgb(r,g,b) tuple, or a hex color string in the format #RRGGBB.", "type": "string", "oneOf": [ { @@ -388,283 +586,50 @@ }, "attribute": { "type": "object", - "description": "Defines the foreground color, background color, and style for a specific UI element state.", + "description": "Foreground color, background color, and style for a UI element state.", "properties": { - "Foreground": { - "$ref": "#/definitions/Color" - }, - "Background": { - "$ref": "#/definitions/Color" - }, + "Foreground": { "$ref": "#/definitions/Color" }, + "Background": { "$ref": "#/definitions/Color" }, "Style": { "type": "string", "default": "None", - "description": "Text style. Parsed case-insensitively by the application. Can be a single style or a comma-separated list of the following valid flags: None, Bold, Italic, Underline, Faint, Blink, Reverse, Strikethrough (e.g., 'Bold,Italic').", + "description": "Text style. A single flag or a comma-separated list: None, Bold, Italic, Underline, Faint, Blink, Reverse, Strikethrough.", "pattern": "^(None|Bold|Italic|Underline|Faint|Blink|Reverse|Strikethrough)(\\s*,\\s*(None|Bold|Italic|Underline|Faint|Blink|Reverse|Strikethrough))*$" } }, - "required": [ - "Foreground", - "Background" - ], + "required": [ "Foreground", "Background" ], "additionalProperties": false }, "scheme": { "type": "object", - "description": "A collection of attributes defining the appearance of UI elements in different states (normal, focused, etc.).", + "description": "Attributes for UI elements in different visual roles.", "properties": { - "Normal": { - "$ref": "#/definitions/attribute", - "description": "Appearance in the normal, non-focused state." - }, - "Focus": { - "$ref": "#/definitions/attribute", - "description": "Appearance when the element has input focus." - }, - "HotNormal": { - "$ref": "#/definitions/attribute", - "description": "Appearance for a 'hot' element (e.g., mouse hover) that is not focused." - }, - "HotFocus": { - "$ref": "#/definitions/attribute", - "description": "Appearance for a 'hot' element that also has input focus." - }, - "Disabled": { - "$ref": "#/definitions/attribute", - "description": "Appearance when the element is disabled." - }, - "Active": { - "$ref": "#/definitions/attribute", - "description": "Appearance when the element is active." - }, - "HotActive": { - "$ref": "#/definitions/attribute", - "description": "Appearance for a 'hot' element that is also active." - }, - "Highlight": { - "$ref": "#/definitions/attribute", - "description": "Appearance for a highlighted element." - }, - "Editable": { - "$ref": "#/definitions/attribute", - "description": "Appearance for an editable element." - }, - "ReadOnly": { - "$ref": "#/definitions/attribute", - "description": "Appearance for a read-only element." - }, - "Code": { - "$ref": "#/definitions/attribute", - "description": "Appearance for preformatted or source code content." - }, - "CodeComment": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code comments." - }, - "CodeKeyword": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code keywords." - }, - "CodeString": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code string literals." - }, - "CodeNumber": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code numeric literals." - }, - "CodeOperator": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code operators." - }, - "CodeType": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code type names." - }, - "CodePreprocessor": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code preprocessor directives." - }, - "CodeIdentifier": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code identifiers." - }, - "CodeConstant": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code constants." - }, - "CodePunctuation": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code punctuation." - }, - "CodeFunctionName": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code function names." - }, - "CodeAttribute": { - "$ref": "#/definitions/attribute", - "description": "Appearance for source-code attributes." - } + "Normal": { "$ref": "#/definitions/attribute", "description": "Normal, non-focused state." }, + "Focus": { "$ref": "#/definitions/attribute", "description": "Has input focus." }, + "HotNormal": { "$ref": "#/definitions/attribute", "description": "Hot (e.g. hover) and not focused." }, + "HotFocus": { "$ref": "#/definitions/attribute", "description": "Hot and focused." }, + "Disabled": { "$ref": "#/definitions/attribute", "description": "Disabled." }, + "Active": { "$ref": "#/definitions/attribute", "description": "Active." }, + "HotActive": { "$ref": "#/definitions/attribute", "description": "Hot and active." }, + "Highlight": { "$ref": "#/definitions/attribute", "description": "Highlighted." }, + "Editable": { "$ref": "#/definitions/attribute", "description": "Editable." }, + "ReadOnly": { "$ref": "#/definitions/attribute", "description": "Read-only." }, + "Code": { "$ref": "#/definitions/attribute", "description": "Preformatted or source code." }, + "CodeComment": { "$ref": "#/definitions/attribute", "description": "Source-code comments." }, + "CodeKeyword": { "$ref": "#/definitions/attribute", "description": "Source-code keywords." }, + "CodeString": { "$ref": "#/definitions/attribute", "description": "Source-code string literals." }, + "CodeNumber": { "$ref": "#/definitions/attribute", "description": "Source-code numeric literals." }, + "CodeOperator": { "$ref": "#/definitions/attribute", "description": "Source-code operators." }, + "CodeType": { "$ref": "#/definitions/attribute", "description": "Source-code type names." }, + "CodePreprocessor": { "$ref": "#/definitions/attribute", "description": "Source-code preprocessor directives." }, + "CodeIdentifier": { "$ref": "#/definitions/attribute", "description": "Source-code identifiers." }, + "CodeConstant": { "$ref": "#/definitions/attribute", "description": "Source-code constants." }, + "CodePunctuation": { "$ref": "#/definitions/attribute", "description": "Source-code punctuation." }, + "CodeFunctionName": { "$ref": "#/definitions/attribute", "description": "Source-code function names." }, + "CodeAttribute": { "$ref": "#/definitions/attribute", "description": "Source-code attributes." } }, - "required": [ - "Normal" - ], + "required": [ "Normal" ], "additionalProperties": false - }, - "themeSettings": { - "type": "object", - "description": "Contains all settings for a specific theme, including default styles for various UI elements, glyphs, and color schemes.", - "properties": { - "BasedOn": { - "type": "string", - "description": "The name of another theme that this theme inherits settings from. Settings in the current theme will override those from the base theme." - }, - "Dialog.DefaultButtonAlignment": { - "type": "string", - "enum": [ - "Start", - "End", - "Center", - "Fill" - ], - "description": "Default horizontal alignment for buttons within Dialog views." - }, - "Dialog.DefaultButtonAlignmentModes": { - "type": "string", - "enum": [ - "None", - "AddSpaceBetweenItems", - "AddSpaceAroundItems" - ], - "description": "Specifies how extra space is distributed between buttons in Dialog views when alignment is 'Start', 'End', or 'Center'." - }, - "Dialog.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for Dialog views." - }, - "Dialog.DefaultShadow": { - "type": "string", - "enum": [ - "None", - "Transparent", - "Opaque" - ], - "description": "Default shadow style for Dialog views, rendered behind the dialog." - }, - "FrameView.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for FrameView controls." - }, - "Window.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for Window views." - }, - "MessageBox.DefaultButtonAlignment": { - "type": "string", - "enum": [ - "Start", - "End", - "Center", - "Fill" - ], - "description": "Default horizontal alignment for buttons within MessageBox views." - }, - "MessageBox.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for MessageBox views." - }, - "Button.DefaultShadow": { - "type": "string", - "enum": [ - "None", - "Transparent", - "Opaque" - ], - "description": "Default shadow style for Button controls, often used for 3D effect." - }, - "Menu.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for the newer Menu control and its sub-menus." - }, - "MenuBar.DefaultBorderStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy", - "Rounded" - ], - "description": "Default border style for the MenuBar control." - }, - "StatusBar.DefaultSeparatorLineStyle": { - "type": "string", - "enum": [ - "None", - "Single", - "Double", - "Heavy" - ], - "description": "Default style for separator lines in the StatusBar." - }, - "Schemes": { - "type": "array", - "description": "A list of scheme definitions for this theme. Each item in the array is an object containing one or more named schemes (e.g., 'Runnable', 'Base', 'Menu').", - "items": { - "type": "object", - "description": "An object where each key is a scheme name (e.g., 'Base', 'Error') and its value is the scheme definition.", - "patternProperties": { - "^[A-Za-z][A-Za-z0-9_]*$": { - "$ref": "#/definitions/scheme" - } - }, - "additionalProperties": false, - "minProperties": 1 - } - } - }, - "patternProperties": { - "^Glyphs\\.[A-Za-z0-9]+$": { - "type": "string", - "description": "A specific glyph character used by the theme for drawing UI elements like borders, arrows, indicators, etc. (e.g., 'Glyphs.LeftArrow', 'Glyphs.HLine'). The string value is the character to be used." - } - }, - "additionalProperties": true } } } diff --git a/docfx/scripts/OutputView/OutputView.cs b/docfx/scripts/OutputView/OutputView.cs index 7226a237ea..b96c043fe7 100644 --- a/docfx/scripts/OutputView/OutputView.cs +++ b/docfx/scripts/OutputView/OutputView.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Terminal.Gui.App; using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; @@ -8,22 +8,19 @@ using Attribute = Terminal.Gui.Drawing.Attribute; // Disable all shadows and highlights -ConfigurationManager.RuntimeConfig = """ - { - "Themes": [ - { - "Default": { - "Window.DefaultShadow": "None", - "Dialog.DefaultShadow": "None", - "Button.DefaultShadow": "None", - "Menu.DefaultBorderStyle": "Single" - } - } - ] - } - """; - -ConfigurationManager.Enable (ConfigLocations.Runtime); +TuiConfigurationBuilder.Shared.RuntimeConfig = """ +{ + "Themes": { + "Default": { + "Window": { "DefaultShadow": "None" }, + "Dialog": { "DefaultShadow": "None" }, + "Button": { "DefaultShadow": "None" }, + "Menu": { "DefaultBorderStyle": "Single" } + } + } +} +"""; +TuiConfigurationBuilder.Shared.ApplyToStaticFacades (); // Get the view name and output file from commandline string? viewName = null; diff --git a/llms.txt b/llms.txt index 3bd5337663..862d6f87fa 100644 --- a/llms.txt +++ b/llms.txt @@ -27,7 +27,7 @@ | `view.Bounds` | `view.Viewport` | | `LayoutStyle.Computed` | Removed — all layout is declarative via `Pos`/`Dim` | | `new RadioGroup (...)` | `new OptionSelector { ... }` | -| `Colors.ColorSchemes ["name"]` | `Schemes.Resolve ("name")` or use `Scheme` directly | +| `Colors.ColorSchemes ["name"]` | `SchemeManager.GetScheme ("name")` or use `Scheme` directly | | `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | > **Full v1→v2 corrections**: See [ai-v2-primer.md](ai-v2-primer.md) @@ -103,7 +103,7 @@ public sealed class MainWindow : Runnable | `Terminal.Gui.Drawing` | `Color`, `Attribute`, `Scheme`, `LineCanvas`, `Glyphs` | | `Terminal.Gui.Input` | `Key`, `KeyCode`, `Command`, `KeyBindings`, `MouseBindings` | | `Terminal.Gui.Text` | `TextFormatter`, `TextDirection` | -| `Terminal.Gui.Configuration` | `ConfigurationManager`, themes | +| `Terminal.Gui.Configuration` | `TuiConfigurationBuilder`, Settings POCOs, themes | ### Layout System (Pos/Dim) diff --git a/plans/4918-restore-pre-4949-ansi-input-behavior.md b/plans/4918-restore-pre-4949-ansi-input-behavior.md new file mode 100644 index 0000000000..1655841f14 --- /dev/null +++ b/plans/4918-restore-pre-4949-ansi-input-behavior.md @@ -0,0 +1,28 @@ +# Restore Pre-#4949 ANSI Input Behavior + +## Problem Statement + +Windows Terminal previously emitted some Portuguese keyboard input as duplicated legacy characters while Kitty keyboard mode was enabled. Terminal.Gui expanded #4927's mixed Kitty-plus-legacy suppression through the #4949/#4977 raw-fallback workaround, which later evolved into a 50 ms suppression window. Windows Terminal fixed the underlying Kitty keyboard encoding defect, so Terminal.Gui can restore the behavior immediately before #4949 without the timing heuristic. + +## Implementation Steps + +1. Replace the timed ANSI printable-suppression state with the pre-#4949 single pending-printable state. +2. Arm suppression only after an unmodified printable parsed key press, compare only the next fallback key, and never arm suppression from fallback input. +3. Retain the protected input-pipeline clock accessor for compatibility, although suppression no longer uses time. +4. Carry the matching parser pattern through the internal input pipeline so only Kitty CSI-u input can arm suppression; legacy sequences such as Shift+Tab must remain lossless. +5. Require the fallback duplicate to be adjacent in the raw input stream so intervening keyboard, mouse, paste, or response input invalidates suppression. +6. Update pipeline regressions to verify time-independent mixed-input suppression, Portuguese Kitty-plus-legacy input, lossless repeated legacy input, Shift+Tab followed by Tab, and intervening parser input. + +## File Changes + +- `Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs`: remove timed suppression and restore single-use pending suppression. +- `Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs`: expose parser-pattern provenance to the internal input pipeline. +- `Terminal.Gui/Drivers/Input/InputProcessorImpl.cs`: retain the protected clock accessor and route parser-pattern provenance through the parser/fallback hooks. +- `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs`: update and add focused regression coverage. + +## Verification + +1. Run the focused `KittyKeyboardPipelineTests` class. +2. Build the solution in Debug configuration. +3. Run the full parallelizable unit-test project. +4. Run the full non-parallelizable unit-test project. diff --git a/specs/breaking-changes-cm-mec.md b/specs/breaking-changes-cm-mec.md index 24d712a996..b7bea0bd99 100644 --- a/specs/breaking-changes-cm-mec.md +++ b/specs/breaking-changes-cm-mec.md @@ -1,6 +1,6 @@ # Breaking-Change Analysis: CM → MEC Migration (#5411 vs #5416) -> Companion to [replace-cm-with-mec.md](./replace-cm-with-mec.md). +> **Status:** Historical. Companion to [replace-cm-with-mec.md](./replace-cm-with-mec.md). Live contract: [config.md](../docfx/docs/config.md). > Tracks the umbrella issue [#4943](https://github.com/gui-cs/Terminal.Gui/issues/4943). This document records what is **actually** breaking in diff --git a/specs/remove-legacy-cm-prep.md b/specs/remove-legacy-cm-prep.md new file mode 100644 index 0000000000..59dc64dfd1 --- /dev/null +++ b/specs/remove-legacy-cm-prep.md @@ -0,0 +1,225 @@ +# CM Removal — Pre-Work Inventories & Baselines + +> **Status:** Historical inventory used while implementing #5416. Not the live configuration contract. + +Companion artifact for [`remove-legacy-cm.md`](./remove-legacy-cm.md). +Captures the empirical "before" state used by phases **G** (examples sweep), +**E** (test deletion), and **I** (AOT measurement) so the eventual delta +claim is reproducible. + +Generated against base SHA `83ded73aa` (`origin/copilot/replace-cm-with-mec`, +post Phase A1/B/C of #5411). My branch is rebased onto this commit; spec +commit is `f122ebe6a`. + +--- + +## 1. AOT Size Baseline (Phase I) + +Command: `dotnet publish Examples/NativeAot/NativeAot.csproj -c Release -r win-x64` +Platform: Windows x64, .NET 10 SDK, clean `bin/obj` before publish. + +| Artifact | Bytes | MB | +|--------------------------------|--------------|--------| +| `NativeAot.exe` (AOT, single) | 23,873,536 | 22.77 | +| `Terminal.Gui.dll` (Release) | 1,854,464 | 1.77 | +| Publish dir total | 96,319,509 | 91.86 | + +### Reflection-rooting metric + +`Terminal.Gui/Configuration/ConfigPropertyHostTypes.cs` currently roots +**31** types via `[DynamicDependency]`. Deleting this file in Phase E +should drop those 31 root anchors and let the AOT trimmer collect their +unused members. This is the headline number for the "AOT size reduction" +claim in #5416's PR description. + +### Reproducing this number after Phase E + +```powershell +Remove-Item Examples/NativeAot/bin, Examples/NativeAot/obj -Recurse -Force -EA 0 +Remove-Item Terminal.Gui/bin, Terminal.Gui/obj -Recurse -Force -EA 0 +dotnet publish Examples/NativeAot/NativeAot.csproj -c Release -r win-x64 +Get-Item Examples/NativeAot/bin/Release/net10.0/win-x64/publish/NativeAot.exe | + Select-Object Name, Length +``` + +Record the new `NativeAot.exe` byte count and the delta vs. 23,873,536. +A meaningful win should be ≥ 1 MB given the 31 rooted types include +view types with large transitive surfaces (`MenuBar`, `StatusBar`, +`TableView`, `TreeView`, `Dialog`, etc.). + +--- + +## 2. Examples Inventory (Phase G) + +Total `Examples/` files touching CM: **~115 source/doc files**, but +**99% are the same one-line incantation** — the actual rewrite surface +is small. + +### 2.1 Dominant pattern (~105 occurrences, mostly UICatalog scenarios) + +Single call in scenario constructor / `Main`: + +```csharp +ConfigurationManager.Enable (ConfigLocations.All); +``` + +**Replacement:** Replace with MEC equivalent (likely +`Application.Create ().UseTuiConfiguration ().Init ()` or whatever the +final builder shape settles on — see spec §5.6). + +Files using only this pattern (mechanical sed, no review needed): +all `Examples/UICatalog/Scenarios/*.cs` except the four below. + +### 2.2 Non-trivial scenarios needing review + +| File | Lines | What it does | Replacement notes | +|------|-------|--------------|-------------------| +| `Examples/UICatalog/UICatalogRunnable.cs` | 30, 131, 203, 715 | Subscribes to `ConfigurationManager.Applied`; reads `IsEnabled`; uses `[ConfigurationProperty (Scope = typeof (AppSettingsScope), OmitClassName = true)]` on a settings field | Subscribe to `ThemeChanges.ThemeChanged` (already exists post-#5411); move `[ConfigurationProperty]` to a POCO bound via `IOptionsMonitor` | +| `Examples/UICatalog/Runner.cs` | 14, 39, 373, 378, 379 | Sets `RuntimeConfig` JSON string for driver overrides; explicit `Load + Apply`; bridges `ThemeManager.ThemeChanged → CM.Apply` | Build an in-memory `IConfigurationSource` for the driver overrides; drop the bridge (MEC reload handles it) | +| `Examples/UICatalog/Scenarios/ConfigurationEditor.cs` | 23, 51, 75, 222, 227, 232, 247 | The CM editor scenario itself — enumerates `SourcesManager.Sources`, edits `RuntimeConfig`, calls `GetHardCodedConfig`/`GetEmptyConfig` | Rewrite to walk `IConfigurationRoot.Providers` and edit a designated `IConfigurationSource`. **This scenario is the heaviest item in Phase G.** | +| `Examples/UICatalog/Scenarios/Themes.cs` | 89 | `ConfigurationManager.Apply ()` after a theme switch | Drop the call (MEC reload-on-change) | +| `Examples/UICatalog/Scenarios/ThemeFallback.cs` | 73 | Same pattern | Drop the call | +| `Examples/UICatalog/Scenarios/CodeViewDemo.cs` | 153 | Same pattern | Drop the call | +| `Examples/UICatalog/Scenarios/TextInputControls.cs` | 384 | `ConfigurationManager.Applied += ...` | Subscribe to `ThemeChanges.ThemeChanged` | +| `Examples/UICatalog/Scenarios/Shortcuts.cs` | 345 | Reads `ConfigurationManager.IsEnabled` | Drop the check (MEC is always on or behind a builder opt-in) | +| `Examples/UICatalog/UICatalog.cs` | 296 | `Enable` call at app startup | Replace with builder call | +| `Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs` | 57 | `Enable` call | Replace | + +### 2.3 Standalone examples (each has one `Enable` call) + +`Example.cs`, `SelfContained/Program.cs`, `CommunityToolkitExample/Program.cs`, +`NativeAot/Program.cs`, `ShortcutTest.cs`, `ReactiveExample/Program.cs`, +`PromptExample/Program.cs`, `ScenarioRunner/Program.cs` (2 calls). + +`Examples/Example/Example.cs:13` additionally sets `RuntimeConfig` +to a JSON literal — needs the same in-memory-source treatment as +`Runner.cs`. + +### 2.4 Docs / READMEs to update (Phase H) + +`Examples/Config/README.md`, `Examples/CommunityToolkitExample/README.md`, +`Examples/SelfContained/README.md`, `Examples/ReactiveExample/README.md`, +`Examples/NativeAot/README.md` — all reference the legacy `Enable` +incantation. + +`Examples/Config/example_config.json` — uses the legacy flat +`"ConfigurationManager.ThrowOnJsonErrors"` key. Migrate when D-02 +lands. + +### 2.5 AOT-blocker note + +`Examples/NativeAot/README.md` explicitly documents `ConfigurationManager.Initialize`, +`DeepCloner`, `SourceGenerationContext` as the reasons AOT works the way +it does today. After CM removal, this README needs a full rewrite — the +trim warnings should largely disappear. + +--- + +## 3. Test Inventory (Phase E) + +Total Configuration tests across all test projects: **25 files**. + +### 3.1 DELETE — pure CM/scope/Initialize tests (8 files) + +| File | Project | CM refs | +|------|---------|---------| +| `ConfigurationMangerTests.cs` | NonParallelizable | 49 | +| `ConfigPropertyAssemblyScanTests.cs` | NonParallelizable | 9 | +| `SourcesManagerLoadNullJsonTests.cs` | NonParallelizable | 5 | +| `GlyphTests.cs` | NonParallelizable | uses `static CM` import, `RuntimeConfig`, `ThemeManager.GetCurrentTheme()` | +| `SourcesManagerTests.cs` | Parallelizable | 69 | +| `ScopeTests.cs` | Parallelizable | 18 | +| `ScopeJsonConverterTests.cs` | Parallelizable | 5 | +| `SettingsScopeTests.cs` / `ThemeScopeTests.cs` | Parallelizable | 2 / 4 | +| `ConfigPropertyTests.cs` / `ConfigPropertyHostTypesTests.cs` / `ConfigurationPropertyAttributeTests.cs` | Parallelizable | 10 / 10 / 2 | +| `DeepClonerTests.cs` | Parallelizable | 7 | + +(That's 13 files counted, not 8 — the 8/13 split depends on whether +glyph behavior gets re-tested against MEC; see §3.3 *port* row.) + +### 3.2 KEEP — JSON converter tests still valid against `TuiSerializerContext` (8 files) + +These test converter behavior, not CM machinery. The converters +themselves are kept (per spec §4.2) and now wire through +`TuiSerializerContext`. + +- `AttributeJsonConverterTests.cs` +- `ColorJsonConverterTests.cs` +- `ConcurrentDictionaryJsonConverterTests.cs` +- `KeyJsonConverterTests.cs` +- `KeyCodeJsonConverterTests.cs` +- `RuneJsonConverterTests.cs` +- `SchemeJsonConverterTests.cs` *(verify it doesn't depend on `ThemeScope` resolution)* +- `MemorySizeEstimator.cs` *(helper, not a test)* + +### 3.3 KEEP — already MEC-based (3 files) + +- `MecThemeTests.cs` +- `MecSettingsTests.cs` +- `MecAppSettingsTests.cs` +- `SchemeManagerTests.cs` *(verify which manager — likely MEC since legacy `SchemeManager` is `[Obsolete]`)* + +### 3.4 PORT — behaviors that should survive as MEC tests + +The `GlyphTests` "Apply over defaults" behavior is a real +user-observable contract: when you override a glyph in `config.json`, +`Glyphs.LeftBracket` reflects it. After deletion, add an equivalent +test against the MEC pipeline (load JSON → assert `Glyphs.*`). + +Same applies to any `ConfigurationMangerTests` case that asserts +end-to-end behavior (file-load → apply → property-reflects) that isn't +already covered by `MecSettingsTests` / `MecThemeTests`. A line-by-line +audit is required in Phase E — not in this prep doc. + +### 3.5 Benchmarks (Phase E) + +| File | Action | +|------|--------| +| `Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs` | **Port** to measure `Application.Create().UseTuiConfiguration().Init()` cold-start, or delete if MEC startup is well-characterized elsewhere | +| `Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs` | **Port** to measure `IThemeManager.Theme = "X"` (which triggers `ThemeChanges.ThemeChanged`) — this is still the same user-facing operation | + +These benchmarks are the only quantitative basis for any "startup is +faster after CM removal" claim, so porting (not deleting) is preferred. + +### 3.6 Cross-test-project sanity check + +One outlier hit in +`Tests/UnitTestsParallelizable/Input/Keyboard/CommandInsertCaretKeyBindingTests.cs` +matched the broad regex but contains no actual CM references — false +positive (likely `Settings` substring). No action. + +--- + +## 4. What This Prep Did NOT Touch + +- D-02 (`config.json` shape decision) — still gated on source-session input. +- `TuiConfigMigrator` design — blocked on D-02. +- Migration guide draft — deferred until D-02 resolves so the user-facing + examples are accurate. +- Phase A2 (Mec managers own theme/scheme data) — owned by the source + session per the last cross-session sync. + +--- + +## 5. Next Executable Step (now unblocked) + +D-02 resolved to **α-lite** (detect + warn; no library-side migration). +See `remove-legacy-cm.md` §5.4 and Phase D for the full scope. Concretely: + +1. Rewrite `Terminal.Gui/Resources/config.json` once (flat → nested, + array-themes → dict-themes). +2. Add ~20 LOC peek-and-warn to `TuiConfigurationBuilder.AddTuiJsonFile` + (detect top-level keys containing `.` or `Themes` as a JSON array; + emit one `WARN` log per offending file). +3. Delete `Terminal.Gui/Configuration/ScopeJsonConverter.cs` — nothing + left in the library reads the legacy shape. +4. Wire `JsonConfigurationSource.OnLoadException → TuiJsonErrors` + aggregator in `TuiConfigurationExtensions` (spec §5.7). +5. Rewrite every `Examples/.../config.json` to nested. +6. Add `Tools/MigrateConfig/` console app (~50 LOC, separate csproj, + not in any shipping solution). +7. Two parallelizable tests: one asserts the warning fires on a + flat-key sample, one on an array-themes sample. **No translation + logic to test.** + +Once Phase D lands, Phase A2 (Mec managers consume `IOptionsMonitor.CurrentValue` directly) unblocks. diff --git a/specs/remove-legacy-cm.md b/specs/remove-legacy-cm.md new file mode 100644 index 0000000000..d957542baf --- /dev/null +++ b/specs/remove-legacy-cm.md @@ -0,0 +1,496 @@ +# Spec: Remove Legacy ConfigurationManager (Phase 4–6 of CM → MEC) + +> **Status:** Implemented in PR #5416 (`tig/remove-cm-followup`). +> **Tracking Issue:** [#4943](https://github.com/gui-cs/Terminal.Gui/issues/4943) +> **PR:** [#5416 — `tig/remove-cm-followup`](https://github.com/gui-cs/Terminal.Gui/pull/5416) (stacked on `copilot/replace-cm-with-mec`) +> **Predecessor Spec:** [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md) + +--- + +## 1. Purpose + +PR #5411 completed the **functional** CM → MEC migration plus the cross-cutting cleanup that did not require deleting CM itself: + +- Per‑component Settings POCOs with static `Defaults` facades. +- `TuiConfigurationBuilder` + `TuiConfigurationExtensions` providing the MEC-based source chain. +- `IThemeManager` / `ISchemeManager` services. `IThemeManager.ThemeChanged` exists and is wired (A1 done). +- `Terminal.Gui.Configuration.ThemeChanges` static facade — bridges both `ConfigurationManager.Applied` (because `CM.Apply()` writes `ConfigProperty` values directly, bypassing C# setters) and `IThemeManager.ThemeChanged` into a single observer-friendly event. All four internal view subscribers (`Menu`, `MenuBar`, `StatusBar`, `LineCanvas`) have migrated off `ConfigurationManager.Applied`. +- `Terminal.Gui.Configuration.TuiSerializerContext` — internal, non-obsolete holder of the configured `SourceGenerationContext` instance with all custom converters and JSON options. All 7 internal JSON converter consumers reference `TuiSerializerContext.Instance` directly; their `#pragma warning disable CS0618` blocks are gone. `ConfigurationManager.SerializerContext` is now a one-line delegator. +- All `[ConfigurationProperty(...)]` attributes removed from production view/option properties (only the attribute *class* still exists). +- `ConfigurationManager`, `SourcesManager`, `ConfigProperty`, `Scope`, `DeepCloner`, `ScopeJsonConverter` marked `[Obsolete]` and only referenced internally during the transition. + +This PR finishes the migration by **deleting** every legacy CM type, **migrating** the embedded `Resources/config.json` to a MEC-compatible shape, **promoting** `MecThemeManager`/`MecSchemeManager` from event-bridging wrappers to data owners (A2), and **removing** all CM-specific tests. The result is the AOT size reduction and parallel-test unlock that motivated #4943. + +This corresponds to **Phases 4, 5, and 6** of the predecessor spec, executed as a single stacked PR. See the `Phase 3A.x — Internal subscriber rewiring (landed in #5411)` subsection of [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md) for the authoritative record of what shipped in the parent PR. + +--- + +## 2. Goals + +1. **Complete Phase A2** — Migrate runtime theme/scheme data ownership from `ConfigurationManager.Settings["Themes"]` (parsed by `ScopeJsonConverter`) into `IOptionsMonitor`. This is the gating prerequisite for deleting `ScopeJsonConverter` and is tightly coupled to the D-02 resource-shape decision (see §5.4). +2. **Delete every legacy CM type and attribute** (no `[Obsolete]` shim period — they were already shimmed in #5411). +3. **Eliminate CM-related anti-trim patterns**: `ConfigPropertyHostTypes.GetTypes()`, `[DynamicDependency]` on 29 host types, `Assembly.GetTypes()` reflection scan, `DeepCloner` reflection, `MakeGenericType` usage. +4. **Migrate the embedded `Terminal.Gui/Resources/config.json`** from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` layout to a MEC-readable shape. Either nested-only with a one-release migration helper, **or** a custom MEC source that parses the legacy shape directly — see D-02 in §5.4. +5. **Delete the four-line `ConfigurationManager.Applied` bridge** inside `ThemeChanges` once `ConfigurationManager` is gone. Keep `ThemeChanges` itself as the supported public observer facade. +6. **Delete the `ConfigurationManager.SerializerContext` delegator** (one field). All real consumers already reference `TuiSerializerContext.Instance`. +7. **Delete all CM-only tests** (`ConfigurationMangerTests`, `ConfigPropertyAssemblyScanTests`, `ConfigPropertyTests`, `ConfigPropertyHostTypesTests`, `DeepClonerTests`, `ScopeJsonConverterTests`, `ScopeTests`, `SettingsScopeTests`, `ThemeScopeTests`, `ConfigurationPropertyAttributeTests`, legacy `SourcesManagerTests`). Keep MEC equivalents. +8. **Measure and record the NativeAOT binary-size delta** in PR #5416 so the stated motivation in #4943 is verifiable. +9. **Update `docfx/docs/config.md`** so the documented contract matches the shipped contract (Constitution: *Documentation Is the Spec*). + +### Non‑goals + +- Generic Host / `IHostBuilder` adoption (deferred, Q‑02). +- View constructor injection of `IOptionsMonitor` (deferred — static `Defaults` facade remains per D‑01). +- Multi-instance `IApplication` scoping of themes (D‑03). +- Lazy driver registration / assembly splitting / TextMate conditional compilation (out of scope per #10 of predecessor spec). + +--- + +## 3. Constitution Alignment + +| Tenet | How this PR aligns | +|-------|--------------------| +| ✅ **Testability First** | Removing CM static state moves every theme/scheme test into `UnitTestsParallelizable`. | +| ✅ **Performance Is a Feature** | Removes the `Assembly.GetTypes()` scan + DeepCloner reflection at module init. Hot path (`Rune` read on a glyph, `ShadowStyles` read on Button) becomes a direct field read on a POCO. | +| ✅ **Users Have Final Control** | All configurable surfaces stay configurable. Only the *internal mechanism* changes; the documented public configuration model is MEC. | +| ✅ **Separation of Concerns** | Views no longer reach into a process-wide static (`ConfigurationManager.Settings["Foo"]`). They observe `IOptionsMonitor` (via the static `Defaults` facade for now). | +| ✅ **Respect What Came Before** | The 9‑level precedence semantics are preserved through the MEC provider chain assembled in `TuiConfigurationExtensions`. The user-facing JSON keeps the same *shape* aside from the flat→nested key migration. | +| 🔴 **Documentation Is the Spec** | `docfx/docs/config.md` is rewritten in this PR; no out-of-date references to `ConfigurationManager.Enable`, `RuntimeConfig`, `Applied` event, etc. | + +--- + +## 4. Current Residual Surface (post‑#5411) + +The exhaustive inventory of what still has to go. Discovered by `grep`ping the worktree against `copilot/replace-cm-with-mec` HEAD. + +### 4.1 Types to **delete** entirely (`Terminal.Gui/Configuration/`) + +| File | Notes | +|------|-------| +| `ConfigurationManager.cs` (~33 KB) | Static facade; obsolete | +| `ConfigurationManagerEventArgs.cs` | `Applied`/`Updated` event args | +| `ConfigurationManagerNotEnabledException.cs` | No "enabled" concept in MEC | +| `ConfigurationPropertyAttribute.cs` | Attribute already unused on production properties | +| `ConfigProperty.cs` (~28 KB) | Reflection-discovered config property descriptor | +| `ConfigPropertyHostTypes.cs` | `[DynamicDependency]` rooting of 29 host types | +| `ConfigLocations.cs` | Replaced by MEC provider ordering in `TuiConfigurationExtensions` | +| `DeepCloner.cs` (~19 KB) | Reflection-based deep clone; not needed once POCOs replace `Scope` | +| `Scope.cs`, `SettingsScope.cs`, `AppSettingsScope.cs`, `ThemeScope.cs` | Scope abstractions superseded by typed `IOptions` | +| `ScopeJsonConverter.cs` | Flat-key scope deserializer | +| `DictionaryJsonConverter.cs`, `ConcurrentDictionaryJsonConverter.cs` | Only needed by the flat-key scope format | +| `KeyArrayJsonConverter.cs`, `KeyCodeJsonConverter.cs`, `MouseFlagsArrayJsonConverter.cs` | Audit — only delete if unused after migration; otherwise keep and register on the MEC-side `JsonSerializerOptions` | + +### 4.2 Types to **keep** (already MEC-shaped) and harden + +| File | Action | +|------|--------| +| `TuiSerializerContext.cs` | **Already exists post-#5411.** No change. This is the canonical JSON context for the MEC era. | +| `SourceGenerationContext.cs` | Keep. Re-audit `[JsonSerializable]` entries — remove any that reference `SettingsScope` / `ThemeScope` / `AppSettingsScope`. Add `ThemeSettings` and the per-component settings POCOs. | +| `ThemeChanges.cs` | **Already exists post-#5411.** Keep as the supported observer facade. In this PR: remove the `ConfigurationManager.Applied` bridge inside it (one branch of an `OR`) once `ConfigurationManager` is deleted. | +| `AttributeJsonConverter.cs`, `SchemeJsonConverter.cs`, `RuneJsonConverter.cs`, `KeyJsonConverter.cs`, `ColorJsonConverter.cs`, `TraceCategoryJsonConverter.cs`, `DictionaryJsonConverter.cs`, `ConcurrentDictionaryJsonConverter.cs` | Keep. **Already point at `TuiSerializerContext.Instance` post-#5411.** No further work. | +| `Settings/*Settings.cs` (all 30+) | Keep. These are the POCOs that became the source of truth in #5411. **Pattern divergence (A2.1+):** ThemeScope POCOs (the 17/18 bind targets of `BindThemeScope`) are immutable `sealed record` + `Default`/`Current` with `Volatile`-swapped atomic publish; SettingsScope POCOs remain mutable `Defaults`. Rationale: only ThemeScope participates in theme overlay merge; SettingsScope is bound once at app start and never per-theme. | +| `Settings/TuiConfigurationBuilder.cs` | Keep. Becomes the **only** configuration entry point. | +| `Settings/TuiConfigurationExtensions.cs` | Keep. | +| `Settings/IThemeManager.cs`, `Settings/ISchemeManager.cs`, `Settings/MecThemeManager.cs`, `Settings/MecSchemeManager.cs` | Keep. **A1 (event plumbing) is done post-#5411.** A2 (data ownership) is this PR's work — see §5.2. | + +### 4.3 Types to **rename** (drop the `Mec` prefix once the legacy peers are gone) + +| Old (post‑#5411) | New | Outcome | +|------------------|-----|---------| +| `MecSchemeManager` | `SchemeManager` | **Done.** `SchemeManager` implements `ISchemeManager` with explicit `GetScheme`. | +| `MecThemeManager` | `ThemeManager` | **Kept.** Static `ThemeManager` was retained as a MEC-backed facade (`ThemeManager.Theme =`). A static `ThemeChanged` event cannot coexist with the instance event on `IThemeManager`. | +| `Tests/UnitTestsParallelizable/Configuration/MecSettingsTests.cs` | `SettingsTests.cs` | Renamed | +| `Tests/UnitTestsParallelizable/Configuration/MecThemeTests.cs` | `ThemeTests.cs` | Renamed | +| `Tests/UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs` | `AppSettingsTests.cs` | Renamed | +| `Tests/UnitTestsParallelizable/Configuration/MecDottedKeyTests.cs` | `DottedKeyTests.cs` | Renamed | + +### 4.4 Callers to **rewire** (production source outside `Configuration/`) + +| File | Current dependency | Replacement | +|------|--------------------|-------------| +| `ModuleInitializers.cs` | Calls `ConfigurationManager.Initialize ()` then `mecBuilder.ApplyToStaticFacades ()` | Single call to `TuiConfigurationBuilder.ApplyToStaticFacades ()`. Delete the dual-init suppression. Gated by A2 completion. | +| `App/ApplicationImpl.Lifecycle.cs` | `ConfigurationManager.PrintJsonErrors ()` | Replace with an equivalent aggregated-error printer fed by `JsonConfigurationSource.OnLoadException`. See §5.4 and §6 Phase D. | +| `App/Application.cs` | Several `ConfigurationManager.*` references in xmldoc/method bodies | Replace with `TuiConfigurationBuilder` references or delete. | +| `App/Tracing/Trace.cs` | One CM reference | Replace with `TraceSettings.Defaults.EnabledCategories`. | +| `Drawing/Scheme.cs` | xmldoc references CM in a comment | Update wording. | +| `Drawing/Glyphs.cs` | (None — already POCO-backed via `GlyphSettings.Defaults`.) | No change. Confirm via grep after removal. | +| `Input/CommandBindingsBase.cs` | Comment refers to `ConfigurationManager.Apply` / `DeepMemberWiseCopy` | Update comment; the dictionary‑copy workaround stays because the underlying race is now in `IOptionsMonitor` callbacks. | +| Stale `using Terminal.Gui.Configuration;` / xmldoc references in: `Views/Menu/PopoverMenu.cs`, `Views/Selectors/SelectorBase.cs`, `Views/Window.cs`, `Views/Dialog.cs`, `Views/FrameView.cs`, `Views/MessageBox.cs`, `Views/HexView.cs`, `Views/CharMap/CharMap.cs`, `Views/CheckBox.cs`, `Views/Button.cs`, `Views/FileDialogs/FileDialog.cs`, `Views/FileDialogs/FileDialogStyle.cs`, `Views/LinearRange/LinearRangeDefaults.cs`, `Views/LinearRange/LinearRangeViewBase.cs`, `Views/TextInput/TextField/TextField.cs`, `Views/TextInput/TextView/TextView.cs`, `Views/TreeView/TreeViewT.cs`, `ViewBase/View.Keyboard.cs`, `ViewBase/Mouse/View.Mouse.cs`, `ViewBase/Adornment/BorderView.Arrangement.cs`, `Text/NerdFonts.cs`, `FileServices/FileSystemIconProvider.cs`, `Drawing/Color/Color.cs`, `Input/Keyboard/Key.cs`, `Drivers/Driver.cs`, `App/Legacy/Application.Mouse.cs` | No live code dependency (verified — `[ConfigurationProperty(` grep returns only the attribute file itself). | Mechanical comment sweep. | + +**Already rewired in #5411 (no action in this PR):** `Views/Menu/Menu.cs`, `Views/Menu/MenuBar.cs`, `Views/StatusBar.cs`, `Drawing/LineCanvas/LineCanvas.cs` all subscribe to `ThemeChanges.ThemeChanged` instead of `ConfigurationManager.Applied`. + +### 4.5 Resource files + +| File | Action | +|------|--------| +| `Terminal.Gui/Resources/config.json` (52 KB, ~1,500 lines) | **Rewrite** in nested MEC format. Keys like `"Button.DefaultShadow"` become `Button: { DefaultShadow: ... }`. The `"Themes": [ { "Default": { ... } } ]` array-of-single-key-objects becomes `Themes: { Default: { ... }, Dark: { ... } }`. Schemes stay as a nested map. The `$schema` URL stays; the hosted schema must be updated in tandem (Q‑03 — file a tracking issue). | +| `Examples/Config/example_config.json` | Migrate alongside library `config.json`. | +| `Tests/.../*config.json` (if any) | Audit and migrate. | + +### 4.6 Tests to **delete** (`Tests/`) + +| File | Reason | +|------|--------| +| `UnitTests.NonParallelizable/Configuration/ConfigurationMangerTests.cs` | Tests deleted facade. | +| `UnitTests.NonParallelizable/Configuration/ConfigPropertyAssemblyScanTests.cs` | Tests deleted reflection scan. | +| `UnitTests.NonParallelizable/Configuration/SourcesManagerLoadNullJsonTests.cs` | Replaced by `SourcesManagerTests` in `UnitTestsParallelizable` (which already test the MEC builder). | +| `UnitTests.NonParallelizable/Configuration/GlyphTests.cs` | Move to parallelizable if it doesn't depend on static state; otherwise replace with MEC equivalent. | +| `UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs` | Tests deleted type. | +| `UnitTestsParallelizable/Configuration/ConfigPropertyHostTypesTests.cs` | Tests deleted type. | +| `UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs` | Tests deleted attribute. | +| `UnitTestsParallelizable/Configuration/DeepClonerTests.cs` | Tests deleted utility. | +| `UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs` | Tests deleted converter. | +| `UnitTestsParallelizable/Configuration/ScopeTests.cs` | Tests deleted base class. | +| `UnitTestsParallelizable/Configuration/SettingsScopeTests.cs` | Tests deleted scope. | +| `UnitTestsParallelizable/Configuration/ThemeScopeTests.cs` | Tests deleted scope. | +| `Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs` | Replace with `TuiConfigurationBuilderBuildBenchmark`. | +| `Benchmarks/Configuration/ThemeSwitchBenchmark.cs` | Rewrite against `IThemeManager.SwitchTheme`. | + +### 4.7 Tests to **keep** (and audit) + +- `UnitTestsParallelizable/Configuration/MecAppSettingsTests.cs` +- `UnitTestsParallelizable/Configuration/MecSettingsTests.cs` +- `UnitTestsParallelizable/Configuration/MecThemeTests.cs` +- `UnitTestsParallelizable/Configuration/SourcesManagerTests.cs` (MEC version — confirm not the legacy one) +- `UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs` +- `UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs` +- `UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs` +- All `*DefaultKeyBindingsTests.cs` (these read `View.DefaultKeyBindings`; after CM removal they read it via the bound POCO) + +### 4.8 Examples to **rewrite** + +| File | Action | +|------|--------| +| `Examples/UICatalog/Runner.cs` | Replace `ConfigurationManager.RuntimeConfig = "..."` with `TuiConfigurationBuilder.Runtime(...)` | +| `Examples/UICatalog/UICatalog.cs` and `UICatalogRunnable.cs` | Replace `ConfigurationManager.Enable/Apply/Applied` with the new builder + `IOptionsMonitor.OnChange` | +| `Examples/UICatalog/Scenarios/ConfigurationEditor.cs` | Heavy CM user. Rewrite to enumerate the `Settings/*Settings.cs` POCOs and serialize via `SourceGenerationContext`. | +| `Examples/UICatalog/Scenarios/Themes.cs`, `ThemeFallback.cs` | Use `IThemeManager.ThemeChanged` and `IThemeManager.SwitchTheme`. | +| `Examples/NativeAot/Program.cs`, `Examples/SelfContained/Program.cs`, `Examples/ReactiveExample/Program.cs`, `Examples/CommunityToolkitExample/Program.cs`, `Examples/ScenarioRunner/Program.cs`, `Examples/ShortcutTest/ShortcutTest.cs`, `Examples/PromptExample/Program.cs`, `Examples/Example/Example.cs` | Drop `ConfigurationManager.Enable (...)`; replace with `new TuiConfigurationBuilder ().ApplyToStaticFacades ()` (or remove entirely if defaults are sufficient). | +| `Examples/Config/README.md`, `Examples/NativeAot/README.md`, `Examples/SelfContained/README.md`, `Examples/ReactiveExample/README.md`, `Examples/CommunityToolkitExample/README.md` | Update documentation to reference MEC. | +| All `Examples/UICatalog/Scenarios/*.cs` files listed in §4 of the discovery output | Most contain only stale `using Terminal.Gui.Configuration;` plus an occasional `ConfigurationManager.Applied` subscription. Sweep with a single mechanical pass. | + +--- + +## 5. Detailed Design + +### 5.1 New `IOptionsMonitor` flow + +```text +TuiConfigurationBuilder.Build() + └─ IConfigurationBuilder + ├─ AddTuiLibraryDefaults() (embedded Terminal.Gui.Resources.config.json) + ├─ AddTuiAppDefaults(appName) (entry assembly config.json) + ├─ AddTuiUserFiles(appName) (~/.tui/*.json, ./.tui/*.json) + ├─ AddTuiEnvironmentVariable() (TUI_CONFIG) + └─ AddTuiRuntimeConfig(json) (in-memory string) + └─ IConfiguration root + └─ services.Configure(root.GetSection("Themes")) + └─ services.Configure(root.GetSection("Application")) + └─ services.Configure(root.GetSection("Button")) + └─ ... one Configure per POCO + +TuiConfigurationBuilder.ApplyToStaticFacades() + └─ ButtonSettings.Current = bound ButtonSettings (root + Themes:{active}:Button overlay) + └─ DialogSettings.Current = bound DialogSettings + └─ GlyphSettings.Current = bound GlyphSettings + └─ ... per ThemeScope POCO + └─ ApplicationSettings.Defaults / DriverSettings.Defaults / ... for SettingsScope POCOs +``` + +`ButtonSettings.Current` (and peers) are the **static facade** used by all views per D‑01. The compile-time `Default` snapshot is immutable; `Current` is swapped atomically. The only writer is `TuiConfigurationBuilder.ApplyToStaticFacades` — never `ConfigurationManager.Apply`. + +### 5.2 ThemeManager / SchemeManager: from event-bridging wrapper to data owner (A2) + +Post-#5411 status (**A1 — done**): `IThemeManager.ThemeChanged` exists; `MecThemeManager` subscribes to the legacy static `ThemeManager.ThemeChanged` in its constructor and forwards. `ThemeChanges.ThemeChanged` is the public observer facade used by internal views; it bridges both `ConfigurationManager.Applied` *and* `IThemeManager.ThemeChanged` because `CM.Apply()` writes `ConfigProperty` values directly and bypasses the C# setter, so `ThemeChanged` alone would miss most theme switches today. + +This PR's work (**A2**): make `MecThemeManager` / `MecSchemeManager` the **owners** of theme and scheme runtime data, so the `ConfigurationManager.Applied` half of the `ThemeChanges` bridge can be deleted along with `ConfigurationManager` itself. + +Today the runtime theme/scheme dictionary lives in `ConfigurationManager.Settings["Themes"]`, parsed by `ScopeJsonConverter`. The target shape: + +```csharp +public sealed class ThemeManager : IThemeManager +{ + private readonly IOptionsMonitor _monitor; + private IDisposable? _subscription; + + public ThemeManager (IOptionsMonitor monitor) + { + _monitor = monitor; + _subscription = _monitor.OnChange (HandleChanged); + } + + public string ActiveTheme + { + get => _monitor.CurrentValue.ActiveTheme; + set => SwitchTheme (value); + } + + public IReadOnlyList ThemeNames => _monitor.CurrentValue.Themes.Keys.ToImmutableList (); + + public event EventHandler? ThemeChanged; + + public void SwitchTheme (string name) { /* mutate in-memory provider + reload */ } + + private void HandleChanged (ThemeSettings settings, string? name) + => ThemeChanged?.Invoke (this, settings.ActiveTheme); +} +``` + +A2 is the **gating prerequisite** for deleting `ScopeJsonConverter` and is tied to the D-02 resource-shape decision: the MEC binder cannot bind `ThemeSettings` from the legacy flat-key `Themes: [ { Name: { "Class.Prop": ... } } ]` shape without a custom source. Either: + +- **Option α (recommended):** Rewrite `Terminal.Gui/Resources/config.json` to the MEC-native nested shape, ship `TuiConfigMigrator` for user files. `ScopeJsonConverter` deletes cleanly. +- **Option β:** Keep the legacy shape and write a `LegacyTuiConfigurationSource : IConfigurationSource` that re-emits flat keys as nested MEC keys at load time. Smaller user blast radius; larger ongoing maintenance. + +§5.4 carries the analysis. + +`SchemeManager` becomes an instance class fronting `ThemeSettings.Schemes`. The static `SchemeManager.GetScheme(string)` API used pervasively in views is kept as a **static convenience facade** that forwards to the registered service (via a static `Application.Services` accessor) or to a process-wide fallback when no app has been created — same shape as the `XxxSettings.Defaults` pattern adopted in #5411. + +### 5.3 `ThemeChanges` bridge cleanup + +`ThemeChanges` currently raises its event in response to **either**: + +1. `ConfigurationManager.Applied` (legacy CM apply path), or +2. `IThemeManager.ThemeChanged` (MEC path, post‑A1). + +After A2 lands, (1) is dead — every theme switch goes through `MecThemeManager.SwitchTheme` ⇒ `IOptionsMonitor.OnChange` ⇒ `IThemeManager.ThemeChanged` ⇒ `ThemeChanges.ThemeChanged`. The `ConfigurationManager.Applied` subscription inside `ThemeChanges` deletes alongside `ConfigurationManager`. + +No view-side changes are required — `ThemeChanges` is the public surface and stays. + +### 5.4 Library `config.json`: resolving D-02 + +**Resolution: α-lite (detect + warn, no library-side migration).** + +The MEC read path is pure-nested. `TuiConfigurationBuilder.AddTuiJsonFile` (and the equivalent extension overloads) peek the JSON before binding and, if they detect pre-MEC shapes — top-level keys containing `.`, or `Themes` as a JSON array — emit a single `WARN`-level log identifying the file path and pointing at the migration documentation URL. The legacy shape is **not** parsed; affected settings fall through to defaults. + +**Rationale.** v2 is GA, so silent breakage (γ) is inappropriate. But hand-edited `config.json` usage is rare and concentrated in app authors who control their own upgrade timing, so a perpetual translation layer (β `LegacyTuiConfigurationSource`) or a full bidirectional migrator (α `TuiConfigMigrator`) buys little and costs permanent library surface. α-lite is the minimal GA-appropriate response: ~20 LOC of detection + one log line — no parsing of the legacy shape, no auto-migration, no round-trip tests, no deprecation cycle to engineer. + +**Library resource rewrite (one-time, in this PR).** `Terminal.Gui/Resources/config.json` is regenerated in the nested MEC-native shape: + +Before (flat): +```json +{ + "Themes": [ + { + "Default": { + "Button.DefaultShadow": "Opaque", + "Glyphs.CheckStateChecked": "☑", + "Schemes": [ { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } ] + } + } + ] +} +``` + +After (nested, MEC-native): +```json +{ + "Themes": { + "Default": { + "Button": { "DefaultShadow": "Opaque" }, + "Glyphs": { "CheckStateChecked": "☑" }, + "Schemes": { "Base": { "Normal": { "Foreground": "White", "Background": "Black" } } } + } + } +} +``` + +`Examples/Config/example_config.json` and every UICatalog/example config file gets the same treatment. + +**Detection heuristic (implementation note).** Single `JsonDocument` walk, no schema knowledge required: + +- *Flat key* = any top-level property name in the root object whose name contains `.`. +- *Array themes* = the `Themes` property exists and its `ValueKind == JsonValueKind.Array`. + +Both checks together: < 20 LOC, no allocation beyond the `JsonDocument`. The warn message names the file and links to `docfx/docs/migrate-cm-to-mec.md` (and the migration tool below). + +**Migration aid.** A standalone `Tools/MigrateConfig/` console app (~50 LOC) lives in the repo but is **not** shipped in `Terminal.Gui.dll` and **not** added to any solution that ships. The detection warning references it. The tool is deletable any time without a deprecation cycle. + +**Cleanup horizon.** The detection-and-warn code itself can be deleted in a future minor when issue volume indicates no remaining users hit it. It is small enough to keep indefinitely if preferred — the decision is non-coupled to anything in this PR. + +**Explicitly rejected alternatives** (recorded so reviewers don't re-litigate): + +- ❌ *"Keep both shapes forever, just document both."* Combines β's permanent maintenance cost with α's user confusion cost. No upside. +- ❌ *"Silently translate legacy shapes."* Opaque failure mode if translation is wrong; GA-inappropriate. +- ❌ *"Throw on legacy shape."* Too aggressive for a non-malformed JSON file. Warn-and-ignore is the right contract — the file is still valid JSON, it just no longer matches our schema. + +### 5.5 Source generation context cleanup + +`TuiSerializerContext` (created in #5411) already configures all custom converters and `JsonSerializerOptions`. Remaining work in this PR: + +- Audit `SourceGenerationContext`'s `[JsonSerializable]` entries. Remove anything that references `SettingsScope` / `ThemeScope` / `AppSettingsScope` (those types are being deleted). +- Ensure every Settings POCO actively bound by `TuiConfigurationBuilder` is registered: + +```csharp +[JsonSerializable (typeof (ThemeSettings))] +[JsonSerializable (typeof (ApplicationSettings))] +[JsonSerializable (typeof (ButtonSettings))] +[JsonSerializable (typeof (DialogSettings))] +[JsonSerializable (typeof (GlyphSettings))] +// ... one entry per Settings/*Settings.cs POCO +[JsonSerializable (typeof (Scheme))] +[JsonSerializable (typeof (Attribute))] +[JsonSerializable (typeof (Color))] +[JsonSerializable (typeof (Dictionary))] +[JsonSerializable (typeof (Dictionary))] +internal partial class SourceGenerationContext : JsonSerializerContext { } +``` + +The converter registration is owned by `TuiSerializerContext` and does not move. + +### 5.6 ModuleInitializer simplification + +After A2 + the legacy CM deletion: + +```csharp +[ModuleInitializer] +internal static void InitializeTuiConfiguration () +{ + TuiConfigurationBuilder builder = new (); + builder.ApplyToStaticFacades (); +} +``` + +Single call. No `ConfigurationManager.Initialize ()`, no `#pragma warning disable CS0618`. + +### 5.7 `PrintJsonErrors` replacement (behavior-preserving) + +The v2 contract is "don't fail-fast on bad config.json; collect errors and print at shutdown." MEC supports this via `JsonConfigurationSource.OnLoadException`: + +```csharp +builder.AddJsonStream (stream, source => +{ + source.OnLoadException = ctx => + { + TuiJsonErrors.Add ($"{ctx.Source}: {ctx.Exception.Message}"); + ctx.Ignored = true; + }; +}); +``` + +Wire the same hook on every JSON source registered by `TuiConfigurationExtensions`. `ApplicationImpl.Lifecycle.Shutdown` calls `TuiJsonErrors.Print ()` (or routes through `Logging`) in place of the current `ConfigurationManager.PrintJsonErrors ()` call. + +Caveat: `OnLoadException` covers file/parse errors only. Bind/POCO validation errors (`OptionsValidationException`) don't have an equivalent hook in MEC and will throw on first `IOptions.Value` access. This is acceptable — bind errors are programmer-level (POCO shape mismatch), not user-level (typo in JSON). + +--- + +## 6. Implementation Phases (within this PR) + +Each phase is one or more commits. Tests must build and pass at every commit. **Phases A1, B, and the JSON-converter half of C landed in #5411 and are not repeated here.** + +### Phase D — Library `config.json` rewrite + α-lite detection *(blocks Phase A2)* +Scope (per §5.4 resolution): +1. Rewrite `Terminal.Gui/Resources/config.json` in the nested MEC-native shape. +2. Add the ~20 LOC peek-and-warn to `TuiConfigurationBuilder.AddTuiJsonFile` (and any sibling overloads). Detection heuristic: flat top-level keys containing `.`, or `Themes` as a JSON array. +3. Delete `Terminal.Gui/Configuration/ScopeJsonConverter.cs` (with the legacy parser gone, nothing reads the legacy shape inside `Terminal.Gui.dll`). +4. Rewrite `Examples/Config/example_config.json` and every UICatalog / example config to nested. +5. Add `Tools/MigrateConfig/` — standalone console app (~50 LOC), separate csproj, not in any shipping solution. +6. Add CHANGELOG entry pointing at the tool and the migration guide URL. +7. Wire `JsonConfigurationSource.OnLoadException` per §5.7 so v2 deferred-error behavior is preserved. +8. Update `docfx/schemas/tui-config-schema.json` to the nested MEC shape (Q-03 / #5631). Docs publish hosts it at `https://tui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json`. + +Tests for Phase D: **two** parallelizable tests — one asserts the warning fires on a flat-key sample, one asserts it fires on an array-themes sample. No translation logic to test, so no round-trip tests are required. + +### Phase A2 — Mec managers own runtime theme/scheme data +- Have `MecThemeManager` / `MecSchemeManager` read theme and scheme dictionaries from `IOptionsMonitor.CurrentValue` instead of delegating to the legacy static `ThemeManager` / `SchemeManager`. +- `SwitchTheme` mutates the underlying in-memory MEC source and triggers `IOptionsMonitor.OnChange`. +- Confirm `ThemeChanges.ThemeChanged` is now raised exclusively through the `IThemeManager.ThemeChanged` path; verify no `ConfigurationManager.Applied` round-trip happens at runtime. +- Add tests in `MecThemeTests` for switch / add / remove scheme paths that previously delegated to the legacy code. + +### Phase C-finish — `SourceGenerationContext` POCO audit +- Remove `SettingsScope` / `ThemeScope` / `AppSettingsScope` from `[JsonSerializable]` list. +- Add per-component POCOs and `Dictionary` / `Dictionary` per §5.5. +- Delete the `ConfigurationManager.SerializerContext` one-line delegator field (kept post-#5411 purely to avoid an obsolete-attr breaking change in the parent PR). + +### Phase E — Delete legacy CM types + tests +- Delete every file listed in §4.1 and every test listed in §4.6. +- Delete `ModuleInitializers.cs`'s `ConfigurationManager.Initialize ()` call (§5.6). +- Replace `ApplicationImpl.Lifecycle`'s `ConfigurationManager.PrintJsonErrors ()` call with `TuiJsonErrors.Print ()` per §5.7. +- Delete the `ConfigurationManager.Applied` branch inside `ThemeChanges` per §5.3. +- Sweep stale doc-comment references (§4.4 last row). + +### Phase F — Rename `MecThemeManager`/`MecSchemeManager` → `ThemeManager`/`SchemeManager` +- Test files renamed (`SettingsTests`, `ThemeTests`, `AppSettingsTests`, `DottedKeyTests`). +- Production `MecThemeManager` / `MecSchemeManager` **keep the `Mec` prefix**: static `ThemeManager` / `SchemeManager` facades were retained for `ThemeManager.Theme =` and `SchemeManager.GetScheme` callers. C# cannot host both the static facade members and the `IThemeManager` / `ISchemeManager` instance members on one type (`ThemeChanged` event and `GetScheme` collide). The instance types stay the `IThemeManager` / `ISchemeManager` implementations returned by `TuiConfigurationBuilder`. + +### Phase G — Update examples (Runner, UICatalog, NativeAot, etc.) +- §4.8 sweep. + +### Phase H — Update `docfx/docs/config.md` + migration guide +- Rewrite `config.md` to reflect the MEC contract. +- Remove every reference to `ConfigurationManager.Enable`, `ConfigLocations`, `ConfigurationProperty`, `SettingsScope`, `ThemeScope`, `AppSettingsScope`. +- Document `TuiConfigurationBuilder`, `IThemeManager`, `ISchemeManager`, the nested JSON schema, the migration helper, `ThemeChanges`, `TuiSerializerContext`. +- Add `docfx/docs/migrate-cm-to-mec.md` cheatsheet. + +### Phase I — Verification +- Run full test matrix (`UnitTestsParallelizable`, `UnitTests.NonParallelizable`, `IntegrationTests`). +- Confirm zero new warnings (the `CS0618` suppressions are gone — if anything tries to use a deleted obsolete API the build fails by design). +- Build `Tests/NativeAotSmoke` with `PublishAot=true` and record `before` / `after` binary size in the PR description. +- Run `Benchmarks/Configuration/*` and record delta. + +#### Phase I results + +| Check | Result | +|-------|--------| +| `Tests/NativeAotSmoke` `PublishAot=true` `win-arm64` | develop `24,640,512` (23.50 MB) → this PR `24,201,728` (23.08 MB), **−438,784 (−1.78%)** | +| Nested-only binder | Dotted keys and array-Themes are not applied; convert with `Tools/MigrateConfig` | +| Test matrix | CI on PR #5416 (parallel, non-parallel, integration) | +| Benchmarks | `TuiConfigurationBuilderBuildBenchmark` replaces `ConfigurationManagerLoadBenchmark`; `ThemeSwitchBenchmark` uses `ThemeManager.Theme =` | + +--- + +## 7. Breaking Changes (Public API) + +The following public surface is **removed**. Source-incompatible for any consumer that touched these (the predecessor #5411 marked them all `[Obsolete]`). + +- `Terminal.Gui.Configuration.ConfigurationManager` (class) +- `Terminal.Gui.Configuration.ConfigurationManagerNotEnabledException` +- `Terminal.Gui.Configuration.ConfigurationPropertyAttribute` +- `Terminal.Gui.Configuration.ConfigLocations` +- `Terminal.Gui.Configuration.SettingsScope`, `ThemeScope`, `AppSettingsScope`, `Scope` +- `Terminal.Gui.Configuration.ConfigProperty` +- `Terminal.Gui.Configuration.ConfigPropertyHostTypes` +- `Terminal.Gui.Configuration.SourcesManager` (replaced by `TuiConfigurationBuilder`) +- `Terminal.Gui.Configuration.DeepCloner` +- `Terminal.Gui.Configuration.ScopeJsonConverter` +- The `Terminal.Gui.Configuration.ConfigurationManager.Applied` / `Updated` events + +Static `ThemeManager` remains the process-wide facade (`ThemeManager.Theme =`). `IThemeManager` is `MecThemeManager` (per-builder). `SchemeManager` is both the static facade and `ISchemeManager`. + +### JSON file breaking change + +The library `Resources/config.json` is nested MEC. User files and `RuntimeConfig` in the legacy flat-key format (`"Button.DefaultShadow": "..."`) or array-themes format are **not** applied; a `WARN` points at the migration documentation and `Tools/MigrateConfig/`. No library-side auto-migration of on-disk files ships. The hosted JSON schema (`docfx/schemas/tui-config-schema.json`) is updated in this PR (#5631). + +### Migration guide + +A separate `docfx/docs/migrate-cm-to-mec.md` is added in Phase H with a side-by-side cheatsheet. The PR description links to it. + +--- + +## 8. Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| Hidden third-party consumers still call obsolete CM API | They received an `[Obsolete]` warning in #5411 with the message pointing at `TuiConfigurationBuilder`. One release window has elapsed by the time this PR ships. | +| Nested JSON breaks existing user config files | α-lite (§5.4): detect-and-warn at load time, fall through to defaults rather than fail. Standalone `Tools/MigrateConfig/` console app + `migrate-cm-to-mec.md` guide give users a one-shot fix path. If issue volume after release indicates more help is needed, β (`LegacyTuiConfigurationSource`) remains a viable hotfix. | +| Renaming `MecThemeManager` → `ThemeManager` clashes with the deleted legacy `ThemeManager` if a partial revert lands | Phase F runs only after Phase E commits; if reverted, the rename is reverted with it. | +| AOT size delta is smaller than predicted | Acceptable. The parallel-test and decoupling wins still justify the change. Record actual delta in the PR. | +| `IOptionsMonitor.OnChange` is not invoked in NativeAOT due to missing trim roots | Verified in #5411. Re-verify with a smoke test in `Examples/NativeAot` as part of Phase I. | +| Bind-time (`OptionsValidationException`) errors are not aggregated like file-parse errors | Accepted. Bind errors are programmer-level (POCO shape mismatch). The user-level "typo in JSON" path is fully preserved via `JsonConfigurationSource.OnLoadException` (§5.7). | + +--- + +## 9. Out of Scope + +Carried forward from the predecessor spec (§10), unchanged: + +1. Generic Host / `IHostBuilder` adoption. +2. Constructor injection mandate for all Views. +3. View-level DI. +4. Multiple concurrent `IApplication` instances. +5. TextMate / Markdig conditional compilation. +6. Lazy driver registration. +7. Assembly splitting. + +--- + +*Authors: @copilot. Companion to [`specs/replace-cm-with-mec.md`](./replace-cm-with-mec.md). Tracking PR: #5416.* diff --git a/specs/replace-cm-with-mec.md b/specs/replace-cm-with-mec.md index 67d5d7af18..79662e2c86 100644 --- a/specs/replace-cm-with-mec.md +++ b/specs/replace-cm-with-mec.md @@ -1,6 +1,6 @@ # Spec: Replace ConfigurationManager (CM) with Microsoft.Extensions.Configuration (MEC) -> **Status:** In Progress — MEC migration is implemented; current focus is finishing complex-type migration in PR #5411. +> **Status:** Historical. The MEC pipeline shipped in #5411; legacy CM types were deleted in #5416. Current contract: [config.md](../docfx/docs/config.md) and [migrate-cm-to-mec.md](../docfx/docs/migrate-cm-to-mec.md). Do not treat the APIs below as live. > **Tracking Issue:** [#4943](https://github.com/gui-cs/Terminal.Gui/issues/4943) > **Related analysis:** See [@tig's AOT binary-size comment](https://github.com/gui-cs/Terminal.Gui/issues/4943#issuecomment-XXXXXX) @@ -1257,7 +1257,7 @@ The following are explicitly **not** part of this proposal: | `Application` | `IsMouseDisabled` | `bool` | `false` | | `ConfigurationManager` | `ThrowOnJsonErrors` | `bool?` | `false` | | `Driver` | `Force16Colors` | `bool` | `false` | -| `Driver` | `SizeDetection` | `SizeDetectionMode` | `AnsiQuery` | +| `Driver` | `SizeDetection` | `SizeDetectionMode` | `Polling` | | `Key` | `Separator` | `char` | `'+'` | | `MenuBar` | `DefaultKey` | `Key` | `F10` | | `PopoverMenu` | `DefaultKey` | `Key` | `Shift+F10` |