@navios/commander-tui 1.3.0 → 1.5.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 (240) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/{src/__tests__/mocks/scm-mock.ts → dist/base/src/__tests__/mocks/scm-mock.d.ts} +3 -1
  3. package/dist/base/src/__tests__/mocks/scm-mock.d.ts.map +1 -0
  4. package/dist/base/src/__tests__/setup.d.ts +2 -0
  5. package/dist/base/src/__tests__/setup.d.ts.map +1 -0
  6. package/dist/base/src/__tests__/utils/factories.d.ts +67 -0
  7. package/dist/base/src/__tests__/utils/factories.d.ts.map +1 -0
  8. package/dist/base/src/__tests__/utils/render-utils.d.ts +21 -0
  9. package/dist/base/src/__tests__/utils/render-utils.d.ts.map +1 -0
  10. package/dist/base/src/__tests__/utils/test-container.d.ts +20 -0
  11. package/dist/base/src/__tests__/utils/test-container.d.ts.map +1 -0
  12. package/dist/base/src/adapters/interface.d.ts +9 -2
  13. package/dist/base/src/adapters/interface.d.ts.map +1 -1
  14. package/dist/base/src/overrides/missing-adapter.override.d.ts +10 -0
  15. package/dist/base/src/overrides/missing-adapter.override.d.ts.map +1 -1
  16. package/dist/base/src/services/index.d.ts +1 -0
  17. package/dist/base/src/services/index.d.ts.map +1 -1
  18. package/dist/base/src/services/logger.d.ts.map +1 -1
  19. package/dist/base/src/services/readline_prompt.d.ts +27 -0
  20. package/dist/base/src/services/readline_prompt.d.ts.map +1 -0
  21. package/dist/base/src/services/screen.d.ts +14 -8
  22. package/dist/base/src/services/screen.d.ts.map +1 -1
  23. package/dist/base/src/services/screen_manager.d.ts +62 -12
  24. package/dist/base/src/services/screen_manager.d.ts.map +1 -1
  25. package/dist/base/src/tokens/logger.d.ts +2 -2
  26. package/dist/base/src/types/events.types.d.ts +40 -0
  27. package/dist/base/src/types/events.types.d.ts.map +1 -0
  28. package/dist/base/src/types/index.d.ts +1 -0
  29. package/dist/base/src/types/index.d.ts.map +1 -1
  30. package/dist/base/src/types/screen.types.d.ts +28 -0
  31. package/dist/base/src/types/screen.types.d.ts.map +1 -1
  32. package/dist/base/src/utils/colors/helpers.d.ts +0 -16
  33. package/dist/base/src/utils/colors/helpers.d.ts.map +1 -1
  34. package/dist/base/src/utils/index.d.ts +2 -0
  35. package/dist/base/src/utils/index.d.ts.map +1 -1
  36. package/dist/base/src/utils/prompt.d.ts +7 -0
  37. package/dist/base/src/utils/prompt.d.ts.map +1 -0
  38. package/dist/base/src/utils/runtime.d.ts +10 -0
  39. package/dist/base/src/utils/runtime.d.ts.map +1 -0
  40. package/dist/base/src/utils/stdout-printer.d.ts +5 -0
  41. package/dist/base/src/utils/stdout-printer.d.ts.map +1 -1
  42. package/dist/base/tsconfig.base.tsbuildinfo +1 -1
  43. package/dist/base/tsconfig.tsbuildinfo +1 -0
  44. package/dist/ink/src/adapters/ink/components/file/file_log.d.ts +28 -0
  45. package/dist/ink/src/adapters/ink/components/file/file_log.d.ts.map +1 -0
  46. package/dist/ink/src/adapters/ink/components/file/index.d.ts +2 -0
  47. package/dist/ink/src/adapters/ink/components/file/index.d.ts.map +1 -0
  48. package/dist/ink/src/adapters/ink/components/filter/filter_bar.d.ts +7 -0
  49. package/dist/ink/src/adapters/ink/components/filter/filter_bar.d.ts.map +1 -0
  50. package/dist/ink/src/adapters/ink/components/filter/index.d.ts +2 -0
  51. package/dist/ink/src/adapters/ink/components/filter/index.d.ts.map +1 -0
  52. package/dist/ink/src/adapters/ink/components/help/help_overlay.d.ts +6 -0
  53. package/dist/ink/src/adapters/ink/components/help/help_overlay.d.ts.map +1 -0
  54. package/dist/ink/src/adapters/ink/components/help/index.d.ts +2 -0
  55. package/dist/ink/src/adapters/ink/components/help/index.d.ts.map +1 -0
  56. package/dist/ink/src/adapters/ink/components/index.d.ts +9 -0
  57. package/dist/ink/src/adapters/ink/components/index.d.ts.map +1 -0
  58. package/dist/ink/src/adapters/ink/components/log/index.d.ts +2 -0
  59. package/dist/ink/src/adapters/ink/components/log/index.d.ts.map +1 -0
  60. package/dist/ink/src/adapters/ink/components/log/log_message.d.ts +15 -0
  61. package/dist/ink/src/adapters/ink/components/log/log_message.d.ts.map +1 -0
  62. package/dist/ink/src/adapters/ink/components/prompt/index.d.ts +2 -0
  63. package/dist/ink/src/adapters/ink/components/prompt/index.d.ts.map +1 -0
  64. package/dist/ink/src/adapters/ink/components/prompt/prompt_renderer.d.ts +6 -0
  65. package/dist/ink/src/adapters/ink/components/prompt/prompt_renderer.d.ts.map +1 -0
  66. package/dist/ink/src/adapters/ink/components/screen/group_renderer.d.ts +14 -0
  67. package/dist/ink/src/adapters/ink/components/screen/group_renderer.d.ts.map +1 -0
  68. package/dist/ink/src/adapters/ink/components/screen/index.d.ts +7 -0
  69. package/dist/ink/src/adapters/ink/components/screen/index.d.ts.map +1 -0
  70. package/dist/ink/src/adapters/ink/components/screen/loading_message.d.ts +6 -0
  71. package/dist/ink/src/adapters/ink/components/screen/loading_message.d.ts.map +1 -0
  72. package/dist/ink/src/adapters/ink/components/screen/message_renderer.d.ts +6 -0
  73. package/dist/ink/src/adapters/ink/components/screen/message_renderer.d.ts.map +1 -0
  74. package/dist/ink/src/adapters/ink/components/screen/progress_message.d.ts +6 -0
  75. package/dist/ink/src/adapters/ink/components/screen/progress_message.d.ts.map +1 -0
  76. package/dist/ink/src/adapters/ink/components/screen/screen_bridge.d.ts +14 -0
  77. package/dist/ink/src/adapters/ink/components/screen/screen_bridge.d.ts.map +1 -0
  78. package/dist/ink/src/adapters/ink/components/screen/table_message.d.ts +6 -0
  79. package/dist/ink/src/adapters/ink/components/screen/table_message.d.ts.map +1 -0
  80. package/dist/ink/src/adapters/ink/components/screen_manager_bridge.d.ts +8 -0
  81. package/dist/ink/src/adapters/ink/components/screen_manager_bridge.d.ts.map +1 -0
  82. package/dist/ink/src/adapters/ink/components/sidebar/index.d.ts +4 -0
  83. package/dist/ink/src/adapters/ink/components/sidebar/index.d.ts.map +1 -0
  84. package/dist/ink/src/adapters/ink/components/sidebar/sidebar.d.ts +11 -0
  85. package/dist/ink/src/adapters/ink/components/sidebar/sidebar.d.ts.map +1 -0
  86. package/dist/ink/src/adapters/ink/components/sidebar/sidebar_item.d.ts +9 -0
  87. package/dist/ink/src/adapters/ink/components/sidebar/sidebar_item.d.ts.map +1 -0
  88. package/dist/ink/src/adapters/ink/components/sidebar/sidebar_separator.d.ts +2 -0
  89. package/dist/ink/src/adapters/ink/components/sidebar/sidebar_separator.d.ts.map +1 -0
  90. package/dist/ink/src/adapters/ink/context/index.d.ts +2 -0
  91. package/dist/ink/src/adapters/ink/context/index.d.ts.map +1 -0
  92. package/dist/ink/src/adapters/ink/context/logger_context.d.ts +33 -0
  93. package/dist/ink/src/adapters/ink/context/logger_context.d.ts.map +1 -0
  94. package/dist/ink/src/adapters/ink/hooks/index.d.ts +2 -0
  95. package/dist/ink/src/adapters/ink/hooks/index.d.ts.map +1 -0
  96. package/dist/ink/src/adapters/ink/hooks/use_theme.d.ts +7 -0
  97. package/dist/ink/src/adapters/ink/hooks/use_theme.d.ts.map +1 -0
  98. package/dist/ink/src/adapters/ink/index.d.ts +18 -0
  99. package/dist/ink/src/adapters/ink/index.d.ts.map +1 -0
  100. package/dist/ink/src/adapters/react-shared/hooks/index.d.ts +4 -0
  101. package/dist/ink/src/adapters/react-shared/hooks/index.d.ts.map +1 -0
  102. package/dist/ink/src/adapters/react-shared/hooks/use_filter_state.d.ts +18 -0
  103. package/dist/ink/src/adapters/react-shared/hooks/use_filter_state.d.ts.map +1 -0
  104. package/dist/ink/src/adapters/react-shared/hooks/use_keyboard_manager.d.ts +26 -0
  105. package/dist/ink/src/adapters/react-shared/hooks/use_keyboard_manager.d.ts.map +1 -0
  106. package/dist/ink/src/adapters/react-shared/hooks/use_manager_subscription.d.ts +7 -0
  107. package/dist/ink/src/adapters/react-shared/hooks/use_manager_subscription.d.ts.map +1 -0
  108. package/dist/ink/src/adapters/react-shared/index.d.ts +2 -0
  109. package/dist/ink/src/adapters/react-shared/index.d.ts.map +1 -0
  110. package/dist/ink/tsconfig.ink.tsbuildinfo +1 -0
  111. package/dist/react/src/adapters/react-shared/hooks/index.d.ts +4 -0
  112. package/dist/react/src/adapters/react-shared/hooks/index.d.ts.map +1 -0
  113. package/dist/react/src/adapters/react-shared/hooks/use_filter_state.d.ts +18 -0
  114. package/dist/react/src/adapters/react-shared/hooks/use_filter_state.d.ts.map +1 -0
  115. package/dist/react/src/adapters/react-shared/hooks/use_keyboard_manager.d.ts +26 -0
  116. package/dist/react/src/adapters/react-shared/hooks/use_keyboard_manager.d.ts.map +1 -0
  117. package/dist/react/src/adapters/react-shared/hooks/use_manager_subscription.d.ts +7 -0
  118. package/dist/react/src/adapters/react-shared/hooks/use_manager_subscription.d.ts.map +1 -0
  119. package/dist/react/src/adapters/react-shared/index.d.ts +2 -0
  120. package/dist/react/src/adapters/react-shared/index.d.ts.map +1 -0
  121. package/dist/react/tsconfig.react.tsbuildinfo +1 -1
  122. package/dist/solid/tsconfig.solid.tsbuildinfo +1 -1
  123. package/lib/index.cjs +2180 -181
  124. package/lib/index.cjs.map +1 -1
  125. package/lib/index.d.cts +3199 -277
  126. package/lib/index.d.cts.map +1 -0
  127. package/lib/index.d.mts +3199 -277
  128. package/lib/index.d.mts.map +1 -0
  129. package/lib/index.mjs +2101 -138
  130. package/lib/index.mjs.map +1 -1
  131. package/package.json +1 -31
  132. package/src/__tests__/services/logger.spec.ts +32 -0
  133. package/src/__tests__/services/screen.spec.ts +114 -101
  134. package/src/__tests__/utils/factories.ts +2 -0
  135. package/src/adapters/interface.ts +10 -2
  136. package/src/overrides/missing-adapter.override.ts +16 -6
  137. package/src/services/index.ts +1 -0
  138. package/src/services/logger.ts +7 -1
  139. package/src/services/readline_prompt.ts +194 -0
  140. package/src/services/screen.ts +108 -57
  141. package/src/services/screen_manager.ts +287 -74
  142. package/src/types/events.types.ts +84 -0
  143. package/src/types/index.ts +3 -0
  144. package/src/types/screen.types.ts +34 -0
  145. package/src/utils/colors/helpers.ts +0 -25
  146. package/src/utils/index.ts +6 -0
  147. package/src/utils/prompt.ts +18 -0
  148. package/src/utils/runtime.ts +14 -0
  149. package/src/utils/stdout-printer.ts +16 -5
  150. package/tsconfig.base.json +1 -17
  151. package/tsconfig.json +1 -6
  152. package/tsdown.config.mts +13 -70
  153. package/lib/adapters/react/index.cjs +0 -1768
  154. package/lib/adapters/react/index.cjs.map +0 -1
  155. package/lib/adapters/react/index.d.cts +0 -80
  156. package/lib/adapters/react/index.d.mts +0 -80
  157. package/lib/adapters/react/index.mjs +0 -1760
  158. package/lib/adapters/react/index.mjs.map +0 -1
  159. package/lib/adapters/solid/index.cjs +0 -3855
  160. package/lib/adapters/solid/index.cjs.map +0 -1
  161. package/lib/adapters/solid/index.d.cts +0 -74
  162. package/lib/adapters/solid/index.d.mts +0 -74
  163. package/lib/adapters/solid/index.mjs +0 -3847
  164. package/lib/adapters/solid/index.mjs.map +0 -1
  165. package/lib/filter_engine-DXqu9Vaq.cjs +0 -1836
  166. package/lib/filter_engine-DXqu9Vaq.cjs.map +0 -1
  167. package/lib/filter_engine-DmqhEhpA.mjs +0 -1609
  168. package/lib/filter_engine-DmqhEhpA.mjs.map +0 -1
  169. package/lib/interface-CTHQ08aY.d.mts +0 -699
  170. package/lib/interface-DQEIz9Mb.d.cts +0 -699
  171. package/src/__tests__/components/__snapshots__/filter_bar.spec.tsx.snap +0 -2293
  172. package/src/__tests__/components/__snapshots__/loading_message.spec.tsx.snap +0 -1414
  173. package/src/__tests__/components/__snapshots__/log_message.spec.tsx.snap +0 -3245
  174. package/src/__tests__/components/__snapshots__/progress_message.spec.tsx.snap +0 -1783
  175. package/src/__tests__/components/__snapshots__/prompt_renderer.spec.tsx.snap +0 -3203
  176. package/src/__tests__/components/__snapshots__/sidebar.spec.tsx.snap +0 -2857
  177. package/src/__tests__/components/filter_bar.spec.tsx +0 -190
  178. package/src/__tests__/components/loading_message.spec.tsx +0 -110
  179. package/src/__tests__/components/log_message.spec.tsx +0 -166
  180. package/src/__tests__/components/progress_message.spec.tsx +0 -147
  181. package/src/__tests__/components/prompt_renderer.spec.tsx +0 -274
  182. package/src/__tests__/components/sidebar.spec.tsx +0 -322
  183. package/src/__tests__/utils/render-utils.tsx +0 -39
  184. package/src/adapters/react/components/file/file_log.tsx +0 -241
  185. package/src/adapters/react/components/file/index.ts +0 -1
  186. package/src/adapters/react/components/filter/filter_bar.tsx +0 -79
  187. package/src/adapters/react/components/filter/index.ts +0 -1
  188. package/src/adapters/react/components/help/help_overlay.tsx +0 -100
  189. package/src/adapters/react/components/help/index.ts +0 -1
  190. package/src/adapters/react/components/log/index.ts +0 -1
  191. package/src/adapters/react/components/log/log_message.tsx +0 -102
  192. package/src/adapters/react/components/prompt/index.ts +0 -2
  193. package/src/adapters/react/components/prompt/prompt_renderer.tsx +0 -346
  194. package/src/adapters/react/components/screen/group_renderer.tsx +0 -64
  195. package/src/adapters/react/components/screen/index.ts +0 -6
  196. package/src/adapters/react/components/screen/loading_message.tsx +0 -43
  197. package/src/adapters/react/components/screen/message_renderer.tsx +0 -108
  198. package/src/adapters/react/components/screen/progress_message.tsx +0 -60
  199. package/src/adapters/react/components/screen/screen_bridge.tsx +0 -149
  200. package/src/adapters/react/components/screen/table_message.tsx +0 -57
  201. package/src/adapters/react/components/screen_manager_bridge.tsx +0 -245
  202. package/src/adapters/react/components/sidebar/index.ts +0 -3
  203. package/src/adapters/react/components/sidebar/sidebar.tsx +0 -102
  204. package/src/adapters/react/components/sidebar/sidebar_item.tsx +0 -50
  205. package/src/adapters/react/components/sidebar/sidebar_separator.tsx +0 -13
  206. package/src/adapters/react/context/index.ts +0 -1
  207. package/src/adapters/react/context/logger_context.tsx +0 -109
  208. package/src/adapters/react/hooks/index.ts +0 -1
  209. package/src/adapters/react/hooks/use_theme.ts +0 -12
  210. package/src/adapters/react/index.ts +0 -39
  211. package/src/adapters/solid/components/file/file_log.tsx +0 -221
  212. package/src/adapters/solid/components/file/index.ts +0 -1
  213. package/src/adapters/solid/components/filter/filter_bar.tsx +0 -84
  214. package/src/adapters/solid/components/filter/index.ts +0 -1
  215. package/src/adapters/solid/components/help/help_overlay.tsx +0 -106
  216. package/src/adapters/solid/components/help/index.ts +0 -1
  217. package/src/adapters/solid/components/log/index.ts +0 -1
  218. package/src/adapters/solid/components/log/log_message.tsx +0 -92
  219. package/src/adapters/solid/components/prompt/index.ts +0 -2
  220. package/src/adapters/solid/components/prompt/prompt_renderer.tsx +0 -350
  221. package/src/adapters/solid/components/screen/group_renderer.tsx +0 -61
  222. package/src/adapters/solid/components/screen/index.ts +0 -6
  223. package/src/adapters/solid/components/screen/loading_message.tsx +0 -39
  224. package/src/adapters/solid/components/screen/message_renderer.tsx +0 -122
  225. package/src/adapters/solid/components/screen/progress_message.tsx +0 -61
  226. package/src/adapters/solid/components/screen/screen_bridge.tsx +0 -155
  227. package/src/adapters/solid/components/screen/table_message.tsx +0 -58
  228. package/src/adapters/solid/components/screen_manager_bridge.tsx +0 -243
  229. package/src/adapters/solid/components/sidebar/index.ts +0 -3
  230. package/src/adapters/solid/components/sidebar/sidebar.tsx +0 -108
  231. package/src/adapters/solid/components/sidebar/sidebar_item.tsx +0 -55
  232. package/src/adapters/solid/components/sidebar/sidebar_separator.tsx +0 -13
  233. package/src/adapters/solid/context/index.ts +0 -2
  234. package/src/adapters/solid/context/logger_context.tsx +0 -95
  235. package/src/adapters/solid/hooks/index.ts +0 -1
  236. package/src/adapters/solid/hooks/use_theme.ts +0 -12
  237. package/src/adapters/solid/index.tsx +0 -43
  238. package/src/adapters/solid/jsx.d.ts +0 -98
  239. package/tsconfig.react.json +0 -16
  240. package/tsconfig.solid.json +0 -16
