@@ -3,6 +3,7 @@ use std::sync::Arc;
33
44use anyhow:: { Result , bail} ;
55
6+ use crate :: commands:: { self , CommandInfo , CommandResult } ;
67use crate :: tui:: app:: { App , AppAction , AppMode , SidebarFocus } ;
78use crate :: tui:: command_palette:: {
89 CommandPaletteView , build_entries as build_command_palette_entries,
@@ -52,6 +53,7 @@ impl HotbarActionRegistry {
5253 pub fn with_builtins ( ) -> Self {
5354 let mut registry = Self :: new ( ) ;
5455 registry. register_builtins ( ) ;
56+ registry. register_slash_commands ( ) ;
5557 registry
5658 }
5759
@@ -113,6 +115,12 @@ impl HotbarActionRegistry {
113115 ) ) ;
114116 }
115117
118+ pub ( crate ) fn register_slash_commands ( & mut self ) {
119+ for info in commands:: command_infos ( ) {
120+ self . register ( SlashHotbarAction :: new ( info) ) ;
121+ }
122+ }
123+
116124 #[ allow( dead_code) ]
117125 #[ must_use]
118126 pub fn get ( & self , id : & str ) -> Option < Arc < dyn HotbarAction > > {
@@ -137,6 +145,13 @@ impl HotbarActionRegistry {
137145 }
138146}
139147
148+ fn dispatch_command_result ( app : & mut App , result : CommandResult ) -> HotbarDispatch {
149+ app. status_message = result. message ;
150+ result
151+ . action
152+ . map_or ( HotbarDispatch :: Handled , HotbarDispatch :: AppAction )
153+ }
154+
140155#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
141156enum AppHotbarKind {
142157 VoiceToggle ,
@@ -198,11 +213,7 @@ impl HotbarAction for AppHotbarAction {
198213 match self . kind {
199214 AppHotbarKind :: VoiceToggle => {
200215 let result = crate :: commands:: voice:: voice ( app) ;
201- app. status_message = result. message ;
202- match result. action {
203- Some ( action) => Ok ( HotbarDispatch :: AppAction ( action) ) ,
204- None => Ok ( HotbarDispatch :: Handled ) ,
205- }
216+ Ok ( dispatch_command_result ( app, result) )
206217 }
207218 AppHotbarKind :: SessionCompact => {
208219 if app. is_compacting {
@@ -283,6 +294,64 @@ impl HotbarAction for AppHotbarAction {
283294 }
284295}
285296
297+ #[ allow( dead_code) ]
298+ struct SlashHotbarAction {
299+ info : & ' static CommandInfo ,
300+ id : String ,
301+ short_label : String ,
302+ }
303+
304+ impl SlashHotbarAction {
305+ fn new ( info : & ' static CommandInfo ) -> Self {
306+ Self {
307+ info,
308+ id : format ! ( "slash.{}" , info. name) ,
309+ short_label : info. name . chars ( ) . take ( 7 ) . collect ( ) ,
310+ }
311+ }
312+
313+ fn prefill_composer ( & self , app : & mut App ) {
314+ app. clear_input_recoverable ( ) ;
315+ app. input = format ! ( "/{} " , self . info. name) ;
316+ app. cursor_position = app. input . chars ( ) . count ( ) ;
317+ app. slash_menu_hidden = false ;
318+ app. needs_redraw = true ;
319+ app. status_message = Some ( format ! (
320+ "Command needs arguments; complete {}" ,
321+ app. input. trim_end( )
322+ ) ) ;
323+ }
324+ }
325+
326+ impl HotbarAction for SlashHotbarAction {
327+ fn id ( & self ) -> & str {
328+ & self . id
329+ }
330+
331+ fn short_label ( & self ) -> & str {
332+ & self . short_label
333+ }
334+
335+ fn category ( & self ) -> & str {
336+ "slash"
337+ }
338+
339+ fn is_active ( & self , _app : & App ) -> bool {
340+ false
341+ }
342+
343+ fn dispatch ( & self , app : & mut App ) -> Result < HotbarDispatch > {
344+ if self . info . requires_required_argument ( ) {
345+ self . prefill_composer ( app) ;
346+ return Ok ( HotbarDispatch :: Handled ) ;
347+ }
348+
349+ let input = format ! ( "/{}" , self . info. name) ;
350+ let result = commands:: execute ( & input, app) ;
351+ Ok ( dispatch_command_result ( app, result) )
352+ }
353+ }
354+
286355#[ cfg( test) ]
287356mod tests {
288357 use std:: path:: PathBuf ;
@@ -322,7 +391,8 @@ mod tests {
322391
323392 #[ test]
324393 fn builtins_register_expected_actions ( ) {
325- let registry = HotbarActionRegistry :: with_builtins ( ) ;
394+ let mut registry = HotbarActionRegistry :: new ( ) ;
395+ registry. register_builtins ( ) ;
326396 let ids = registry. iter ( ) . map ( HotbarAction :: id) . collect :: < Vec < _ > > ( ) ;
327397
328398 assert_eq ! (
@@ -359,6 +429,73 @@ mod tests {
359429 HotbarActionRegistry :: with_builtins( ) . len( )
360430 ) ;
361431 assert ! ( app. hotbar_actions. get( "mode.agent" ) . is_some( ) ) ;
432+ assert ! ( app. hotbar_actions. get( "slash.help" ) . is_some( ) ) ;
433+ assert ! ( app. hotbar_actions. get( "slash.mode" ) . is_some( ) ) ;
434+ }
435+
436+ #[ test]
437+ fn slash_commands_register_as_hotbar_actions ( ) {
438+ let registry = HotbarActionRegistry :: with_builtins ( ) ;
439+
440+ for info in commands:: command_infos ( ) {
441+ let action_id = format ! ( "slash.{}" , info. name) ;
442+ let action = registry
443+ . get ( & action_id)
444+ . unwrap_or_else ( || panic ! ( "missing slash hotbar action for /{}" , info. name) ) ;
445+ assert_eq ! ( action. category( ) , "slash" ) ;
446+ assert ! ( !action. is_active( & test_app( ) ) ) ;
447+ assert ! (
448+ action. short_label( ) . chars( ) . count( ) <= 7 ,
449+ "{action_id} has an overlong short label"
450+ ) ;
451+ }
452+ }
453+
454+ #[ test]
455+ fn slash_hotbar_action_dispatches_argless_command ( ) {
456+ let registry = HotbarActionRegistry :: with_builtins ( ) ;
457+ let mode = registry. get ( "slash.mode" ) . expect ( "mode slash action" ) ;
458+ let mut app = test_app ( ) ;
459+
460+ assert_eq ! (
461+ mode. dispatch( & mut app) . expect( "dispatch /mode" ) ,
462+ HotbarDispatch :: AppAction ( AppAction :: OpenModePicker )
463+ ) ;
464+ assert ! ( app. input. is_empty( ) ) ;
465+ }
466+
467+ #[ test]
468+ fn slash_hotbar_action_dispatches_optional_argument_command_with_no_args ( ) {
469+ let registry = HotbarActionRegistry :: with_builtins ( ) ;
470+ let task = registry. get ( "slash.task" ) . expect ( "task slash action" ) ;
471+ let mut app = test_app ( ) ;
472+
473+ assert_eq ! (
474+ task. dispatch( & mut app) . expect( "dispatch /task" ) ,
475+ HotbarDispatch :: AppAction ( AppAction :: TaskList )
476+ ) ;
477+ assert ! ( app. input. is_empty( ) ) ;
478+ }
479+
480+ #[ test]
481+ fn slash_hotbar_action_prefills_required_argument_command ( ) {
482+ let registry = HotbarActionRegistry :: with_builtins ( ) ;
483+ let rename = registry. get ( "slash.rename" ) . expect ( "rename slash action" ) ;
484+ let mut app = test_app ( ) ;
485+ app. input = "draft" . to_string ( ) ;
486+ app. cursor_position = app. input . chars ( ) . count ( ) ;
487+
488+ assert_eq ! (
489+ rename. dispatch( & mut app) . expect( "dispatch /rename" ) ,
490+ HotbarDispatch :: Handled
491+ ) ;
492+ assert_eq ! ( app. input, "/rename " ) ;
493+ assert_eq ! ( app. cursor_position, app. input. chars( ) . count( ) ) ;
494+ assert_eq ! ( app. clear_undo_buffer. as_deref( ) , Some ( "draft" ) ) ;
495+ assert_eq ! (
496+ app. status_message. as_deref( ) ,
497+ Some ( "Command needs arguments; complete /rename" )
498+ ) ;
362499 }
363500
364501 #[ test]
0 commit comments