44
55use PhpSchool \CliMenu \Action \ExitAction ;
66use PhpSchool \CliMenu \Action \GoBackAction ;
7+ use PhpSchool \CliMenu \Exception \InvalidShortcutException ;
78use PhpSchool \CliMenu \MenuItem \AsciiArtItem ;
89use PhpSchool \CliMenu \MenuItem \LineBreakItem ;
910use PhpSchool \CliMenu \MenuItem \MenuItemInterface ;
1011use PhpSchool \CliMenu \MenuItem \MenuMenuItem ;
1112use PhpSchool \CliMenu \MenuItem \SelectableItem ;
1213use PhpSchool \CliMenu \CliMenu ;
14+ use PhpSchool \CliMenu \MenuItem \SplitItem ;
1315use PhpSchool \CliMenu \MenuItem \StaticItem ;
1416use PhpSchool \CliMenu \MenuStyle ;
1517use PhpSchool \CliMenu \Terminal \TerminalFactory ;
@@ -56,6 +58,22 @@ class CliMenuBuilder
5658 */
5759 private $ disabled = false ;
5860
61+ /**
62+ * Whether or not to auto create keyboard shortcuts for items
63+ * when they contain square brackets. Eg: [M]y item
64+ *
65+ * @var bool
66+ */
67+ private $ autoShortcuts = false ;
68+
69+ /**
70+ * Regex to auto match for shortcuts defaults to looking
71+ * for a single character encased in square brackets
72+ *
73+ * @var string
74+ */
75+ private $ autoShortcutsRegex = '/\[(.)\]/ ' ;
76+
5977 /**
6078 * @var bool
6179 */
@@ -87,6 +105,8 @@ public function addMenuItem(MenuItemInterface $item) : self
87105 {
88106 $ this ->menu ->addItem ($ item );
89107
108+ $ this ->processItemShortcut ($ item );
109+
90110 return $ this ;
91111 }
92112
@@ -135,6 +155,10 @@ public function addSubMenu(string $text, \Closure $callback) : self
135155 {
136156 $ builder = self ::newSubMenu ($ this ->terminal );
137157
158+ if ($ this ->autoShortcuts ) {
159+ $ builder ->enableAutoShortcuts ($ this ->autoShortcutsRegex );
160+ }
161+
138162 $ callback = $ callback ->bindTo ($ builder );
139163 $ callback ($ builder );
140164
@@ -147,12 +171,14 @@ public function addSubMenu(string $text, \Closure $callback) : self
147171 $ menu ->setStyle ($ this ->menu ->getStyle ());
148172 }
149173
150- $ this ->menu ->addItem (new MenuMenuItem (
174+ $ this ->menu ->addItem ($ item = new MenuMenuItem (
151175 $ text ,
152176 $ menu ,
153177 $ builder ->isMenuDisabled ()
154178 ));
155-
179+
180+ $ this ->processItemShortcut ($ item );
181+
156182 return $ this ;
157183 }
158184
@@ -167,24 +193,98 @@ public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : s
167193 $ menu ->setStyle ($ this ->menu ->getStyle ());
168194 }
169195
170- $ this ->menu ->addItem (new MenuMenuItem (
196+ $ this ->menu ->addItem ($ item = new MenuMenuItem (
171197 $ text ,
172198 $ menu ,
173199 $ builder ->isMenuDisabled ()
174200 ));
175201
202+ $ this ->processItemShortcut ($ item );
203+
176204 return $ this ;
177205 }
178206
207+ public function enableAutoShortcuts (string $ regex = null ) : self
208+ {
209+ $ this ->autoShortcuts = true ;
210+
211+ if (null !== $ regex ) {
212+ $ this ->autoShortcutsRegex = $ regex ;
213+ }
214+
215+ return $ this ;
216+ }
217+
218+ private function extractShortcut (string $ title ) : ?string
219+ {
220+ preg_match ($ this ->autoShortcutsRegex , $ title , $ match );
221+
222+ if (!isset ($ match [1 ])) {
223+ return null ;
224+ }
225+
226+ if (mb_strlen ($ match [1 ]) > 1 ) {
227+ throw InvalidShortcutException::fromShortcut ($ match [1 ]);
228+ }
229+
230+ return isset ($ match [1 ]) ? strtolower ($ match [1 ]) : null ;
231+ }
232+
233+ private function processItemShortcut (MenuItemInterface $ item ) : void
234+ {
235+ $ this ->processIndividualShortcut ($ item , function (CliMenu $ menu ) use ($ item ) {
236+ $ menu ->executeAsSelected ($ item );
237+ });
238+ }
239+
240+ private function processSplitItemShortcuts (SplitItem $ splitItem ) : void
241+ {
242+ foreach ($ splitItem ->getItems () as $ item ) {
243+ $ this ->processIndividualShortcut ($ item , function (CliMenu $ menu ) use ($ splitItem , $ item ) {
244+ $ current = $ splitItem ->getSelectedItemIndex ();
245+
246+ $ splitItem ->setSelectedItemIndex (
247+ array_search ($ item , $ splitItem ->getItems (), true )
248+ );
249+
250+ $ menu ->executeAsSelected ($ splitItem );
251+
252+ if ($ current !== null ) {
253+ $ splitItem ->setSelectedItemIndex ($ current );
254+ }
255+ });
256+ }
257+ }
258+
259+ private function processIndividualShortcut (MenuItemInterface $ item , callable $ callback ) : void
260+ {
261+ if (!$ this ->autoShortcuts ) {
262+ return ;
263+ }
264+
265+ if ($ shortcut = $ this ->extractShortcut ($ item ->getText ())) {
266+ $ this ->menu ->addCustomControlMapping (
267+ $ shortcut ,
268+ $ callback
269+ );
270+ }
271+ }
272+
179273 public function addSplitItem (\Closure $ callback ) : self
180274 {
181275 $ builder = new SplitItemBuilder ($ this ->menu );
182276
277+ if ($ this ->autoShortcuts ) {
278+ $ builder ->enableAutoShortcuts ($ this ->autoShortcutsRegex );
279+ }
280+
183281 $ callback = $ callback ->bindTo ($ builder );
184282 $ callback ($ builder );
185283
186- $ this ->menu ->addItem ($ builder ->build ());
187-
284+ $ this ->menu ->addItem ($ splitItem = $ builder ->build ());
285+
286+ $ this ->processSplitItemShortcuts ($ splitItem );
287+
188288 return $ this ;
189289 }
190290
0 commit comments