brut 0.16.0.pre.pre → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/CHANGELOG.md +9 -3
- data/Gemfile.lock +44 -34
- data/bin/docs +7 -0
- data/brut-css/package-lock.json +2 -2
- data/brut-css/package.json +1 -1
- data/brut-js/package-lock.json +311 -2
- data/brut-js/package.json +2 -1
- data/brut.gemspec +2 -0
- data/brutrb.com/jobs.md +1 -1
- data/docs/404.html +2 -2
- data/docs/adrs.html +4 -4
- data/docs/ai.html +4 -4
- data/docs/api/Brut/BackEnd/SeedData.html +2 -2
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +2 -2
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +2 -2
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +2 -2
- data/docs/api/Brut/BackEnd/Sidekiq.html +2 -2
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +2 -2
- data/docs/api/Brut/BackEnd/Validators.html +2 -2
- data/docs/api/Brut/BackEnd.html +2 -2
- data/docs/api/Brut/CLI/App.html +2 -2
- data/docs/api/Brut/CLI/AppRunner.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +3 -3
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Create.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Status.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB.html +2 -2
- data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +2 -2
- data/docs/api/Brut/CLI/Apps/DeployBase.html +2 -2
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +8 -6
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold.html +2 -2
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +15 -7
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +2 -2
- data/docs/api/Brut/CLI/Apps/Test/JS.html +2 -2
- data/docs/api/Brut/CLI/Apps/Test/Run.html +2 -2
- data/docs/api/Brut/CLI/Apps/Test.html +2 -2
- data/docs/api/Brut/CLI/Apps.html +2 -2
- data/docs/api/Brut/CLI/Command.html +2 -2
- data/docs/api/Brut/CLI/Error.html +2 -2
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +2 -2
- data/docs/api/Brut/CLI/ExecutionResults.html +2 -2
- data/docs/api/Brut/CLI/Executor.html +2 -2
- data/docs/api/Brut/CLI/InvalidOption.html +2 -2
- data/docs/api/Brut/CLI/Options.html +2 -2
- data/docs/api/Brut/CLI/Output.html +2 -2
- data/docs/api/Brut/CLI/SystemExecError.html +2 -2
- data/docs/api/Brut/CLI.html +2 -2
- data/docs/api/Brut/FactoryBot.html +2 -2
- data/docs/api/Brut/Framework/App.html +2 -2
- data/docs/api/Brut/Framework/Config.html +2 -2
- data/docs/api/Brut/Framework/Container.html +11 -7
- data/docs/api/Brut/Framework/Error.html +2 -2
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +2 -2
- data/docs/api/Brut/Framework/Errors/Bug.html +2 -2
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +2 -2
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +2 -2
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +2 -2
- data/docs/api/Brut/Framework/Errors/NotFound.html +2 -2
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +2 -2
- data/docs/api/Brut/Framework/Errors.html +2 -2
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +2 -2
- data/docs/api/Brut/Framework/MCP.html +3 -3
- data/docs/api/Brut/Framework/ProjectEnvironment.html +2 -2
- data/docs/api/Brut/Framework.html +2 -2
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +2 -2
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +2 -2
- data/docs/api/Brut/FrontEnd/Component.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Input.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +2 -2
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +2 -2
- data/docs/api/Brut/FrontEnd/Components.html +2 -2
- data/docs/api/Brut/FrontEnd/CsrfProtector.html +2 -2
- data/docs/api/Brut/FrontEnd/Download.html +2 -2
- data/docs/api/Brut/FrontEnd/Flash.html +2 -2
- data/docs/api/Brut/FrontEnd/Form.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/Button.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/Input.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +2 -2
- data/docs/api/Brut/FrontEnd/Forms.html +2 -2
- data/docs/api/Brut/FrontEnd/GenericResponse.html +2 -2
- data/docs/api/Brut/FrontEnd/Handler.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +2 -2
- data/docs/api/Brut/FrontEnd/Handlers.html +2 -2
- data/docs/api/Brut/FrontEnd/HandlingResults.html +2 -2
- data/docs/api/Brut/FrontEnd/HttpMethod.html +2 -2
- data/docs/api/Brut/FrontEnd/HttpStatus.html +2 -2
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +2 -2
- data/docs/api/Brut/FrontEnd/Layout.html +2 -2
- data/docs/api/Brut/FrontEnd/Middleware.html +2 -2
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +2 -2
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +2 -2
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +2 -2
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +2 -2
- data/docs/api/Brut/FrontEnd/Middlewares.html +2 -2
- data/docs/api/Brut/FrontEnd/Page.html +2 -2
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +2 -2
- data/docs/api/Brut/FrontEnd/Pages.html +2 -2
- data/docs/api/Brut/FrontEnd/RequestContext.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHook.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +2 -2
- data/docs/api/Brut/FrontEnd/RouteHooks.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing/Route.html +2 -2
- data/docs/api/Brut/FrontEnd/Routing.html +2 -2
- data/docs/api/Brut/FrontEnd/Session.html +2 -2
- data/docs/api/Brut/FrontEnd.html +2 -2
- data/docs/api/Brut/I18n/BaseMethods.html +2 -2
- data/docs/api/Brut/I18n/ForBackEnd.html +2 -2
- data/docs/api/Brut/I18n/ForCLI.html +2 -2
- data/docs/api/Brut/I18n/ForHTML.html +2 -2
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +2 -2
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +2 -2
- data/docs/api/Brut/I18n.html +2 -2
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +2 -2
- data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +2 -2
- data/docs/api/Brut/Instrumentation/Methods.html +2 -2
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +2 -2
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +2 -2
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +2 -2
- data/docs/api/Brut/Instrumentation.html +2 -2
- data/docs/api/Brut/RubocopConfig.html +2 -2
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +2 -2
- data/docs/api/Brut/SinatraHelpers.html +2 -2
- data/docs/api/Brut/SpecSupport/ClockSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +2 -2
- data/docs/api/Brut/SpecSupport/E2eSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +2 -2
- data/docs/api/Brut/SpecSupport/FlashSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +2 -2
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +2 -2
- data/docs/api/Brut/SpecSupport/Matchers.html +2 -2
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +2 -2
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +2 -2
- data/docs/api/Brut/SpecSupport/SessionSupport.html +2 -2
- data/docs/api/Brut/SpecSupport.html +2 -2
- data/docs/api/Brut/TUI/AnsiEscapeCode/Mod.html +409 -0
- data/docs/api/Brut/TUI/AnsiEscapeCode.html +426 -0
- data/docs/api/Brut/TUI/EventLoop/Deque.html +531 -0
- data/docs/api/Brut/TUI/EventLoop.html +676 -0
- data/docs/api/Brut/TUI/Events/BaseEvent.html +449 -0
- data/docs/api/Brut/TUI/Events/EventBus.html +485 -0
- data/docs/api/Brut/TUI/Events/EventLoopStarted.html +211 -0
- data/docs/api/Brut/TUI/Events/Exception.html +523 -0
- data/docs/api/Brut/TUI/Events/Tick.html +294 -0
- data/docs/api/Brut/TUI/Events.html +131 -0
- data/docs/api/Brut/TUI/MarkupString.html +537 -0
- data/docs/api/Brut/TUI/Script/BlockStep.html +300 -0
- data/docs/api/Brut/TUI/Script/Events/CommandExecutionFailed.html +252 -0
- data/docs/api/Brut/TUI/Script/Events/CommandExecutionSucceeded.html +163 -0
- data/docs/api/Brut/TUI/Script/Events/CommandStdErr.html +163 -0
- data/docs/api/Brut/TUI/Script/Events/CommandStdOut.html +300 -0
- data/docs/api/Brut/TUI/Script/Events/ExecutingCommand.html +298 -0
- data/docs/api/Brut/TUI/Script/Events/Message.html +345 -0
- data/docs/api/Brut/TUI/Script/Events/PhaseCompleted.html +229 -0
- data/docs/api/Brut/TUI/Script/Events/PhaseStarted.html +350 -0
- data/docs/api/Brut/TUI/Script/Events/ScriptCompleted.html +282 -0
- data/docs/api/Brut/TUI/Script/Events/ScriptStarted.html +343 -0
- data/docs/api/Brut/TUI/Script/Events/StepCompleted.html +163 -0
- data/docs/api/Brut/TUI/Script/Events/StepStarted.html +346 -0
- data/docs/api/Brut/TUI/Script/Events.html +115 -0
- data/docs/api/Brut/TUI/Script/ExecStep/ProcessStatusFailed.html +210 -0
- data/docs/api/Brut/TUI/Script/ExecStep.html +493 -0
- data/docs/api/Brut/TUI/Script/LoggingSubscriber.html +914 -0
- data/docs/api/Brut/TUI/Script/PutsSubscriber.html +783 -0
- data/docs/api/Brut/TUI/Script/Step.html +313 -0
- data/docs/api/Brut/TUI/Script.html +1250 -0
- data/docs/api/Brut/TUI/Terminal.html +593 -0
- data/docs/api/Brut/TUI/TerminalTheme.html +1403 -0
- data/docs/api/Brut/TUI/Themes/Dark.html +706 -0
- data/docs/api/Brut/TUI/Themes/Light.html +804 -0
- data/docs/api/Brut/TUI/Themes/None.html +218 -0
- data/docs/api/Brut/TUI/Themes.html +115 -0
- data/docs/api/Brut/TUI.html +129 -0
- data/docs/api/Brut.html +4 -4
- data/docs/api/Clock.html +2 -2
- data/docs/api/ModuleName.html +2 -2
- data/docs/api/RichString.html +2 -2
- data/docs/api/SemanticLogger/Appender/Async.html +2 -2
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +2 -2
- data/docs/api/Sequel/Extensions/BrutMigrations.html +2 -2
- data/docs/api/Sequel/Extensions.html +2 -2
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +2 -2
- data/docs/api/Sequel/Plugins/CreatedAt.html +2 -2
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +2 -2
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +2 -2
- data/docs/api/Sequel/Plugins/ExternalId.html +2 -2
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +2 -2
- data/docs/api/Sequel/Plugins/FindBang.html +2 -2
- data/docs/api/Sequel/Plugins.html +2 -2
- data/docs/api/Sequel.html +2 -2
- data/docs/api/_index.html +247 -2
- data/docs/api/class_list.html +1 -1
- data/docs/api/file.README.html +2 -2
- data/docs/api/index.html +2 -2
- data/docs/api/method_list.html +1551 -431
- data/docs/api/top-level-namespace.html +2 -2
- data/docs/assets/{app.Dm7v_ouO.js → app.B8jAEB7R.js} +1 -1
- data/docs/assets/chunks/@localSearchIndexroot.DJ8mocCj.js +1 -0
- data/docs/assets/chunks/{VPLocalSearchBox.DQK6jQou.js → VPLocalSearchBox.gF-Po_fz.js} +1 -1
- data/docs/assets/chunks/{theme.BuExsdM9.js → theme.BjPAOJkz.js} +2 -2
- data/docs/assets/{components.md.Dfd3w6UW.js → components.md.Ber8UBM0.js} +3 -3
- data/docs/assets/{configuration.md.DTYoV2Ea.js → configuration.md.DrJ6YVoZ.js} +1 -1
- data/docs/assets/{deployment.md.C1u5ep0g.js → deployment.md.CHTx2eTR.js} +10 -3
- data/docs/assets/{deployment.md.C1u5ep0g.lean.js → deployment.md.CHTx2eTR.lean.js} +1 -1
- data/docs/assets/{forms.md.DEkmJUvb.js → forms.md.RK0zkhm0.js} +2 -2
- data/docs/assets/{forms.md.DEkmJUvb.lean.js → forms.md.RK0zkhm0.lean.js} +1 -1
- data/docs/assets/{getting-started.md.DO-4eoGW.js → getting-started.md.CGJ44juQ.js} +2 -2
- data/docs/assets/jobs.md.Bi3qb3v6.js +25 -0
- data/docs/assets/jobs.md.Bi3qb3v6.lean.js +1 -0
- data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.js +12 -0
- data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.lean.js +1 -0
- data/docs/assets/roadmap.md.DqC1Y7Zt.js +1 -0
- data/docs/assets/{roadmap.md.CJsbUmK_.lean.js → roadmap.md.DqC1Y7Zt.lean.js} +1 -1
- data/docs/assets/{tutorials_02-dialog.md.D2vSjDVf.js → tutorials_02-dialog.md.DE5WfCXI.js} +1 -1
- data/docs/assets.html +4 -4
- data/docs/brut-js/api/AjaxSubmit.html +1 -1
- data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
- data/docs/brut-js/api/Autosubmit.html +1 -1
- data/docs/brut-js/api/Autosubmit.js.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
- data/docs/brut-js/api/BrutCustomElements.html +1 -1
- data/docs/brut-js/api/BufferedLogger.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
- data/docs/brut-js/api/Form.html +1 -1
- data/docs/brut-js/api/Form.js.html +1 -1
- data/docs/brut-js/api/I18nTranslation.html +1 -1
- data/docs/brut-js/api/I18nTranslation.js.html +1 -1
- data/docs/brut-js/api/LocaleDetection.html +1 -1
- data/docs/brut-js/api/LocaleDetection.js.html +1 -1
- data/docs/brut-js/api/Logger.html +1 -1
- data/docs/brut-js/api/Logger.js.html +1 -1
- data/docs/brut-js/api/Message.html +1 -1
- data/docs/brut-js/api/Message.js.html +1 -1
- data/docs/brut-js/api/PrefixedLogger.html +1 -1
- data/docs/brut-js/api/RichString.html +1 -1
- data/docs/brut-js/api/RichString.js.html +1 -1
- data/docs/brut-js/api/Tabs.html +1 -1
- data/docs/brut-js/api/Tabs.js.html +1 -1
- data/docs/brut-js/api/Toast.html +1 -1
- data/docs/brut-js/api/Toast.js.html +1 -1
- data/docs/brut-js/api/Tracing.html +1 -1
- data/docs/brut-js/api/Tracing.js.html +1 -1
- data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
- data/docs/brut-js/api/external-Performance.html +1 -1
- data/docs/brut-js/api/external-Promise.html +1 -1
- data/docs/brut-js/api/external-ValidityState.html +1 -1
- data/docs/brut-js/api/external-Window.html +1 -1
- data/docs/brut-js/api/external-fetch.html +1 -1
- data/docs/brut-js/api/global.html +1 -1
- data/docs/brut-js/api/index.html +1 -1
- data/docs/brut-js/api/index.js.html +1 -1
- data/docs/brut-js/api/module-testing.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
- data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
- data/docs/brut-js/api/testing.DOMCreator.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
- data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
- data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
- data/docs/brut-js/api/testing_index.js.html +1 -1
- data/docs/brut-js.html +4 -4
- data/docs/business-logic.html +4 -4
- data/docs/cli.html +4 -4
- data/docs/components.html +8 -8
- data/docs/configuration.html +5 -5
- data/docs/css.html +4 -4
- data/docs/custom-element-tests.html +4 -4
- data/docs/database-access.html +4 -4
- data/docs/database-schema.html +4 -4
- data/docs/deployment.html +13 -6
- data/docs/dev-environment.html +4 -4
- data/docs/dir-structure.html +4 -4
- data/docs/doc-conventions.html +4 -4
- data/docs/end-to-end-tests.html +4 -4
- data/docs/features.html +4 -4
- data/docs/flash-and-session.html +4 -4
- data/docs/form-constraints.html +4 -4
- data/docs/forms.html +6 -6
- data/docs/getting-started.html +7 -7
- data/docs/handlers.html +4 -4
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +4 -4
- data/docs/i18n.html +4 -4
- data/docs/index.html +3 -3
- data/docs/instrumentation.html +4 -4
- data/docs/javascript.html +4 -4
- data/docs/jobs.html +29 -5
- data/docs/keyword-injection.html +4 -4
- data/docs/layouts.html +4 -4
- data/docs/lsp.html +4 -4
- data/docs/markdown-examples.html +4 -4
- data/docs/middleware.html +4 -4
- data/docs/overview.html +4 -4
- data/docs/pages.html +4 -4
- data/docs/recipes/alternate-layouts.html +5 -5
- data/docs/recipes/authentication.html +5 -5
- data/docs/recipes/custom-flash.html +5 -5
- data/docs/recipes/dev-env-secrets.html +40 -0
- data/docs/recipes/form-errors.html +5 -5
- data/docs/recipes/indexed-forms.html +5 -5
- data/docs/recipes/migrations.html +5 -5
- data/docs/recipes/text-field-component.html +5 -5
- data/docs/roadmap.html +5 -5
- data/docs/routes.html +4 -4
- data/docs/security.html +4 -4
- data/docs/seed-data.html +4 -4
- data/docs/space-time-continuum.html +4 -4
- data/docs/tutorial.html +4 -4
- data/docs/tutorials/01-intro.html +4 -4
- data/docs/tutorials/02-dialog.html +5 -5
- data/docs/unit-tests.html +4 -4
- data/docs/why.html +4 -4
- data/lib/brut/cli/apps/build_assets.rb +1 -1
- data/lib/brut/cli/apps/heroku_container_based_deploy.rb +1 -1
- data/lib/brut/cli/apps/test.rb +6 -4
- data/lib/brut/tui/ansi_escape_code.rb +104 -0
- data/lib/brut/tui/event_loop.rb +168 -0
- data/lib/brut/tui/events/base_event.rb +29 -0
- data/lib/brut/tui/events/event_bus.rb +73 -0
- data/lib/brut/tui/events/event_loop_started.rb +5 -0
- data/lib/brut/tui/events/exception.rb +24 -0
- data/lib/brut/tui/events/tick.rb +12 -0
- data/lib/brut/tui/events.rb +7 -0
- data/lib/brut/tui/markup_string.rb +68 -0
- data/lib/brut/tui/script/block_step.rb +17 -0
- data/lib/brut/tui/script/events/command_execution_failed.rb +4 -0
- data/lib/brut/tui/script/events/command_execution_succeeded.rb +3 -0
- data/lib/brut/tui/script/events/command_std_err.rb +3 -0
- data/lib/brut/tui/script/events/command_std_out.rb +13 -0
- data/lib/brut/tui/script/events/executing_command.rb +12 -0
- data/lib/brut/tui/script/events/message.rb +15 -0
- data/lib/brut/tui/script/events/phase_completed.rb +4 -0
- data/lib/brut/tui/script/events/phase_started.rb +14 -0
- data/lib/brut/tui/script/events/script_completed.rb +5 -0
- data/lib/brut/tui/script/events/script_started.rb +12 -0
- data/lib/brut/tui/script/events/step_completed.rb +3 -0
- data/lib/brut/tui/script/events/step_started.rb +12 -0
- data/lib/brut/tui/script/events.rb +14 -0
- data/lib/brut/tui/script/exec_step.rb +60 -0
- data/lib/brut/tui/script/logging_subscriber.rb +98 -0
- data/lib/brut/tui/script/puts_subscriber.rb +109 -0
- data/lib/brut/tui/script/step.rb +13 -0
- data/lib/brut/tui/script.rb +211 -0
- data/lib/brut/tui/terminal.rb +74 -0
- data/lib/brut/tui/terminal_theme.rb +140 -0
- data/lib/brut/tui/themes/dark.rb +14 -0
- data/lib/brut/tui/themes/light.rb +17 -0
- data/lib/brut/tui/themes/none.rb +9 -0
- data/lib/brut/tui/themes.rb +5 -0
- data/lib/brut/tui.rb +15 -0
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +1 -0
- data/mkbrut/Gemfile.lock +2 -1
- data/mkbrut/lib/mkbrut/version.rb +1 -1
- data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +9 -7
- data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +2 -2
- data/specs/brut/tui/ansi_escape_code.spec.rb +30 -0
- data/specs/brut/tui/event_loop.spec.rb +70 -0
- data/specs/brut/tui/events/base_event.spec.rb +26 -0
- data/specs/brut/tui/events/event_bus.spec.rb +141 -0
- data/specs/brut/tui/events/exception.spec.rb +19 -0
- data/specs/brut/tui/events/test_event.rb +5 -0
- data/specs/spec_helper.rb +4 -0
- metadata +131 -21
- data/docs/assets/chunks/@localSearchIndexroot.BiNFswvo.js +0 -1
- data/docs/assets/jobs.md.Bc7Y1YpK.js +0 -1
- data/docs/assets/jobs.md.Bc7Y1YpK.lean.js +0 -1
- data/docs/assets/roadmap.md.CJsbUmK_.js +0 -1
- /data/docs/assets/{components.md.Dfd3w6UW.lean.js → components.md.Ber8UBM0.lean.js} +0 -0
- /data/docs/assets/{configuration.md.DTYoV2Ea.lean.js → configuration.md.DrJ6YVoZ.lean.js} +0 -0
- /data/docs/assets/{getting-started.md.DO-4eoGW.lean.js → getting-started.md.CGJ44juQ.lean.js} +0 -0
- /data/docs/assets/{tutorials_02-dialog.md.D2vSjDVf.lean.js → tutorials_02-dialog.md.DE5WfCXI.lean.js} +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Fired when a step is about to execute
|
|
2
|
+
class Brut::TUI::Script::Events::StepStarted < Brut::TUI::Events::BaseEvent
|
|
3
|
+
def initialize(step:)
|
|
4
|
+
@step = step
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Adds `description` to the keyword arguments
|
|
8
|
+
def deconstruct_keys(keys=nil)
|
|
9
|
+
super.merge({ description: @step.description })
|
|
10
|
+
end
|
|
11
|
+
def to_s = @description
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Brut::TUI::Script::Events
|
|
2
|
+
autoload :ScriptStarted , "brut/tui/script/events/script_started"
|
|
3
|
+
autoload :ScriptCompleted , "brut/tui/script/events/script_completed"
|
|
4
|
+
autoload :PhaseStarted , "brut/tui/script/events/phase_started"
|
|
5
|
+
autoload :PhaseCompleted , "brut/tui/script/events/phase_completed"
|
|
6
|
+
autoload :StepStarted , "brut/tui/script/events/step_started"
|
|
7
|
+
autoload :StepCompleted , "brut/tui/script/events/step_completed"
|
|
8
|
+
autoload :ExecutingCommand , "brut/tui/script/events/executing_command"
|
|
9
|
+
autoload :CommandStdOut, "brut/tui/script/events/command_std_out"
|
|
10
|
+
autoload :CommandStdErr, "brut/tui/script/events/command_std_err"
|
|
11
|
+
autoload :CommandExecutionSucceeded , "brut/tui/script/events/command_execution_succeeded"
|
|
12
|
+
autoload :CommandExecutionFailed , "brut/tui/script/events/command_execution_failed"
|
|
13
|
+
autoload :Message , "brut/tui/script/events/message"
|
|
14
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
# A step whose behavior is to run a command as a child process.
|
|
4
|
+
# Fires {Brut::TUI::Script::Events::StepStarted} then
|
|
5
|
+
# {Brut::TUI::Script::Events::ExecutingCommand} before the command is run.
|
|
6
|
+
# The command is then run with `Open3.popen3` and the stderr and stdout
|
|
7
|
+
# are streamed as output is available. Each block of bytes read
|
|
8
|
+
# generates a {Brut::TUI::Script::Events::CommandStdOut}
|
|
9
|
+
# or a {Brut::TUI::Script::Events::CommandStdErr} event with
|
|
10
|
+
# the bytes reads.
|
|
11
|
+
#
|
|
12
|
+
# If the command exited nonzero,
|
|
13
|
+
# {Brut::TUI::Script::Events::CommandExecutionSucceeded} is fired,
|
|
14
|
+
# otherwise {Brut::TUI::Script::Events::CommandExecutionFailed} is fired,
|
|
15
|
+
# Either way, {Brut::TUI::Script::Events::StepCompleted} is fired
|
|
16
|
+
# *unless* there is an unhandled exception.
|
|
17
|
+
class Brut::TUI::Script::ExecStep < Brut::TUI::Script::Step
|
|
18
|
+
|
|
19
|
+
attr_reader :command
|
|
20
|
+
def initialize(event_loop, description, command:)
|
|
21
|
+
super(event_loop, description)
|
|
22
|
+
@command = command
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def deconstruct_keys(keys=nil)
|
|
26
|
+
super.deconstruct_keys(keys).merge({ command: @command, strip_ansi: false })
|
|
27
|
+
end
|
|
28
|
+
def run!
|
|
29
|
+
event_loop << Events::StepStarted.new(step: self)
|
|
30
|
+
event_loop << Events::ExecutingCommand.new(step: self)
|
|
31
|
+
|
|
32
|
+
wait_thread = Open3.popen3(*@command) do |_stdin,stdout,stderr,wait_thread|
|
|
33
|
+
o = stdout.read_nonblock(10, exception: false)
|
|
34
|
+
e = stderr.read_nonblock(10, exception: false)
|
|
35
|
+
while o || e
|
|
36
|
+
if o
|
|
37
|
+
if o != :wait_readable
|
|
38
|
+
event_loop << Events::CommandStdOut.new(step: self, output: o)
|
|
39
|
+
end
|
|
40
|
+
o = stdout.read_nonblock(10, exception: false)
|
|
41
|
+
end
|
|
42
|
+
if e
|
|
43
|
+
if e != :wait_readable
|
|
44
|
+
event_loop << Events::CommandStdErr.new(step: self, output: e)
|
|
45
|
+
end
|
|
46
|
+
e = stderr.read_nonblock(10, exception: false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
wait_thread
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if wait_thread.value.success?
|
|
53
|
+
event_loop << Events::CommandExecutionSucceeded.new(step: self)
|
|
54
|
+
else
|
|
55
|
+
event_loop << Events::CommandExecutionFailed.new(step: self)
|
|
56
|
+
end
|
|
57
|
+
event_loop << Events::StepCompleted.new(step: self)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# A subscriber that uses Ruby's logger to log messages.
|
|
2
|
+
# The purpose of this is to ensure that every bit of available information about
|
|
3
|
+
# a `Script` is placed somewhere for later review. This allows any output to the terminal
|
|
4
|
+
# to be more brief or user-friendly without losing information.
|
|
5
|
+
class Brut::TUI::Script::LoggingSubscriber
|
|
6
|
+
def initialize(progname, logfile:)
|
|
7
|
+
@logger = Logger.new(logfile, progname:)
|
|
8
|
+
@logger.formatter = proc { |severity, time, progname, msg|
|
|
9
|
+
"#{time} - [ #{progname} ] #{severity}: #{strip_ansi(msg)}\n"
|
|
10
|
+
}
|
|
11
|
+
@stdout = {}
|
|
12
|
+
@stderr = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_event_loop_started(event)
|
|
16
|
+
@logger.debug("TUI Event loop started")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_exception(event)
|
|
20
|
+
@logger.error("#{event.exit? ? 'FATAL' : 'non-fatal'} ExceptionEvent: #{event.exception.class}: #{event.exception.message}\n #{event.exception.backtrace.join("\n ")}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_executing_command(command:)
|
|
24
|
+
@logger.info("Executing command `#{command}`")
|
|
25
|
+
@stdout[command] = ''
|
|
26
|
+
@stderr[command] = ''
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_command_std_out(command:, output:)
|
|
30
|
+
@stdout[command] << output
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_command_std_err(command:, output:)
|
|
34
|
+
@stderr[command] << output
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def on_command_execution_succeeded(command:)
|
|
38
|
+
if !@stdout[command].empty?
|
|
39
|
+
@logger.info("\n#{strip_ansi(@stdout[command])}")
|
|
40
|
+
end
|
|
41
|
+
if !@stderr[command].empty?
|
|
42
|
+
@logger.warn("\n#{strip_ansi(@stderr[command])}")
|
|
43
|
+
end
|
|
44
|
+
@logger.info("Command `#{command}` executed successfully.")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_command_execution_failed(command:)
|
|
48
|
+
if !@stdout[command].empty?
|
|
49
|
+
@logger.info("\n#{strip_ansi(@stdout[command])}")
|
|
50
|
+
end
|
|
51
|
+
if !@stderr[command].empty?
|
|
52
|
+
@logger.warn("\n#{strip_ansi(@stderr[command])}")
|
|
53
|
+
end
|
|
54
|
+
@logger.error("Command `#{command}` failed.")
|
|
55
|
+
raise "DOH"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def on_model_updated(*)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def on_tick(*)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def on_script_completed(*)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_script_started(*)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def on_any_event(event)
|
|
71
|
+
case event
|
|
72
|
+
in { description: }
|
|
73
|
+
@logger.info(description)
|
|
74
|
+
in { message:, type: :warning }
|
|
75
|
+
@logger.warn(message)
|
|
76
|
+
in { message:, type: :error }
|
|
77
|
+
@logger.error(message)
|
|
78
|
+
in { message: }
|
|
79
|
+
@logger.info(message)
|
|
80
|
+
in { handler_method_name: }
|
|
81
|
+
@logger.info(handler_method_name)
|
|
82
|
+
else
|
|
83
|
+
@logger.info(event.to_s)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
ANSI_ESCAPE = %r{
|
|
89
|
+
\e\[[@-Z\\-_] | # 1-byte sequences
|
|
90
|
+
\e\[[0-?]*[ -\/]*[@-~] | # CSI sequences
|
|
91
|
+
\e\][^\a]*\a | # OSC sequences
|
|
92
|
+
\eP[^\a]*\a | # DCS
|
|
93
|
+
\e_[^\a]*\a | # APC
|
|
94
|
+
\e\^[^\a]*\a # PM
|
|
95
|
+
}x
|
|
96
|
+
|
|
97
|
+
def strip_ansi(string) = string.gsub(ANSI_ESCAPE, '')
|
|
98
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# A subscriber that attempts to provide a user-friend, brief, summarized, fancy
|
|
2
|
+
# output for the script that is running. It is intended to show you
|
|
3
|
+
# what's happening at a high level, but deferring all details (like child process
|
|
4
|
+
# output) to the {Brut::TUI::Script::LoggingSubscriber}'s log file.
|
|
5
|
+
class Brut::TUI::Script::PutsSubscriber
|
|
6
|
+
def initialize(progname, terminal:, theme:, stdout: false, stderr: false)
|
|
7
|
+
@progname = progname
|
|
8
|
+
@terminal = terminal
|
|
9
|
+
@theme = theme
|
|
10
|
+
@prefix_recent = false
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@stderr = stderr
|
|
13
|
+
@stdout_buffer = {}
|
|
14
|
+
@stderr_buffer = {}
|
|
15
|
+
@step_indent = "Phase 1/1 ".length
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def on_phase_started(description:, step_number:, total_steps:)
|
|
19
|
+
total_format_string = if total_steps < 10
|
|
20
|
+
"%1d"
|
|
21
|
+
else
|
|
22
|
+
"%2d"
|
|
23
|
+
end
|
|
24
|
+
preamble = sprintf("Phase %d/#{total_format_string}", step_number, total_steps)
|
|
25
|
+
@step_indent = preamble.length + 1
|
|
26
|
+
println @theme.reset + @theme.bold + preamble + @theme.reset + " " + @theme.with_markup(description, text: :heading)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_step_started(description:)
|
|
30
|
+
println @theme.with_markup(description), step_indent: true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_executing_command(command:)
|
|
34
|
+
println @theme.with_markup("> `#{command}`"), step_indent: true
|
|
35
|
+
$stdout.print @theme.reset
|
|
36
|
+
@stdout_buffer[command] = ""
|
|
37
|
+
@stderr_buffer[command] = ""
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_command_execution_failed(command:)
|
|
41
|
+
println @theme.with_markup("FAILED", text: :error), step_indent: true
|
|
42
|
+
if !@stdout
|
|
43
|
+
println ""
|
|
44
|
+
println @stdout_buffer[command] + "\n"
|
|
45
|
+
end
|
|
46
|
+
if !@stderr
|
|
47
|
+
println ""
|
|
48
|
+
println @theme.warning + @stderr_buffer[command]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def on_exception(exception:)
|
|
54
|
+
println @theme.error + "Exception: #{exception.class}: #{exception.message}\n #{exception.backtrace.join("\n ")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def on_command_std_out(output:, command:)
|
|
58
|
+
if @stdout
|
|
59
|
+
@prefix_recent = false
|
|
60
|
+
$stdout.print @theme.normal + @theme.weak + output + @theme.reset
|
|
61
|
+
$stdout.flush
|
|
62
|
+
else
|
|
63
|
+
@stdout_buffer[command] << output
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_command_std_err(output:, command:)
|
|
68
|
+
if @stderr
|
|
69
|
+
@prefix_recent = false
|
|
70
|
+
$stdout.print @theme.warning + output + @theme.reset
|
|
71
|
+
$stdout.flush
|
|
72
|
+
else
|
|
73
|
+
@stderr_buffer[command] << output
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def on_command_execution_succeeded(command:)
|
|
78
|
+
println @theme.with_markup("✅", text: :success), step_indent: true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def on_message(message:, type:)
|
|
82
|
+
if type == :done
|
|
83
|
+
println @theme.with_markup(message, text: :success)
|
|
84
|
+
else
|
|
85
|
+
println @theme.with_markup(message, text: type), step_indent: true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def prefix_string = "[ #{@progname} ] "
|
|
93
|
+
|
|
94
|
+
def step_indent = @step_indent
|
|
95
|
+
|
|
96
|
+
def println(message, step_indent: false)
|
|
97
|
+
@terminal.io.puts prefix + (step_indent ? " " * self.step_indent : "") + message
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def prefix
|
|
101
|
+
if @prefix_recent
|
|
102
|
+
@theme.italic + @theme.weak + prefix_string + @theme.weak_off + @theme.italic_off
|
|
103
|
+
else
|
|
104
|
+
@prefix_recent = true
|
|
105
|
+
prefix_string
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Base class for specific types of steps
|
|
2
|
+
class Brut::TUI::Script::Step
|
|
3
|
+
|
|
4
|
+
Events = Brut::TUI::Script::Events
|
|
5
|
+
|
|
6
|
+
attr_reader :description
|
|
7
|
+
def initialize(event_loop, description, exec: nil, &block)
|
|
8
|
+
@event_loop = event_loop
|
|
9
|
+
@description = description
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private attr_reader :event_loop
|
|
13
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
# A TUI script is a set of steps that are executed in order. Steps
|
|
5
|
+
# can be grouped int phases, and each step can either shell out
|
|
6
|
+
# to an external command or execute Ruby code.
|
|
7
|
+
#
|
|
8
|
+
# You are intended to subclass this class and implement `#execute`, which can use
|
|
9
|
+
# the various methods described here to describe the script. In the context of a
|
|
10
|
+
# Brut CLI, your subclass would be used inside the {Brut::CLI::Command#execute} method, where
|
|
11
|
+
# it would call `#run!` (not `#execute`).
|
|
12
|
+
#
|
|
13
|
+
# Inside `#execute` you should call `#phase` for each logical phase/grouping of steps. There must be at least
|
|
14
|
+
# one phase. Inside each `phase` you should call `#step` one or more times. When a phase is actually executed,
|
|
15
|
+
# it's contents are executed, so anything inside the block will be called. Intermediate variables to allow `step`s to
|
|
16
|
+
# communicate will work.
|
|
17
|
+
#
|
|
18
|
+
# After all phases, call `done` with a success message.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# def execute
|
|
22
|
+
# phase "Set up test data" do
|
|
23
|
+
# step "Initializing database",
|
|
24
|
+
# exec: "bin/db rebuild -e test"
|
|
25
|
+
# step "Loading data" do
|
|
26
|
+
# MyData.each do |data|
|
|
27
|
+
# notify "Inserting #{data}"
|
|
28
|
+
# data.insert!
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# phase "Checking data integrity" do
|
|
33
|
+
# problems = []
|
|
34
|
+
# step "Analyzing data" do
|
|
35
|
+
# MyData.each do |data|
|
|
36
|
+
# if !data.analyze
|
|
37
|
+
# problems << data
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
# step "Checking problems" do
|
|
42
|
+
# abort = false
|
|
43
|
+
# problems.each do |problem|
|
|
44
|
+
# if problem.warning?
|
|
45
|
+
# warning "#{problem} may be an issue, but we can proceed"
|
|
46
|
+
# else
|
|
47
|
+
# error "#{problem} will prevent our test from working"
|
|
48
|
+
# abort = true
|
|
49
|
+
# end
|
|
50
|
+
# end
|
|
51
|
+
# if abort
|
|
52
|
+
# raise "Problems prevented script from working"
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# done "All ready to go!"
|
|
58
|
+
# end
|
|
59
|
+
class Brut::TUI::Script
|
|
60
|
+
|
|
61
|
+
autoload(:Step , "brut/tui/script/step")
|
|
62
|
+
autoload(:BlockStep , "brut/tui/script/block_step")
|
|
63
|
+
autoload(:ExecStep , "brut/tui/script/exec_step")
|
|
64
|
+
autoload(:LoggingSubscriber , "brut/tui/script/logging_subscriber")
|
|
65
|
+
autoload(:PutsSubscriber , "brut/tui/script/puts_subscriber")
|
|
66
|
+
autoload(:Events , "brut/tui/script/events")
|
|
67
|
+
|
|
68
|
+
# Return the basename of a log file unique to this script. This will use
|
|
69
|
+
# the subclass name to come up with a reasonable name, however your
|
|
70
|
+
# class can override this.
|
|
71
|
+
def self.log_filename
|
|
72
|
+
name = self.name.gsub(/Script$/,"").gsub(/::$/,"").split("::").last
|
|
73
|
+
name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Create the script. By default, two subscribers are set up: {Brut::TUI::Script::LoggingSubscriber}
|
|
77
|
+
# and {Brut::TUI::Script::PutsSubscriber}. The logging subscriber will output a detailed
|
|
78
|
+
# log into `logs/` using the file named by {.log_filename}. The `PutsSubscriber` will
|
|
79
|
+
# output ANSI-Fancy messages as the script proceeds.
|
|
80
|
+
#
|
|
81
|
+
# @param root_dir [String|Pathname] path to the root of the Brut project.
|
|
82
|
+
# @param ansi [true|false] if true, the {Brut::TUI::Script::PutsSubscriber} will use
|
|
83
|
+
# ANSI escape codes to format the output. If false, it won't.
|
|
84
|
+
def initialize(root_dir: nil, ansi: true)
|
|
85
|
+
@root_dir = root_dir ? Pathname(root_dir).expand_path : nil
|
|
86
|
+
logs_dir = if @root_dir
|
|
87
|
+
@root_dir / "logs"
|
|
88
|
+
else
|
|
89
|
+
Pathame(".") / "logs"
|
|
90
|
+
end
|
|
91
|
+
FileUtils.mkdir_p(logs_dir)
|
|
92
|
+
@event_loop = Brut::TUI::EventLoop.new
|
|
93
|
+
|
|
94
|
+
logging_subscriber = LoggingSubscriber.new($0, logfile: logs_dir / "#{self.class.log_filename}.log")
|
|
95
|
+
terminal = Brut::TUI::Terminal.new
|
|
96
|
+
theme = if ansi
|
|
97
|
+
Brut::TUI::TerminalTheme.based_on_background(terminal)
|
|
98
|
+
else
|
|
99
|
+
Brut::TUI::Themes::None.new(terminal)
|
|
100
|
+
end
|
|
101
|
+
puts_subscriber = Brut::TUI::Script::PutsSubscriber.new($0, terminal:, theme:)
|
|
102
|
+
|
|
103
|
+
@event_loop.subscribe(Brut::TUI::Events::EventLoopStarted,self)
|
|
104
|
+
@event_loop.subscribe_to_all(logging_subscriber)
|
|
105
|
+
@event_loop.subscribe_to_all(puts_subscriber)
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @!visibility private
|
|
110
|
+
def on_event_loop_started(event)
|
|
111
|
+
Thread.new do
|
|
112
|
+
begin
|
|
113
|
+
@phases = []
|
|
114
|
+
self.execute
|
|
115
|
+
@event_loop << Events::ScriptStarted.new(phases: @phases)
|
|
116
|
+
@phases.each_with_index do |(name,block), index|
|
|
117
|
+
step_number = index + 1
|
|
118
|
+
@event_loop << Events::PhaseStarted.new(name, step_number: step_number, total_steps: @phases.length)
|
|
119
|
+
block.()
|
|
120
|
+
@event_loop << Events::PhaseCompleted.new(name, step_number: step_number, total_steps: @phases.length)
|
|
121
|
+
end
|
|
122
|
+
@event_loop << Events::Message.new(message: @done_message || "Script completed successfully", type: :done)
|
|
123
|
+
rescue => ex
|
|
124
|
+
@event_loop << Brut::TUI::Events::Exception.new(ex)
|
|
125
|
+
end
|
|
126
|
+
@event_loop << Events::ScriptCompleted.new
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Entry point for a script. This method starts the event loop.
|
|
131
|
+
def run!
|
|
132
|
+
if !self.methods.include?(:execute)
|
|
133
|
+
raise "You must implement the execute method in your Brut::TUI::Script subclass"
|
|
134
|
+
end
|
|
135
|
+
@event_loop.run
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Create a new phase for the script. A phase is basically a named group
|
|
140
|
+
# of steps. The block is not executed immediately, so you may not pass
|
|
141
|
+
# data from one phase to another.
|
|
142
|
+
#
|
|
143
|
+
# @param name [String] The name of the phase
|
|
144
|
+
# @param block [Proc] The block to execute for the phase
|
|
145
|
+
def phase(name, &block)
|
|
146
|
+
@phases << [ name, block ]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A step to run inside a phase. Steps within phases are executed immediately once the phase
|
|
150
|
+
# has started, so they can pass data to one anther.
|
|
151
|
+
#
|
|
152
|
+
# @param description [String] Message to show the user about this step.
|
|
153
|
+
# @param exec [String|nil] if non-nil, this step will execute this as a command. A block given is ignored.
|
|
154
|
+
# If `nil`, a block should be given that contains the step's code.
|
|
155
|
+
# @yield if `exec` is `nil`, this block will be executed for the step
|
|
156
|
+
def step(description, exec: nil, &block)
|
|
157
|
+
step = if exec.nil?
|
|
158
|
+
BlockStep.new(@event_loop, description, &block)
|
|
159
|
+
else
|
|
160
|
+
ExecStep.new(@event_loop, description, command: exec)
|
|
161
|
+
end
|
|
162
|
+
step.run!
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Notify the user of an event
|
|
166
|
+
#
|
|
167
|
+
# @param message [String] Message to show
|
|
168
|
+
def notify(message)
|
|
169
|
+
@event_loop << Events::Message.new(message:, type: :notification)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Warn the user of an event
|
|
173
|
+
#
|
|
174
|
+
# @param message [String] Message to show
|
|
175
|
+
def warning(message)
|
|
176
|
+
@event_loop << Events::Message.new(message:, type: :warning)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Let the user know something succeeded
|
|
180
|
+
#
|
|
181
|
+
# @param message [String] Message to show
|
|
182
|
+
def success(message)
|
|
183
|
+
@event_loop << Events::Message.new(message:, type: :success)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Message to show if the script completes successful. Should only be called once
|
|
187
|
+
# and not inside a phase or step.
|
|
188
|
+
#
|
|
189
|
+
# @param message [String] Message to show
|
|
190
|
+
def done(message)
|
|
191
|
+
@done_message = message
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Let the user know there was an error. Note that this will not stop
|
|
195
|
+
# the script. Raise an exception to do that.
|
|
196
|
+
#
|
|
197
|
+
# @param message [String] Message to show
|
|
198
|
+
def error(message)
|
|
199
|
+
@event_loop << Events::Message.new(message:, type: :error)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Wrap a fully-qualified filename in code markup and trim the path to only
|
|
203
|
+
# show the path relative to the root dir. This is much friendlier than showing a
|
|
204
|
+
# long expanded path.
|
|
205
|
+
#
|
|
206
|
+
# @param path [String|Pathname] a fully-qualified path to something inside your Brut app.
|
|
207
|
+
def filename(path)
|
|
208
|
+
path = Pathname(path).expand_path
|
|
209
|
+
"`" + (@root_dir ? path.relative_path_from(@root_dir) : path).to_s + "`"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
require "timeout"
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
# Stores metdata about the current temrinal in use. This should provide any metadata
|
|
6
|
+
# about the terminal that can be reliably determined.
|
|
7
|
+
class Brut::TUI::Terminal
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
trap("WINCH") do
|
|
11
|
+
@winsize = IO.console.winsize
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Number of rows for the current terminal. Should be consistent even after a resize
|
|
16
|
+
def rows = winsize[0]
|
|
17
|
+
# Number of columns for the current terminal. Should be consistent even after a resize
|
|
18
|
+
def cols = winsize[1]
|
|
19
|
+
|
|
20
|
+
# The IO used for outputing information to the terminal. Prefer this over `STDOUT`.
|
|
21
|
+
def io = $stdout
|
|
22
|
+
|
|
23
|
+
# The IO to use for requesting input from the user
|
|
24
|
+
def stdin = $stdin
|
|
25
|
+
|
|
26
|
+
# Best attempt to guess the background color of the current terminal.
|
|
27
|
+
# This will not refresh if that color is changed, and its behavior will
|
|
28
|
+
# default to black if determining the color doesn't work or isn't supported.
|
|
29
|
+
def background_color
|
|
30
|
+
@bakground_color ||= begin
|
|
31
|
+
io.write "\e]11;?\a"
|
|
32
|
+
io.flush
|
|
33
|
+
|
|
34
|
+
result = nil
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
# Read the reply, which should look like:
|
|
38
|
+
# ESC ] 11 ; rgb:0000/0000/0000 BEL
|
|
39
|
+
Timeout.timeout(0.1) do
|
|
40
|
+
buf = +""
|
|
41
|
+
loop do
|
|
42
|
+
ch = stdin.getch
|
|
43
|
+
buf << ch
|
|
44
|
+
break if ch == "\a" || buf.end_with?("\e\\") # BEL or ST terminator
|
|
45
|
+
end
|
|
46
|
+
result = buf
|
|
47
|
+
end
|
|
48
|
+
rescue Timeout::Error
|
|
49
|
+
# no reply within 100ms — terminal probably doesn’t support it
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
rgb = [ 0, 0, 0 ]
|
|
53
|
+
if result
|
|
54
|
+
if result =~ /\e\]11;([^ \a\e]*)[\a\e\\]/
|
|
55
|
+
color = Regexp.last_match(1)
|
|
56
|
+
parts = color[/rgb:(.*)/, 1]&.split("/")
|
|
57
|
+
if parts
|
|
58
|
+
|
|
59
|
+
rgb = parts.map { it.to_i(16) / 0xffff.to_f * 256 }.map(&:to_i)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
rgb
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def winsize
|
|
71
|
+
@winsize ||= IO.console.winsize
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|