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.
Files changed (456) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/CHANGELOG.md +9 -3
  4. data/Gemfile.lock +44 -34
  5. data/bin/docs +7 -0
  6. data/brut-css/package-lock.json +2 -2
  7. data/brut-css/package.json +1 -1
  8. data/brut-js/package-lock.json +311 -2
  9. data/brut-js/package.json +2 -1
  10. data/brut.gemspec +2 -0
  11. data/brutrb.com/jobs.md +1 -1
  12. data/docs/404.html +2 -2
  13. data/docs/adrs.html +4 -4
  14. data/docs/ai.html +4 -4
  15. data/docs/api/Brut/BackEnd/SeedData.html +2 -2
  16. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +2 -2
  17. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +2 -2
  18. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +2 -2
  19. data/docs/api/Brut/BackEnd/Sidekiq.html +2 -2
  20. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +2 -2
  21. data/docs/api/Brut/BackEnd/Validators.html +2 -2
  22. data/docs/api/Brut/BackEnd.html +2 -2
  23. data/docs/api/Brut/CLI/App.html +2 -2
  24. data/docs/api/Brut/CLI/AppRunner.html +2 -2
  25. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
  26. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +2 -2
  27. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +3 -3
  28. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +2 -2
  29. data/docs/api/Brut/CLI/Apps/BuildAssets.html +2 -2
  30. data/docs/api/Brut/CLI/Apps/DB/Create.html +2 -2
  31. data/docs/api/Brut/CLI/Apps/DB/Drop.html +2 -2
  32. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +2 -2
  33. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +2 -2
  34. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +2 -2
  35. data/docs/api/Brut/CLI/Apps/DB/Seed.html +2 -2
  36. data/docs/api/Brut/CLI/Apps/DB/Status.html +2 -2
  37. data/docs/api/Brut/CLI/Apps/DB.html +2 -2
  38. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +2 -2
  39. data/docs/api/Brut/CLI/Apps/DeployBase.html +2 -2
  40. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +8 -6
  41. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +2 -2
  42. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +2 -2
  43. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +2 -2
  44. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +2 -2
  45. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +2 -2
  46. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +2 -2
  47. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +2 -2
  48. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +2 -2
  49. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +2 -2
  50. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
  51. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +2 -2
  52. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +2 -2
  53. data/docs/api/Brut/CLI/Apps/Scaffold.html +2 -2
  54. data/docs/api/Brut/CLI/Apps/Test/Audit.html +15 -7
  55. data/docs/api/Brut/CLI/Apps/Test/E2e.html +2 -2
  56. data/docs/api/Brut/CLI/Apps/Test/JS.html +2 -2
  57. data/docs/api/Brut/CLI/Apps/Test/Run.html +2 -2
  58. data/docs/api/Brut/CLI/Apps/Test.html +2 -2
  59. data/docs/api/Brut/CLI/Apps.html +2 -2
  60. data/docs/api/Brut/CLI/Command.html +2 -2
  61. data/docs/api/Brut/CLI/Error.html +2 -2
  62. data/docs/api/Brut/CLI/ExecutionResults/Result.html +2 -2
  63. data/docs/api/Brut/CLI/ExecutionResults.html +2 -2
  64. data/docs/api/Brut/CLI/Executor.html +2 -2
  65. data/docs/api/Brut/CLI/InvalidOption.html +2 -2
  66. data/docs/api/Brut/CLI/Options.html +2 -2
  67. data/docs/api/Brut/CLI/Output.html +2 -2
  68. data/docs/api/Brut/CLI/SystemExecError.html +2 -2
  69. data/docs/api/Brut/CLI.html +2 -2
  70. data/docs/api/Brut/FactoryBot.html +2 -2
  71. data/docs/api/Brut/Framework/App.html +2 -2
  72. data/docs/api/Brut/Framework/Config.html +2 -2
  73. data/docs/api/Brut/Framework/Container.html +11 -7
  74. data/docs/api/Brut/Framework/Error.html +2 -2
  75. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +2 -2
  76. data/docs/api/Brut/Framework/Errors/Bug.html +2 -2
  77. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +2 -2
  78. data/docs/api/Brut/Framework/Errors/MissingParameter.html +2 -2
  79. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +2 -2
  80. data/docs/api/Brut/Framework/Errors/NotFound.html +2 -2
  81. data/docs/api/Brut/Framework/Errors/NotImplemented.html +2 -2
  82. data/docs/api/Brut/Framework/Errors.html +2 -2
  83. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +2 -2
  84. data/docs/api/Brut/Framework/MCP.html +3 -3
  85. data/docs/api/Brut/Framework/ProjectEnvironment.html +2 -2
  86. data/docs/api/Brut/Framework.html +2 -2
  87. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +2 -2
  88. data/docs/api/Brut/FrontEnd/Component/Helpers.html +2 -2
  89. data/docs/api/Brut/FrontEnd/Component.html +2 -2
  90. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +2 -2
  91. data/docs/api/Brut/FrontEnd/Components/FormTag.html +2 -2
  92. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +2 -2
  93. data/docs/api/Brut/FrontEnd/Components/Input.html +2 -2
  94. data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +2 -2
  95. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +2 -2
  96. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +2 -2
  97. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +2 -2
  98. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +2 -2
  99. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +2 -2
  100. data/docs/api/Brut/FrontEnd/Components/Inputs.html +2 -2
  101. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +2 -2
  102. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +2 -2
  103. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +2 -2
  104. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +2 -2
  105. data/docs/api/Brut/FrontEnd/Components.html +2 -2
  106. data/docs/api/Brut/FrontEnd/CsrfProtector.html +2 -2
  107. data/docs/api/Brut/FrontEnd/Download.html +2 -2
  108. data/docs/api/Brut/FrontEnd/Flash.html +2 -2
  109. data/docs/api/Brut/FrontEnd/Form.html +2 -2
  110. data/docs/api/Brut/FrontEnd/Forms/Button.html +2 -2
  111. data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +2 -2
  112. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +2 -2
  113. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +2 -2
  114. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +2 -2
  115. data/docs/api/Brut/FrontEnd/Forms/Input.html +2 -2
  116. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +2 -2
  117. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +2 -2
  118. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +2 -2
  119. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +2 -2
  120. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +2 -2
  121. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +2 -2
  122. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +2 -2
  123. data/docs/api/Brut/FrontEnd/Forms.html +2 -2
  124. data/docs/api/Brut/FrontEnd/GenericResponse.html +2 -2
  125. data/docs/api/Brut/FrontEnd/Handler.html +2 -2
  126. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +2 -2
  127. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +2 -2
  128. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +2 -2
  129. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +2 -2
  130. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +2 -2
  131. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +2 -2
  132. data/docs/api/Brut/FrontEnd/Handlers.html +2 -2
  133. data/docs/api/Brut/FrontEnd/HandlingResults.html +2 -2
  134. data/docs/api/Brut/FrontEnd/HttpMethod.html +2 -2
  135. data/docs/api/Brut/FrontEnd/HttpStatus.html +2 -2
  136. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +2 -2
  137. data/docs/api/Brut/FrontEnd/Layout.html +2 -2
  138. data/docs/api/Brut/FrontEnd/Middleware.html +2 -2
  139. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +2 -2
  140. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +2 -2
  141. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +2 -2
  142. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +2 -2
  143. data/docs/api/Brut/FrontEnd/Middlewares.html +2 -2
  144. data/docs/api/Brut/FrontEnd/Page.html +2 -2
  145. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +2 -2
  146. data/docs/api/Brut/FrontEnd/Pages.html +2 -2
  147. data/docs/api/Brut/FrontEnd/RequestContext.html +2 -2
  148. data/docs/api/Brut/FrontEnd/RouteHook.html +2 -2
  149. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +2 -2
  150. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +2 -2
  151. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +2 -2
  152. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +2 -2
  153. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +2 -2
  154. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +2 -2
  155. data/docs/api/Brut/FrontEnd/RouteHooks.html +2 -2
  156. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +2 -2
  157. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +2 -2
  158. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +2 -2
  159. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +2 -2
  160. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +2 -2
  161. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +2 -2
  162. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +2 -2
  163. data/docs/api/Brut/FrontEnd/Routing/Route.html +2 -2
  164. data/docs/api/Brut/FrontEnd/Routing.html +2 -2
  165. data/docs/api/Brut/FrontEnd/Session.html +2 -2
  166. data/docs/api/Brut/FrontEnd.html +2 -2
  167. data/docs/api/Brut/I18n/BaseMethods.html +2 -2
  168. data/docs/api/Brut/I18n/ForBackEnd.html +2 -2
  169. data/docs/api/Brut/I18n/ForCLI.html +2 -2
  170. data/docs/api/Brut/I18n/ForHTML.html +2 -2
  171. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +2 -2
  172. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +2 -2
  173. data/docs/api/Brut/I18n.html +2 -2
  174. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +2 -2
  175. data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +2 -2
  176. data/docs/api/Brut/Instrumentation/Methods.html +2 -2
  177. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +2 -2
  178. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +2 -2
  179. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +2 -2
  180. data/docs/api/Brut/Instrumentation.html +2 -2
  181. data/docs/api/Brut/RubocopConfig.html +2 -2
  182. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +2 -2
  183. data/docs/api/Brut/SinatraHelpers.html +2 -2
  184. data/docs/api/Brut/SpecSupport/ClockSupport.html +2 -2
  185. data/docs/api/Brut/SpecSupport/ComponentSupport.html +2 -2
  186. data/docs/api/Brut/SpecSupport/E2ETestServer.html +2 -2
  187. data/docs/api/Brut/SpecSupport/E2eSupport.html +2 -2
  188. data/docs/api/Brut/SpecSupport/EnhancedNode.html +2 -2
  189. data/docs/api/Brut/SpecSupport/FlashSupport.html +2 -2
  190. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +2 -2
  191. data/docs/api/Brut/SpecSupport/GeneralSupport.html +2 -2
  192. data/docs/api/Brut/SpecSupport/HandlerSupport.html +2 -2
  193. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +2 -2
  194. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +2 -2
  195. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +2 -2
  196. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +2 -2
  197. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +2 -2
  198. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +2 -2
  199. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +2 -2
  200. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +2 -2
  201. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +2 -2
  202. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +2 -2
  203. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +2 -2
  204. data/docs/api/Brut/SpecSupport/Matchers.html +2 -2
  205. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +2 -2
  206. data/docs/api/Brut/SpecSupport/RSpecSetup.html +2 -2
  207. data/docs/api/Brut/SpecSupport/SessionSupport.html +2 -2
  208. data/docs/api/Brut/SpecSupport.html +2 -2
  209. data/docs/api/Brut/TUI/AnsiEscapeCode/Mod.html +409 -0
  210. data/docs/api/Brut/TUI/AnsiEscapeCode.html +426 -0
  211. data/docs/api/Brut/TUI/EventLoop/Deque.html +531 -0
  212. data/docs/api/Brut/TUI/EventLoop.html +676 -0
  213. data/docs/api/Brut/TUI/Events/BaseEvent.html +449 -0
  214. data/docs/api/Brut/TUI/Events/EventBus.html +485 -0
  215. data/docs/api/Brut/TUI/Events/EventLoopStarted.html +211 -0
  216. data/docs/api/Brut/TUI/Events/Exception.html +523 -0
  217. data/docs/api/Brut/TUI/Events/Tick.html +294 -0
  218. data/docs/api/Brut/TUI/Events.html +131 -0
  219. data/docs/api/Brut/TUI/MarkupString.html +537 -0
  220. data/docs/api/Brut/TUI/Script/BlockStep.html +300 -0
  221. data/docs/api/Brut/TUI/Script/Events/CommandExecutionFailed.html +252 -0
  222. data/docs/api/Brut/TUI/Script/Events/CommandExecutionSucceeded.html +163 -0
  223. data/docs/api/Brut/TUI/Script/Events/CommandStdErr.html +163 -0
  224. data/docs/api/Brut/TUI/Script/Events/CommandStdOut.html +300 -0
  225. data/docs/api/Brut/TUI/Script/Events/ExecutingCommand.html +298 -0
  226. data/docs/api/Brut/TUI/Script/Events/Message.html +345 -0
  227. data/docs/api/Brut/TUI/Script/Events/PhaseCompleted.html +229 -0
  228. data/docs/api/Brut/TUI/Script/Events/PhaseStarted.html +350 -0
  229. data/docs/api/Brut/TUI/Script/Events/ScriptCompleted.html +282 -0
  230. data/docs/api/Brut/TUI/Script/Events/ScriptStarted.html +343 -0
  231. data/docs/api/Brut/TUI/Script/Events/StepCompleted.html +163 -0
  232. data/docs/api/Brut/TUI/Script/Events/StepStarted.html +346 -0
  233. data/docs/api/Brut/TUI/Script/Events.html +115 -0
  234. data/docs/api/Brut/TUI/Script/ExecStep/ProcessStatusFailed.html +210 -0
  235. data/docs/api/Brut/TUI/Script/ExecStep.html +493 -0
  236. data/docs/api/Brut/TUI/Script/LoggingSubscriber.html +914 -0
  237. data/docs/api/Brut/TUI/Script/PutsSubscriber.html +783 -0
  238. data/docs/api/Brut/TUI/Script/Step.html +313 -0
  239. data/docs/api/Brut/TUI/Script.html +1250 -0
  240. data/docs/api/Brut/TUI/Terminal.html +593 -0
  241. data/docs/api/Brut/TUI/TerminalTheme.html +1403 -0
  242. data/docs/api/Brut/TUI/Themes/Dark.html +706 -0
  243. data/docs/api/Brut/TUI/Themes/Light.html +804 -0
  244. data/docs/api/Brut/TUI/Themes/None.html +218 -0
  245. data/docs/api/Brut/TUI/Themes.html +115 -0
  246. data/docs/api/Brut/TUI.html +129 -0
  247. data/docs/api/Brut.html +4 -4
  248. data/docs/api/Clock.html +2 -2
  249. data/docs/api/ModuleName.html +2 -2
  250. data/docs/api/RichString.html +2 -2
  251. data/docs/api/SemanticLogger/Appender/Async.html +2 -2
  252. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +2 -2
  253. data/docs/api/Sequel/Extensions/BrutMigrations.html +2 -2
  254. data/docs/api/Sequel/Extensions.html +2 -2
  255. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +2 -2
  256. data/docs/api/Sequel/Plugins/CreatedAt.html +2 -2
  257. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +2 -2
  258. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +2 -2
  259. data/docs/api/Sequel/Plugins/ExternalId.html +2 -2
  260. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +2 -2
  261. data/docs/api/Sequel/Plugins/FindBang.html +2 -2
  262. data/docs/api/Sequel/Plugins.html +2 -2
  263. data/docs/api/Sequel.html +2 -2
  264. data/docs/api/_index.html +247 -2
  265. data/docs/api/class_list.html +1 -1
  266. data/docs/api/file.README.html +2 -2
  267. data/docs/api/index.html +2 -2
  268. data/docs/api/method_list.html +1551 -431
  269. data/docs/api/top-level-namespace.html +2 -2
  270. data/docs/assets/{app.Dm7v_ouO.js → app.B8jAEB7R.js} +1 -1
  271. data/docs/assets/chunks/@localSearchIndexroot.DJ8mocCj.js +1 -0
  272. data/docs/assets/chunks/{VPLocalSearchBox.DQK6jQou.js → VPLocalSearchBox.gF-Po_fz.js} +1 -1
  273. data/docs/assets/chunks/{theme.BuExsdM9.js → theme.BjPAOJkz.js} +2 -2
  274. data/docs/assets/{components.md.Dfd3w6UW.js → components.md.Ber8UBM0.js} +3 -3
  275. data/docs/assets/{configuration.md.DTYoV2Ea.js → configuration.md.DrJ6YVoZ.js} +1 -1
  276. data/docs/assets/{deployment.md.C1u5ep0g.js → deployment.md.CHTx2eTR.js} +10 -3
  277. data/docs/assets/{deployment.md.C1u5ep0g.lean.js → deployment.md.CHTx2eTR.lean.js} +1 -1
  278. data/docs/assets/{forms.md.DEkmJUvb.js → forms.md.RK0zkhm0.js} +2 -2
  279. data/docs/assets/{forms.md.DEkmJUvb.lean.js → forms.md.RK0zkhm0.lean.js} +1 -1
  280. data/docs/assets/{getting-started.md.DO-4eoGW.js → getting-started.md.CGJ44juQ.js} +2 -2
  281. data/docs/assets/jobs.md.Bi3qb3v6.js +25 -0
  282. data/docs/assets/jobs.md.Bi3qb3v6.lean.js +1 -0
  283. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.js +12 -0
  284. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.lean.js +1 -0
  285. data/docs/assets/roadmap.md.DqC1Y7Zt.js +1 -0
  286. data/docs/assets/{roadmap.md.CJsbUmK_.lean.js → roadmap.md.DqC1Y7Zt.lean.js} +1 -1
  287. data/docs/assets/{tutorials_02-dialog.md.D2vSjDVf.js → tutorials_02-dialog.md.DE5WfCXI.js} +1 -1
  288. data/docs/assets.html +4 -4
  289. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  290. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  291. data/docs/brut-js/api/Autosubmit.html +1 -1
  292. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  293. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  294. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  295. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  296. data/docs/brut-js/api/BufferedLogger.html +1 -1
  297. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  298. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  299. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  300. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  301. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  302. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  303. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  304. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  305. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  306. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  307. data/docs/brut-js/api/Form.html +1 -1
  308. data/docs/brut-js/api/Form.js.html +1 -1
  309. data/docs/brut-js/api/I18nTranslation.html +1 -1
  310. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  311. data/docs/brut-js/api/LocaleDetection.html +1 -1
  312. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  313. data/docs/brut-js/api/Logger.html +1 -1
  314. data/docs/brut-js/api/Logger.js.html +1 -1
  315. data/docs/brut-js/api/Message.html +1 -1
  316. data/docs/brut-js/api/Message.js.html +1 -1
  317. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  318. data/docs/brut-js/api/RichString.html +1 -1
  319. data/docs/brut-js/api/RichString.js.html +1 -1
  320. data/docs/brut-js/api/Tabs.html +1 -1
  321. data/docs/brut-js/api/Tabs.js.html +1 -1
  322. data/docs/brut-js/api/Toast.html +1 -1
  323. data/docs/brut-js/api/Toast.js.html +1 -1
  324. data/docs/brut-js/api/Tracing.html +1 -1
  325. data/docs/brut-js/api/Tracing.js.html +1 -1
  326. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  327. data/docs/brut-js/api/external-Performance.html +1 -1
  328. data/docs/brut-js/api/external-Promise.html +1 -1
  329. data/docs/brut-js/api/external-ValidityState.html +1 -1
  330. data/docs/brut-js/api/external-Window.html +1 -1
  331. data/docs/brut-js/api/external-fetch.html +1 -1
  332. data/docs/brut-js/api/global.html +1 -1
  333. data/docs/brut-js/api/index.html +1 -1
  334. data/docs/brut-js/api/index.js.html +1 -1
  335. data/docs/brut-js/api/module-testing.html +1 -1
  336. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  337. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  338. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  339. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  340. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  341. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  342. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  343. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  344. data/docs/brut-js/api/testing_index.js.html +1 -1
  345. data/docs/brut-js.html +4 -4
  346. data/docs/business-logic.html +4 -4
  347. data/docs/cli.html +4 -4
  348. data/docs/components.html +8 -8
  349. data/docs/configuration.html +5 -5
  350. data/docs/css.html +4 -4
  351. data/docs/custom-element-tests.html +4 -4
  352. data/docs/database-access.html +4 -4
  353. data/docs/database-schema.html +4 -4
  354. data/docs/deployment.html +13 -6
  355. data/docs/dev-environment.html +4 -4
  356. data/docs/dir-structure.html +4 -4
  357. data/docs/doc-conventions.html +4 -4
  358. data/docs/end-to-end-tests.html +4 -4
  359. data/docs/features.html +4 -4
  360. data/docs/flash-and-session.html +4 -4
  361. data/docs/form-constraints.html +4 -4
  362. data/docs/forms.html +6 -6
  363. data/docs/getting-started.html +7 -7
  364. data/docs/handlers.html +4 -4
  365. data/docs/hashmap.json +1 -1
  366. data/docs/hooks.html +4 -4
  367. data/docs/i18n.html +4 -4
  368. data/docs/index.html +3 -3
  369. data/docs/instrumentation.html +4 -4
  370. data/docs/javascript.html +4 -4
  371. data/docs/jobs.html +29 -5
  372. data/docs/keyword-injection.html +4 -4
  373. data/docs/layouts.html +4 -4
  374. data/docs/lsp.html +4 -4
  375. data/docs/markdown-examples.html +4 -4
  376. data/docs/middleware.html +4 -4
  377. data/docs/overview.html +4 -4
  378. data/docs/pages.html +4 -4
  379. data/docs/recipes/alternate-layouts.html +5 -5
  380. data/docs/recipes/authentication.html +5 -5
  381. data/docs/recipes/custom-flash.html +5 -5
  382. data/docs/recipes/dev-env-secrets.html +40 -0
  383. data/docs/recipes/form-errors.html +5 -5
  384. data/docs/recipes/indexed-forms.html +5 -5
  385. data/docs/recipes/migrations.html +5 -5
  386. data/docs/recipes/text-field-component.html +5 -5
  387. data/docs/roadmap.html +5 -5
  388. data/docs/routes.html +4 -4
  389. data/docs/security.html +4 -4
  390. data/docs/seed-data.html +4 -4
  391. data/docs/space-time-continuum.html +4 -4
  392. data/docs/tutorial.html +4 -4
  393. data/docs/tutorials/01-intro.html +4 -4
  394. data/docs/tutorials/02-dialog.html +5 -5
  395. data/docs/unit-tests.html +4 -4
  396. data/docs/why.html +4 -4
  397. data/lib/brut/cli/apps/build_assets.rb +1 -1
  398. data/lib/brut/cli/apps/heroku_container_based_deploy.rb +1 -1
  399. data/lib/brut/cli/apps/test.rb +6 -4
  400. data/lib/brut/tui/ansi_escape_code.rb +104 -0
  401. data/lib/brut/tui/event_loop.rb +168 -0
  402. data/lib/brut/tui/events/base_event.rb +29 -0
  403. data/lib/brut/tui/events/event_bus.rb +73 -0
  404. data/lib/brut/tui/events/event_loop_started.rb +5 -0
  405. data/lib/brut/tui/events/exception.rb +24 -0
  406. data/lib/brut/tui/events/tick.rb +12 -0
  407. data/lib/brut/tui/events.rb +7 -0
  408. data/lib/brut/tui/markup_string.rb +68 -0
  409. data/lib/brut/tui/script/block_step.rb +17 -0
  410. data/lib/brut/tui/script/events/command_execution_failed.rb +4 -0
  411. data/lib/brut/tui/script/events/command_execution_succeeded.rb +3 -0
  412. data/lib/brut/tui/script/events/command_std_err.rb +3 -0
  413. data/lib/brut/tui/script/events/command_std_out.rb +13 -0
  414. data/lib/brut/tui/script/events/executing_command.rb +12 -0
  415. data/lib/brut/tui/script/events/message.rb +15 -0
  416. data/lib/brut/tui/script/events/phase_completed.rb +4 -0
  417. data/lib/brut/tui/script/events/phase_started.rb +14 -0
  418. data/lib/brut/tui/script/events/script_completed.rb +5 -0
  419. data/lib/brut/tui/script/events/script_started.rb +12 -0
  420. data/lib/brut/tui/script/events/step_completed.rb +3 -0
  421. data/lib/brut/tui/script/events/step_started.rb +12 -0
  422. data/lib/brut/tui/script/events.rb +14 -0
  423. data/lib/brut/tui/script/exec_step.rb +60 -0
  424. data/lib/brut/tui/script/logging_subscriber.rb +98 -0
  425. data/lib/brut/tui/script/puts_subscriber.rb +109 -0
  426. data/lib/brut/tui/script/step.rb +13 -0
  427. data/lib/brut/tui/script.rb +211 -0
  428. data/lib/brut/tui/terminal.rb +74 -0
  429. data/lib/brut/tui/terminal_theme.rb +140 -0
  430. data/lib/brut/tui/themes/dark.rb +14 -0
  431. data/lib/brut/tui/themes/light.rb +17 -0
  432. data/lib/brut/tui/themes/none.rb +9 -0
  433. data/lib/brut/tui/themes.rb +5 -0
  434. data/lib/brut/tui.rb +15 -0
  435. data/lib/brut/version.rb +1 -1
  436. data/lib/brut.rb +1 -0
  437. data/mkbrut/Gemfile.lock +2 -1
  438. data/mkbrut/lib/mkbrut/version.rb +1 -1
  439. data/mkbrut/templates/segments/Heroku/deploy/Dockerfile +9 -7
  440. data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +2 -2
  441. data/specs/brut/tui/ansi_escape_code.spec.rb +30 -0
  442. data/specs/brut/tui/event_loop.spec.rb +70 -0
  443. data/specs/brut/tui/events/base_event.spec.rb +26 -0
  444. data/specs/brut/tui/events/event_bus.spec.rb +141 -0
  445. data/specs/brut/tui/events/exception.spec.rb +19 -0
  446. data/specs/brut/tui/events/test_event.rb +5 -0
  447. data/specs/spec_helper.rb +4 -0
  448. metadata +131 -21
  449. data/docs/assets/chunks/@localSearchIndexroot.BiNFswvo.js +0 -1
  450. data/docs/assets/jobs.md.Bc7Y1YpK.js +0 -1
  451. data/docs/assets/jobs.md.Bc7Y1YpK.lean.js +0 -1
  452. data/docs/assets/roadmap.md.CJsbUmK_.js +0 -1
  453. /data/docs/assets/{components.md.Dfd3w6UW.lean.js → components.md.Ber8UBM0.lean.js} +0 -0
  454. /data/docs/assets/{configuration.md.DTYoV2Ea.lean.js → configuration.md.DrJ6YVoZ.lean.js} +0 -0
  455. /data/docs/assets/{getting-started.md.DO-4eoGW.lean.js → getting-started.md.CGJ44juQ.lean.js} +0 -0
  456. /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