@@ -0,0 +1,194 @@
1
+ import * as readline from 'node:readline'
2
+
3
+ import type {
4
+ ChoicePromptData,
5
+ ConfirmPromptData,
6
+ InputPromptData,
7
+ MultiChoicePromptData,
8
+ PromptData,
9
+ } from '../types/index.ts'
10
+
11
+ /**
12
+ * Readline-based prompt service for stdout mode.
13
+ * Provides basic interactive prompts when TUI adapter is not available.
14
+ */
15
+ export class ReadlinePromptService {
16
+ private rl: readline.Interface | null = null
17
+ private promptQueue: Array<{
18
+ prompt: PromptData
19
+ resolve: (value: string | boolean | string[]) => void
20
+ }> = []
21
+ private isProcessing = false
22
+
23
+ private ensureInterface(): readline.Interface {
24
+ if (!this.rl) {
25
+ this.rl = readline.createInterface({
26
+ input: process.stdin,
27
+ output: process.stdout,
28
+ })
29
+ }
30
+ return this.rl
31
+ }
32
+
33
+ /**
34
+ * Handle a prompt interactively via readline.
35
+ * Prompts are queued and processed sequentially.
36
+ */
37
+ async handlePrompt(prompt: PromptData): Promise<string | boolean | string[]> {
38
+ return new Promise((resolve) => {
39
+ this.promptQueue.push({ prompt, resolve })
40
+ this.processQueue()
41
+ })
42
+ }
43
+
44
+ private async processQueue(): Promise<void> {
45
+ if (this.isProcessing || this.promptQueue.length === 0) return
46
+
47
+ this.isProcessing = true
48
+
49
+ while (this.promptQueue.length > 0) {
50
+ const { prompt, resolve } = this.promptQueue.shift()!
51
+ const result = await this.processPrompt(prompt)
52
+ resolve(result)
53
+ }
54
+
55
+ this.isProcessing = false
56
+ }
57
+
58
+ private async processPrompt(prompt: PromptData): Promise<string | boolean | string[]> {
59
+ switch (prompt.type) {
60
+ case 'choice':
61
+ return this.handleChoice(prompt)
62
+ case 'confirm':
63
+ return this.handleConfirm(prompt)
64
+ case 'input':
65
+ return this.handleInput(prompt)
66
+ case 'multiChoice':
67
+ return this.handleMultiChoice(prompt)
68
+ }
69
+ }
70
+
71
+ private async handleChoice(prompt: ChoicePromptData): Promise<string> {
72
+ const rl = this.ensureInterface()
73
+
74
+ // Display question and choices
75
+ console.log(`\n${prompt.question}`)
76
+ prompt.choices.forEach((choice, index) => {
77
+ const isDefault = choice.value === prompt.defaultChoice
78
+ const marker = isDefault ? '*' : ' '
79
+ const inputHint = choice.input ? ' (allows custom input)' : ''
80
+ console.log(` ${marker} ${index + 1}. ${choice.label}${inputHint}`)
81
+ })
82
+
83
+ return new Promise((resolve) => {
84
+ rl.question('Enter number (or press Enter for default): ', (answer) => {
85
+ const trimmed = answer.trim()
86
+ if (!trimmed) {
87
+ resolve(prompt.defaultChoice)
88
+ return
89
+ }
90
+
91
+ const num = parseInt(trimmed, 10)
92
+ if (num >= 1 && num <= prompt.choices.length) {
93
+ const choice = prompt.choices[num - 1]
94
+ if (choice.input) {
95
+ // If choice allows input, ask for it
96
+ rl.question(`Enter value for "${choice.label}": `, (inputValue) => {
97
+ resolve(inputValue.trim() || choice.value)
98
+ })
99
+ } else {
100
+ resolve(choice.value)
101
+ }
102
+ } else {
103
+ resolve(prompt.defaultChoice)
104
+ }
105
+ })
106
+ })
107
+ }
108
+
109
+ private async handleConfirm(prompt: ConfirmPromptData): Promise<boolean> {
110
+ const rl = this.ensureInterface()
111
+ const defaultText = prompt.defaultValue ? 'Y/n' : 'y/N'
112
+
113
+ return new Promise((resolve) => {
114
+ rl.question(`${prompt.question} [${defaultText}]: `, (answer) => {
115
+ const trimmed = answer.trim().toLowerCase()
116
+ if (!trimmed) {
117
+ resolve(prompt.defaultValue)
118
+ return
119
+ }
120
+ resolve(trimmed === 'y' || trimmed === 'yes')
121
+ })
122
+ })
123
+ }
124
+
125
+ private async handleInput(prompt: InputPromptData): Promise<string> {
126
+ const rl = this.ensureInterface()
127
+ const defaultHint = prompt.defaultValue ? ` (default: ${prompt.defaultValue})` : ''
128
+ const placeholderHint = prompt.placeholder ? ` [${prompt.placeholder}]` : ''
129
+
130
+ return new Promise((resolve) => {
131
+ rl.question(`${prompt.question}${placeholderHint}${defaultHint}: `, (answer) => {
132
+ resolve(answer.trim() || prompt.defaultValue)
133
+ })
134
+ })
135
+ }
136
+
137
+ private async handleMultiChoice(prompt: MultiChoicePromptData): Promise<string[]> {
138
+ const rl = this.ensureInterface()
139
+
140
+ // Display question and choices
141
+ console.log(`\n${prompt.question}`)
142
+ console.log(`(Select ${prompt.minSelect}-${prompt.maxSelect} options, enter comma-separated numbers)`)
143
+ prompt.choices.forEach((choice, index) => {
144
+ const selected = prompt.selectedIndices.has(index) ? '[x]' : '[ ]'
145
+ console.log(` ${selected} ${index + 1}. ${choice.label}`)
146
+ })
147
+
148
+ return new Promise((resolve) => {
149
+ rl.question('Enter numbers (e.g., 1,3,5): ', (answer) => {
150
+ const trimmed = answer.trim()
151
+ if (!trimmed) {
152
+ // Return default selections
153
+ resolve(
154
+ prompt.choices.filter((_, i) => prompt.selectedIndices.has(i)).map((c) => c.value),
155
+ )
156
+ return
157
+ }
158
+
159
+ const indices = trimmed
160
+ .split(',')
161
+ .map((s) => parseInt(s.trim(), 10) - 1)
162
+ .filter((n) => n >= 0 && n < prompt.choices.length)
163
+
164
+ // Validate selection count
165
+ if (indices.length < prompt.minSelect) {
166
+ console.log(`Minimum ${prompt.minSelect} selections required. Using defaults.`)
167
+ resolve(
168
+ prompt.choices.filter((_, i) => prompt.selectedIndices.has(i)).map((c) => c.value),
169
+ )
170
+ return
171
+ }
172
+
173
+ if (indices.length > prompt.maxSelect) {
174
+ console.log(`Maximum ${prompt.maxSelect} selections allowed. Taking first ${prompt.maxSelect}.`)
175
+ indices.splice(prompt.maxSelect)
176
+ }
177
+
178
+ resolve(indices.map((i) => prompt.choices[i].value))
179
+ })
180
+ })
181
+ }
182
+
183
+ /**
184
+ * Cleanup readline interface
185
+ */
186
+ destroy(): void {
187
+ if (this.rl) {
188
+ this.rl.close()
189
+ this.rl = null
190
+ }
191
+ this.promptQueue = []
192
+ this.isProcessing = false
193
+ }
194
+ }
@@ -1,3 +1,10 @@
1
+ import { EventEmitter } from 'node:events'
2
+
3
+ import type { LogLevel } from '@navios/core'
4
+
5
+ import { RenderMode } from '../types/index.ts'
6
+ import { getPromptDefaultValue, printSingleMessage } from '../utils/index.ts'
7
+
1
8
  import type { ScreenOptions } from '../schemas/index.ts'
