@@ -2700,6 +2700,20 @@ impl OsvmApp {
27002700 /// Handle a key event (from either local terminal or web)
27012701 /// Mirrors the main event_loop key handling for consistent behavior
27022702 fn handle_key_event ( & mut self , key : KeyEvent ) {
2703+ // Check if filter modal is active first
2704+ let filter_modal_active = if self . active_tab == TabIndex :: Graph {
2705+ self . wallet_graph . lock ( )
2706+ . map ( |g| g. filter_modal . active )
2707+ . unwrap_or ( false )
2708+ } else {
2709+ false
2710+ } ;
2711+
2712+ if filter_modal_active {
2713+ self . handle_filter_modal_key ( key) ;
2714+ return ;
2715+ }
2716+
27032717 match key. code {
27042718 // Quit/Escape - handle different contexts
27052719 KeyCode :: Char ( 'q' ) | KeyCode :: Esc => {
@@ -2898,6 +2912,12 @@ impl OsvmApp {
28982912 graph. handle_input ( GraphInput :: ScrollDetailDown ) ;
28992913 }
29002914 }
2915+ // Filter modal (backtick key)
2916+ KeyCode :: Char ( '`' ) if self . active_tab == TabIndex :: Graph => {
2917+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
2918+ graph. handle_input ( GraphInput :: OpenFilterModal ) ;
2919+ }
2920+ }
29012921 // Number keys 1-5 to select tabs
29022922 KeyCode :: Char ( '1' ) if !self . chat_input_active => {
29032923 self . active_tab = TabIndex :: Chat ;
@@ -2952,6 +2972,78 @@ impl OsvmApp {
29522972 }
29532973 }
29542974
2975+ /// Handle key events when filter modal is active
2976+ fn handle_filter_modal_key ( & mut self , key : KeyEvent ) {
2977+ match key. code {
2978+ // Close modal
2979+ KeyCode :: Esc | KeyCode :: Char ( '`' ) | KeyCode :: Char ( 'q' ) => {
2980+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
2981+ graph. handle_input ( GraphInput :: CloseFilterModal ) ;
2982+ }
2983+ }
2984+ // Navigate items
2985+ KeyCode :: Up | KeyCode :: Char ( 'k' ) => {
2986+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
2987+ if graph. filter_modal . selected_index > 0 {
2988+ graph. filter_modal . selected_index -= 1 ;
2989+ // Adjust scroll offset if needed
2990+ if graph. filter_modal . selected_index < graph. filter_modal . scroll_offset {
2991+ graph. filter_modal . scroll_offset = graph. filter_modal . selected_index ;
2992+ }
2993+ }
2994+ }
2995+ }
2996+ KeyCode :: Down | KeyCode :: Char ( 'j' ) => {
2997+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
2998+ let max_idx = graph. filter_modal . current_items ( ) . len ( ) . saturating_sub ( 1 ) ;
2999+ if graph. filter_modal . selected_index < max_idx {
3000+ graph. filter_modal . selected_index += 1 ;
3001+ // Adjust scroll offset if needed (assume 10 visible items)
3002+ if graph. filter_modal . selected_index >= graph. filter_modal . scroll_offset + 15 {
3003+ graph. filter_modal . scroll_offset = graph. filter_modal . selected_index . saturating_sub ( 14 ) ;
3004+ }
3005+ }
3006+ }
3007+ }
3008+ // Toggle selection with Space
3009+ KeyCode :: Char ( ' ' ) => {
3010+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3011+ graph. handle_input ( GraphInput :: FilterToggleItem ) ;
3012+ }
3013+ }
3014+ // Tab to switch between Wallets/Programs/Tokens tabs
3015+ KeyCode :: Tab => {
3016+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3017+ graph. handle_input ( GraphInput :: FilterNextTab ) ;
3018+ }
3019+ }
3020+ KeyCode :: BackTab => {
3021+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3022+ graph. handle_input ( GraphInput :: FilterPrevTab ) ;
3023+ }
3024+ }
3025+ // Select all with 'a'
3026+ KeyCode :: Char ( 'a' ) => {
3027+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3028+ graph. handle_input ( GraphInput :: FilterSelectAll ) ;
3029+ }
3030+ }
3031+ // Deselect all with 'x'
3032+ KeyCode :: Char ( 'x' ) => {
3033+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3034+ graph. handle_input ( GraphInput :: FilterDeselectAll ) ;
3035+ }
3036+ }
3037+ // Enter to apply and close
3038+ KeyCode :: Enter => {
3039+ if let Ok ( mut graph) = self . wallet_graph . lock ( ) {
3040+ graph. handle_input ( GraphInput :: CloseFilterModal ) ;
3041+ }
3042+ }
3043+ _ => { }
3044+ }
3045+ }
3046+
29553047 fn event_loop < B , F > ( & mut self , terminal : & mut Terminal < B > , on_tick : F ) -> Result < ( ) >
29563048 where
29573049 B : ratatui:: backend:: Backend + std:: io:: Write ,
@@ -3715,6 +3807,19 @@ impl OsvmApp {
37153807 }
37163808 }
37173809
3810+ // Render filter modal if active (only on Graph tab)
3811+ let show_filter_modal = if self . active_tab == TabIndex :: Graph {
3812+ self . wallet_graph . lock ( )
3813+ . map ( |g| g. filter_modal . active )
3814+ . unwrap_or ( false )
3815+ } else {
3816+ false
3817+ } ;
3818+
3819+ if show_filter_modal {
3820+ self . render_filter_modal ( f, size) ;
3821+ }
3822+
37183823 // Render help overlay if active
37193824 if self . show_help {
37203825 self . render_help_overlay ( f, size) ;
@@ -4831,6 +4936,147 @@ impl OsvmApp {
48314936 f. render_widget ( search_widget, modal_area) ;
48324937 }
48334938
4939+ /// Render the filter modal for graph node/token filtering
4940+ fn render_filter_modal ( & mut self , f : & mut Frame , area : Rect ) {
4941+ use super :: graph:: FilterTab ;
4942+
4943+ // Get filter modal state
4944+ let ( current_tab, selected_index, scroll_offset, items, counts) = {
4945+ if let Ok ( graph) = self . wallet_graph . lock ( ) {
4946+ let items = graph. filter_modal . current_items ( ) ;
4947+ let counts = (
4948+ graph. filter_modal . all_wallets . len ( ) ,
4949+ graph. filter_modal . all_programs . len ( ) ,
4950+ graph. filter_modal . all_tokens . len ( ) ,
4951+ graph. filter_modal . selected_wallets . len ( ) ,
4952+ graph. filter_modal . selected_programs . len ( ) ,
4953+ graph. filter_modal . selected_tokens . len ( ) ,
4954+ ) ;
4955+ (
4956+ graph. filter_modal . current_tab ,
4957+ graph. filter_modal . selected_index ,
4958+ graph. filter_modal . scroll_offset ,
4959+ items,
4960+ counts,
4961+ )
4962+ } else {
4963+ return ;
4964+ }
4965+ } ;
4966+
4967+ // Center the modal (50% width, 60% height)
4968+ let width = ( area. width * 60 / 100 ) . max ( 50 ) . min ( area. width . saturating_sub ( 4 ) ) ;
4969+ let height = ( area. height * 70 / 100 ) . max ( 20 ) . min ( area. height . saturating_sub ( 4 ) ) ;
4970+ let x = ( area. width . saturating_sub ( width) ) / 2 ;
4971+ let y = ( area. height . saturating_sub ( height) ) / 2 ;
4972+ let modal_area = Rect :: new ( x, y, width, height) ;
4973+
4974+ // Clear background
4975+ f. render_widget ( Clear , modal_area) ;
4976+
4977+ // Create main block
4978+ let block = Block :: default ( )
4979+ . title ( " 🔍 Filter Graph " )
4980+ . title_style ( Style :: default ( ) . fg ( Color :: Cyan ) . add_modifier ( Modifier :: BOLD ) )
4981+ . borders ( Borders :: ALL )
4982+ . border_style ( Style :: default ( ) . fg ( Color :: Cyan ) )
4983+ . style ( Style :: default ( ) . bg ( Color :: Black ) ) ;
4984+
4985+ let inner = block. inner ( modal_area) ;
4986+ f. render_widget ( block, modal_area) ;
4987+
4988+ // Split into tabs bar and content
4989+ let chunks = Layout :: default ( )
4990+ . direction ( Direction :: Vertical )
4991+ . constraints ( [
4992+ Constraint :: Length ( 3 ) , // Tabs
4993+ Constraint :: Min ( 0 ) , // Content
4994+ Constraint :: Length ( 3 ) , // Footer with instructions
4995+ ] )
4996+ . split ( inner) ;
4997+
4998+ // Render tabs
4999+ let ( wallet_count, program_count, token_count, sel_wallet, sel_program, sel_token) = counts;
5000+ let tabs = vec ! [
5001+ format!( " Wallets ({}/{}) " , sel_wallet, wallet_count) ,
5002+ format!( " Programs ({}/{}) " , sel_program, program_count) ,
5003+ format!( " Tokens ({}/{}) " , sel_token, token_count) ,
5004+ ] ;
5005+
5006+ let tab_idx = match current_tab {
5007+ FilterTab :: Wallets => 0 ,
5008+ FilterTab :: Programs => 1 ,
5009+ FilterTab :: Tokens => 2 ,
5010+ } ;
5011+
5012+ let tabs_widget = Tabs :: new ( tabs. iter ( ) . map ( |s| s. as_str ( ) ) . collect :: < Vec < _ > > ( ) )
5013+ . block ( Block :: default ( ) . borders ( Borders :: BOTTOM ) )
5014+ . select ( tab_idx)
5015+ . style ( Style :: default ( ) . fg ( Color :: White ) )
5016+ . highlight_style ( Style :: default ( ) . fg ( Color :: Cyan ) . add_modifier ( Modifier :: BOLD ) ) ;
5017+
5018+ f. render_widget ( tabs_widget, chunks[ 0 ] ) ;
5019+
5020+ // Render content list
5021+ let visible_height = chunks[ 1 ] . height as usize ;
5022+ let display_items: Vec < ListItem > = items
5023+ . iter ( )
5024+ . enumerate ( )
5025+ . skip ( scroll_offset)
5026+ . take ( visible_height)
5027+ . map ( |( idx, ( name, selected) ) | {
5028+ let checkbox = if * selected { "☑" } else { "☐" } ;
5029+ let style = if idx == selected_index {
5030+ Style :: default ( ) . fg ( Color :: Yellow ) . bg ( Color :: DarkGray ) . add_modifier ( Modifier :: BOLD )
5031+ } else if * selected {
5032+ Style :: default ( ) . fg ( Color :: Green )
5033+ } else {
5034+ Style :: default ( ) . fg ( Color :: DarkGray )
5035+ } ;
5036+
5037+ // Truncate long addresses
5038+ let display_name = if name. len ( ) > 50 {
5039+ format ! ( "{}...{}" , & name[ ..20 ] , & name[ name. len( ) -20 ..] )
5040+ } else {
5041+ name. clone ( )
5042+ } ;
5043+
5044+ ListItem :: new ( Line :: from ( vec ! [
5045+ Span :: styled( format!( " {} " , checkbox) , style) ,
5046+ Span :: styled( display_name, style) ,
5047+ ] ) )
5048+ } )
5049+ . collect ( ) ;
5050+
5051+ let list = List :: new ( display_items)
5052+ . block ( Block :: default ( ) ) ;
5053+
5054+ f. render_widget ( list, chunks[ 1 ] ) ;
5055+
5056+ // Render footer with instructions
5057+ let footer_text = Line :: from ( vec ! [
5058+ Span :: styled( " ↑↓ " , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5059+ Span :: raw( "navigate " ) ,
5060+ Span :: styled( "Space" , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5061+ Span :: raw( " toggle " ) ,
5062+ Span :: styled( "Tab" , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5063+ Span :: raw( " switch " ) ,
5064+ Span :: styled( "a" , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5065+ Span :: raw( " all " ) ,
5066+ Span :: styled( "x" , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5067+ Span :: raw( " none " ) ,
5068+ Span :: styled( "Enter/`/Esc" , Style :: default ( ) . fg( Color :: Yellow ) ) ,
5069+ Span :: raw( " apply" ) ,
5070+ ] ) ;
5071+
5072+ let footer = Paragraph :: new ( footer_text)
5073+ . style ( Style :: default ( ) . fg ( Color :: DarkGray ) )
5074+ . alignment ( Alignment :: Center )
5075+ . block ( Block :: default ( ) . borders ( Borders :: TOP ) ) ;
5076+
5077+ f. render_widget ( footer, chunks[ 2 ] ) ;
5078+ }
5079+
48345080 fn render_help_overlay ( & mut self , f : & mut Frame , area : Rect ) {
48355081 // Center the help box
48365082 let width = 70 . min ( area. width . saturating_sub ( 4 ) ) ;
0 commit comments