2
9
  import type {
3
10
  ChoicePromptData,
@@ -8,6 +15,7 @@ import type {
8
15
  MultiChoicePromptData,
9
16
  ProgressMessageData,
10
17
  PromptData,
18
+ ScreenEventMap,
11
19
  ScreenStatus,
12
20
  } from '../types/index.ts'
13
21
 
@@ -19,7 +27,7 @@ interface PendingPrompt {
19
27
  timeoutId?: ReturnType<typeof setTimeout>
20
28
  }
21
29
 
22
- export class ScreenInstance {
30
+ export class ScreenInstance extends EventEmitter<ScreenEventMap> {
23
31
  private id: string
24
32
  private name: string
25
33
  private icon?: string
@@ -28,7 +36,6 @@ export class ScreenInstance {
28
36
  private hidden: boolean = false
29
37
  private messages: MessageData[] = []
30
38
  private manager: ScreenManagerInstance | null = null
31
- private changeListeners: Set<() => void> = new Set()
32
39
  private printFn: ((messages: MessageData[], name: string, isError: boolean) => void) | null = null
33
40
  private hasPrinted: boolean = false
34
41
  private version: number = 0
@@ -38,14 +45,14 @@ export class ScreenInstance {
38
45
  private activePrompt: PendingPrompt | null = null
39
46
 
40
47
  constructor(id: string, options: ScreenOptions) {
48
+ super()
41
49
  this.id = id
42
50
  this.name = options.name
43
51
  this.icon = options.icon
44
52
  this.badgeCount = options.badgeCount ?? 0
45
53
  this.hidden = options.hidden ?? false
46
- if (options.static) {
47
- this.status = 'static'
48
- }
54
+
55
+ this.status = (options.static ?? true) ? 'static' : 'waiting'
49
56
  }
50
57
 
51
58
  incrementVersion(): void {
@@ -88,7 +95,7 @@ export class ScreenInstance {
88
95
 
89
96
  setBadgeCount(count: number): this {
90
97
  this.badgeCount = count
91
- this.notifyChange()
98
+ this.emit('badge:changed', count)
92
99
  return this
93
100
  }
94
101
 
@@ -99,7 +106,7 @@ export class ScreenInstance {
99
106
  setHidden(hidden: boolean): this {
100
107
  this.hidden = hidden
101
108
  this.manager?.onScreenVisibilityChanged(this)
102
- this.notifyChange()
109
+ this.emit('visibility:changed', hidden)
103
110
  return this
104
111
  }
105
112
 
@@ -116,7 +123,18 @@ export class ScreenInstance {
116
123
  }
117
124
 
118
125
  /**
119
- * Set screen status. When success/fail and TUI is not bound, prints to stdout/stderr
126
+ * Check if a log level is enabled globally via the ScreenManager.
127
+ * Returns true if no manager is set or if the level is allowed.
128
+ */
129
+ isLogLevelEnabled(level: LogLevel): boolean {
130
+ if (!this.manager) {
131
+ return true
132
+ }
133
+ return this.manager.isLogLevelEnabled(level)
134
+ }
135
+
136
+ /**
137
+ * Set screen status. When success/fail and not in TUI mode, prints to stdout/stderr
120
138
  */
121
139
  setStatus(status: ScreenStatus): this {
122
140
  const wasComplete = this.status === 'success' || this.status === 'fail'
@@ -124,14 +142,15 @@ export class ScreenInstance {
124
142
 
125
143
  // When transitioning to complete state
126
144
  if (!wasComplete && (status === 'success' || status === 'fail')) {
127
- // Only print if TUI is NOT bound (non-interactive mode)
128
- if (!this.manager?.isTuiBound()) {
145
+ // Only print immediately if NOT in TUI mode
146
+ // In TUI mode, printing happens on unbind()
147
+ if (!this.manager?.hasTuiRenderer()) {
129
148
  this.printToConsole()
130
149
  }
131
150
  this.manager?.onScreenCompleted(this)
132
151
  }
133
152
 
134
- this.notifyChange()
153
+ this.emit('status:changed', status)
135
154
  return this
136
155
  }
137
156
 
@@ -143,10 +162,20 @@ export class ScreenInstance {
143
162
  }
144
163
 
145
164
  /**
146
- * Check if this screen has been printed to console
165
+ * Check if this screen has been printed to console.
166
+ * Static screens in non-TUI modes are considered printed as they print incrementally.
147
167
  */
148
168
  hasPrintedToConsole(): boolean {
149
- return this.hasPrinted
169
+ // If already fully printed, return true
170
+ if (this.hasPrinted) return true
171
+
172
+ // Static screens in non-TUI modes print incrementally, so they're considered "printed"
173
+ // as their messages are output immediately when added
174
+ if (this.status === 'static' && !this.manager?.hasTuiRenderer()) {
175
+ return true
176
+ }
177
+
178
+ return false
150
179
  }
151
180
 
152
181
  /**
@@ -171,7 +200,13 @@ export class ScreenInstance {
171
200
  addMessage(message: MessageData): void {
172
201
  this.messages.push(message)
173
202
  this.incrementVersion()
174
- this.notifyChange()
203
+
204
+ // In non-TUI modes, static screens print messages immediately
205
+ if (this.status === 'static' && !this.manager?.hasTuiRenderer()) {
206
+ this.printSingleMessageToConsole(message)
207
+ }
208
+
209
+ this.emit('message:added', message.id)
175
210
  }
176
211
 
177
212
  /**
@@ -180,12 +215,23 @@ export class ScreenInstance {
180
215
  updateMessage(id: string, updates: Partial<LoadingMessageData>): void {
181
216
  const index = this.messages.findIndex((m) => m.id === id)
182
217
  if (index !== -1) {
218
+ const oldMessage = this.messages[index] as LoadingMessageData
183
219
  this.messages[index] = {
184
220
  ...this.messages[index],
185
221
  ...updates,
186
222
  } as MessageData
187
223
  this.incrementVersion()
188
- this.notifyChange()
224
+
225
+ // In non-TUI modes for static screens, print when loading completes
226
+ if (this.status === 'static' && !this.manager?.hasTuiRenderer()) {
227
+ const newMessage = this.messages[index] as LoadingMessageData
228
+ // Only print when transitioning from 'loading' to 'success'/'fail'
229
+ if (newMessage.status !== 'loading' && oldMessage.status === 'loading') {
230
+ this.printSingleMessageToConsole(newMessage)
231
+ }
232
+ }
233
+
234
+ this.emit('message:updated', id)
189
235
  }
190
236
  }
191
237
 
@@ -195,12 +241,26 @@ export class ScreenInstance {
195
241
  updateProgressMessage(id: string, updates: Partial<ProgressMessageData>): void {
196
242
  const index = this.messages.findIndex((m) => m.id === id)
197
243
  if (index !== -1) {
244
+ const oldMessage = this.messages[index] as ProgressMessageData
198
245
  this.messages[index] = {
199
246
  ...this.messages[index],
200
247
  ...updates,
201
248
  } as MessageData
202
249
  this.incrementVersion()
203
- this.notifyChange()
250
+
251
+ // In non-TUI modes for static screens, print when progress completes
252
+ if (this.status === 'static' && !this.manager?.hasTuiRenderer()) {
253
+ const newMessage = this.messages[index] as ProgressMessageData
254
+ if (
255
+ (newMessage.status === 'complete' || newMessage.status === 'failed') &&
256
+ oldMessage.status !== 'complete' &&
257
+ oldMessage.status !== 'failed'
258
+ ) {
259
+ this.printSingleMessageToConsole(newMessage)
260
+ }
261
+ }
262
+
263
+ this.emit('message:updated', id)
204
264
  }
205
265
  }
206
266
 
@@ -210,7 +270,7 @@ export class ScreenInstance {
210
270
  clear(): this {
211
271
  this.messages = []
212
272
  this.incrementVersion()
213
- this.notifyChange()
273
+ this.emit('messages:cleared')
214
274
  return this
215
275
  }
216
276
 
@@ -224,14 +284,23 @@ export class ScreenInstance {
224
284
  */
225
285
  _addPrompt(prompt: PromptData): Promise<string | boolean | string[]> {
226
286
  return new Promise((resolve) => {
227
- const pending: PendingPrompt = { data: prompt, resolve }
287
+ const mode = this.manager?.getRenderMode() ?? RenderMode.UNBOUND
228
288
 
229
- // If TUI is not bound (non-interactive mode), return default immediately
230
- if (!this.manager?.isTuiBound()) {
289
+ // UNBOUND mode: return defaults immediately (no interaction possible)
290
+ if (mode === RenderMode.UNBOUND) {
231
291
  this.resolvePromptWithDefault(prompt, resolve)
232
292
  return
233
293
  }
234
294
 
295
+ // STDOUT modes (STDOUT_INTERACTIVE, STDOUT_FALLBACK): use readline for interactive prompts
296
+ if (mode === RenderMode.STDOUT_INTERACTIVE || mode === RenderMode.STDOUT_FALLBACK) {
297
+ this.manager?.handleReadlinePrompt(prompt).then(resolve)
298
+ return
299
+ }
300
+
301
+ // TUI_ACTIVE mode: use TUI prompt queue system
302
+ const pending: PendingPrompt = { data: prompt, resolve }
303
+
235
304
  // Set up timeout if specified
236
305
  if (prompt.timeout && prompt.timeoutStarted) {
237
306
  pending.timeoutId = setTimeout(() => {
@@ -240,7 +309,7 @@ export class ScreenInstance {
240
309
  this.activePrompt = null
241
310
  this.activateNextPrompt()
242
311
  this.incrementVersion()
243
- this.notifyChange()
312
+ this.emit('prompt:resolved')
244
313
  } else {
245
314
  // Remove from queue if not yet active
246
315
  const idx = this.promptQueue.indexOf(pending)
@@ -270,20 +339,7 @@ export class ScreenInstance {
270
339
  prompt: PromptData,
271
340
  resolve: (value: string | boolean | string[]) => void,
272
341
  ): void {
273
- switch (prompt.type) {
274
- case 'choice':
275
- resolve(prompt.defaultChoice)
276
- break
277
- case 'confirm':
278
- resolve(prompt.defaultValue)
279
- break
280
- case 'input':
281
- resolve(prompt.defaultValue)
282
- break
283
- case 'multiChoice':
284
- resolve(prompt.choices.filter((_, i) => prompt.selectedIndices.has(i)).map((c) => c.value))
285
- break
286
- }
342
+ resolve(getPromptDefaultValue(prompt))
287
343
  }
288
344
 
289
345
  /**
@@ -316,7 +372,7 @@ export class ScreenInstance {
316
372
  } else if (prompt.type === 'confirm') {
317
373
  ;(prompt as ConfirmPromptData).selectedValue = index === 0
318
374
  }
319
- this.notifyChange()
375
+ this.emit('prompt:updated')
320
376
  }
321
377
 
322
378
  /**
@@ -359,7 +415,7 @@ export class ScreenInstance {
359
415
  const prompt = this.activePrompt.data
360
416
  if (prompt.type === 'confirm') {
361
417
  ;(prompt as ConfirmPromptData).selectedValue = true
362
- this.notifyChange()
418
+ this.emit('prompt:updated')
363
419
  }
364
420
  }
365
421
 
@@ -368,7 +424,7 @@ export class ScreenInstance {
368
424
  const prompt = this.activePrompt.data
369
425
  if (prompt.type === 'confirm') {
370
426
  ;(prompt as ConfirmPromptData).selectedValue = false
371
- this.notifyChange()
427
+ this.emit('prompt:updated')
372
428
  }
373
429
  }
374
430
 
@@ -385,7 +441,7 @@ export class ScreenInstance {
385
441
  } else if (p.selectedIndices.size < p.maxSelect) {
386
442
  p.selectedIndices.add(p.focusedIndex)
387
443
  }
388
- this.notifyChange()
444
+ this.emit('prompt:updated')
389
445
  }
390
446
  }
391
447
 
@@ -401,7 +457,7 @@ export class ScreenInstance {
401
457
  const choice = prompt.choices[prompt.selectedIndex]
402
458
  if (choice?.input) {
403
459
  ;(prompt as ChoicePromptData).inputMode = true
404
- this.notifyChange()
460
+ this.emit('prompt:updated')
405
461
  return true
406
462
  }
407
463
  } else if (prompt.type === 'input') {
@@ -420,7 +476,7 @@ export class ScreenInstance {
420
476
  const prompt = this.activePrompt.data
421
477
  if (prompt.type === 'choice' && prompt.inputMode) {
422
478
  ;(prompt as ChoicePromptData).inputMode = false
423
- this.notifyChange()
479
+ this.emit('prompt:updated')
424
480
  }
425
481
  // Input prompts cannot exit input mode
426
482
  }
@@ -449,10 +505,10 @@ export class ScreenInstance {
449
505
  const prompt = this.activePrompt.data
450
506
  if (prompt.type === 'choice' && prompt.inputMode) {
451
507
  ;(prompt as ChoicePromptData).inputValue = value
452
- this.notifyChange()
508
+ this.emit('prompt:updated')
453
509
  } else if (prompt.type === 'input') {
454
510
  ;(prompt as InputPromptData).value = value
455
- this.notifyChange()
511
+ this.emit('prompt:updated')
456
512
  }
457
513
  }
458
514
 
@@ -465,10 +521,10 @@ export class ScreenInstance {
465
521
  const prompt = this.activePrompt.data
466
522
  if (prompt.type === 'choice' && prompt.inputMode) {
467
523
  ;(prompt as ChoicePromptData).inputValue += char
468
- this.notifyChange()
524
+ this.emit('prompt:updated')
469
525
  } else if (prompt.type === 'input') {
470
526
  ;(prompt as InputPromptData).value += char
471
- this.notifyChange()
527
+ this.emit('prompt:updated')
472
528
  }
473
529
  }
474
530
 
@@ -481,10 +537,10 @@ export class ScreenInstance {
481
537
  const prompt = this.activePrompt.data
482
538
  if (prompt.type === 'choice' && prompt.inputMode) {
483
539
  ;(prompt as ChoicePromptData).inputValue = prompt.inputValue.slice(0, -1)
484
- this.notifyChange()
540
+ this.emit('prompt:updated')
485
541
  } else if (prompt.type === 'input') {
486
542
  ;(prompt as InputPromptData).value = prompt.value.slice(0, -1)
487
- this.notifyChange()
543
+ this.emit('prompt:updated')
488
544
  }
489
545
  }
490
546
 
@@ -550,7 +606,7 @@ export class ScreenInstance {
550
606
  this.activePrompt = null
551
607
  this.activateNextPrompt()
552
608
  this.incrementVersion()
553
- this.notifyChange()
609
+ this.emit('prompt:resolved')
554
610
  }
555
611
 
556
612
  /**
@@ -562,20 +618,15 @@ export class ScreenInstance {
562
618
  // Notify manager to focus this screen
563
619
  this.manager?.onScreenPromptActivated(this)
564
620
  this.incrementVersion()
565
- this.notifyChange()
621
+ this.emit('prompt:activated')
566
622
  }
567
623
  }
568
624
 
569
625
  /**
570
- * Register a change listener for re-renders
626
+ * Print a single message immediately to stdout (for static screens in stdout mode)
571
627
  */
572
- onChange(listener: () => void): () => void {
573
- this.changeListeners.add(listener)
574
- return () => this.changeListeners.delete(listener)
575
- }
576
-
577
- private notifyChange(): void {
578
- this.changeListeners.forEach((listener) => listener())
628
+ private printSingleMessageToConsole(message: MessageData): void {
629
+ printSingleMessage(message, this.name, false)
579
630
  }
580
631
 
581
632
  /**