Fortran · 81086 bytes Raw Blame History
1 ! GTK4 Application Module for Sniffly
2 ! Handles application initialization and main window setup
3 module gtk_app
4 use, intrinsic :: iso_c_binding
5 use gtk, only: gtk_init, gtk_application_new, gtk_application_window_new, &
6 gtk_window_set_title, gtk_window_set_default_size, &
7 gtk_window_present, G_APPLICATION_DEFAULT_FLAGS, &
8 gtk_application_get_active_window, gtk_window_destroy, &
9 gtk_window_set_child, g_signal_connect, &
10 gtk_box_new, gtk_box_append, gtk_box_remove, GTK_ORIENTATION_VERTICAL, &
11 GTK_ORIENTATION_HORIZONTAL, gtk_button_new_with_label, &
12 gtk_widget_set_hexpand, gtk_widget_set_vexpand, gtk_widget_get_first_child, &
13 gtk_label_new, gtk_label_set_text, gtk_widget_set_halign, &
14 GTK_ALIGN_START, gtk_progress_bar_new, gtk_progress_bar_set_fraction, &
15 gtk_progress_bar_set_text, gtk_progress_bar_set_show_text, &
16 gtk_widget_set_visible, gtk_widget_set_sensitive, &
17 gtk_button_new, gtk_button_set_icon_name, gtk_widget_set_tooltip_text, &
18 gtk_entry_new, gtk_entry_buffer_set_text, gtk_entry_get_buffer, &
19 gtk_editable_set_editable, gtk_editable_get_text, &
20 gtk_entry_set_placeholder_text, gtk_widget_add_css_class, &
21 gtk_widget_remove_css_class, gtk_css_provider_new, &
22 gtk_css_provider_load_from_string, gtk_style_context_add_provider_for_display
23 use gdk, only: gdk_display_get_default, gdk_display_get_clipboard, gdk_clipboard_set_text
24 use g, only: g_application_run, g_idle_add, g_timeout_add_seconds_once
25 use treemap_widget, only: create_treemap_widget, set_scan_path, register_navigation_callback, &
26 register_key_handler, register_quit_callback, register_delete_callback, &
27 register_refresh_callback, register_selection_callback, mark_initial_scan_complete, &
28 has_selection, get_selected_node_path
29 use breadcrumb_widget, only: create_breadcrumb_widget, update_breadcrumb_cache, &
30 set_navigation_callback, get_previous_breadcrumb_path, &
31 clear_previous_breadcrumb_path
32 use treemap_renderer, only: register_progress_callback, scan_directory, set_redraw_widget, &
33 register_scan_completion_callback, set_renderer_state_from_tab
34 use tab_manager, only: tab_state, init_tab_manager, create_tab, get_active_tab, &
35 switch_to_tab, num_tabs, active_tab_index, get_path_basename
36 use tab_widget, only: create_tab_bar, refresh_tab_bar, register_tab_switch_callback, &
37 update_tab_visual_states
38 implicit none
39 private
40
41 public :: sniffly_app_run, sniffly_app_quit, sniffly_set_scan_path, &
42 sniffly_update_status, sniffly_show_error, breadcrumb_callback, &
43 sniffly_update_progress, sniffly_show_progress, sniffly_hide_progress, &
44 sniffly_update_status_bar_stats, get_forward_path, update_ui_for_active_tab
45
46 ! Application constants
47 character(len=*), parameter :: APP_ID = "org.fortrangoingonforty.sniffly"
48 character(len=*), parameter :: APP_TITLE = "Sniffly - Disk Space Analyzer"
49 integer, parameter :: DEFAULT_WIDTH = 1024
50 integer, parameter :: DEFAULT_HEIGHT = 768
51
52 ! Global application pointer (will be set in activate callback)
53 type(c_ptr), save :: app_ptr = c_null_ptr
54 type(c_ptr), save :: main_window_ptr = c_null_ptr
55 type(c_ptr), save :: status_label_ptr = c_null_ptr
56 type(c_ptr), save :: progress_bar_ptr = c_null_ptr
57 type(c_ptr), save :: path_entry_ptr = c_null_ptr
58
59 ! Shutdown flag - set when app is closing to prevent widget access
60 logical, save :: app_is_shutting_down = .false.
61
62 ! Global scan path (can be set via command line)
63 character(len=512), save :: global_scan_path = ""
64
65 ! Scan path for async initial scan
66 character(len=512), save :: pending_scan_path = ""
67
68 ! Navigation history for Back/Forward buttons
69 integer, parameter :: MAX_HISTORY = 50
70 character(len=512), dimension(MAX_HISTORY), save :: nav_history
71 integer, save :: nav_history_count = 0
72 integer, save :: nav_history_pos = 0 ! Current position in history (0 = no history)
73 logical, save :: navigating_history = .false. ! Flag: are we using back/forward?
74
75 ! Button pointers for enabling/disabling
76 type(c_ptr), save :: back_btn_ptr = c_null_ptr
77 type(c_ptr), save :: forward_btn_ptr = c_null_ptr
78 type(c_ptr), save :: cancel_scan_btn_ptr = c_null_ptr
79
80 ! Selection-dependent button pointers
81 type(c_ptr), save :: info_btn_ptr = c_null_ptr
82 type(c_ptr), save :: copy_path_btn_ptr = c_null_ptr
83 type(c_ptr), save :: open_finder_btn_ptr = c_null_ptr
84 type(c_ptr), save :: delete_btn_ptr = c_null_ptr
85
86 ! Open directory button pointer (for pulsing on empty tabs)
87 type(c_ptr), save :: open_dir_btn_ptr = c_null_ptr
88
89 ! Drawing area pointer (for redrawing treemap)
90 type(c_ptr), save :: drawing_area_ptr = c_null_ptr
91
92 ! Pending navigation state for synthetic paths
93 logical, save :: pending_synthetic_nav = .false.
94 character(len=512), save :: pending_synthetic_child_name = ""
95 integer, save :: synthetic_nav_attempts = 0 ! Count attempts to avoid infinite loops
96
97 contains
98
99 ! Run the Sniffly GTK application
100 function sniffly_app_run() result(status)
101 integer :: status
102 integer(c_int) :: c_status
103
104 ! Initialize GTK
105 call gtk_init()
106
107 ! Create application
108 app_ptr = gtk_application_new(APP_ID//c_null_char, &
109 G_APPLICATION_DEFAULT_FLAGS)
110
111 if (.not. c_associated(app_ptr)) then
112 print *, "ERROR: Failed to create GTK application"
113 status = 1
114 return
115 end if
116
117 ! Connect activate signal (called when app starts)
118 call g_signal_connect(app_ptr, "activate"//c_null_char, &
119 c_funloc(on_activate), c_null_ptr)
120
121 ! Run the application (enters main loop)
122 c_status = g_application_run(app_ptr, 0, [c_null_ptr])
123 status = int(c_status)
124 end function sniffly_app_run
125
126 ! Quit the application
127 subroutine sniffly_app_quit()
128 use progressive_scanner, only: stop_progressive_scan
129 ! Set shutdown flag first to prevent callbacks from accessing widgets
130 app_is_shutting_down = .true.
131
132 ! Stop any active scans before destroying window
133 call stop_progressive_scan()
134
135 if (c_associated(main_window_ptr)) then
136 call gtk_window_destroy(main_window_ptr)
137 end if
138
139 ! Nullify all widget pointers to prevent access after destruction
140 main_window_ptr = c_null_ptr
141 status_label_ptr = c_null_ptr
142 progress_bar_ptr = c_null_ptr
143 path_entry_ptr = c_null_ptr
144 back_btn_ptr = c_null_ptr
145 forward_btn_ptr = c_null_ptr
146 cancel_scan_btn_ptr = c_null_ptr
147 open_dir_btn_ptr = c_null_ptr
148 end subroutine sniffly_app_quit
149
150 ! Set the directory path to scan (call before sniffly_app_run)
151 subroutine sniffly_set_scan_path(path)
152 character(len=*), intent(in) :: path
153 ! Store in legacy global for now - will be used to create first tab in on_activate
154 global_scan_path = trim(path)
155 print *, "Initial scan path set to: ", trim(global_scan_path)
156 end subroutine sniffly_set_scan_path
157
158 ! Callback when window close button (X) is clicked
159 function on_window_close_request(window, user_data) bind(c) result(stop_propagation)
160 use progressive_scanner, only: stop_progressive_scan
161 type(c_ptr), value :: window, user_data
162 integer(c_int) :: stop_propagation
163
164 print *, "Window close button clicked - stopping scan and cleaning up"
165
166 ! Set shutdown flag first to prevent callbacks from accessing widgets
167 app_is_shutting_down = .true.
168
169 ! Stop any active scans
170 call stop_progressive_scan()
171
172 ! Nullify all widget pointers to prevent access after destruction
173 ! (The window will be destroyed by GTK after we return FALSE)
174 status_label_ptr = c_null_ptr
175 progress_bar_ptr = c_null_ptr
176 path_entry_ptr = c_null_ptr
177 back_btn_ptr = c_null_ptr
178 forward_btn_ptr = c_null_ptr
179 open_dir_btn_ptr = c_null_ptr
180 main_window_ptr = c_null_ptr
181
182 ! Return FALSE (0) to allow the window to close
183 stop_propagation = 0_c_int
184 end function on_window_close_request
185
186 ! Callback when application activates (startup)
187 subroutine on_activate(app, user_data) bind(c)
188 type(c_ptr), value :: app, user_data
189 type(c_ptr) :: drawing_area, main_box, toolbar, open_dir_btn, scan_btn, cancel_scan_btn, back_btn, forward_btn, up_btn, open_finder_btn, copy_path_btn, info_btn, toggle_dotfiles_btn, toggle_ext_btn, toggle_render_btn, delete_btn, status_bar, breadcrumb_widget, breadcrumb_row, tab_bar
190 character(len=512) :: scan_path
191 integer(c_int) :: idle_id
192 integer :: first_tab_index
193
194 ! Initialize tab manager
195 call init_tab_manager()
196 print *, "Tab manager initialized"
197
198 ! Create main window
199 main_window_ptr = gtk_application_window_new(app)
200
201 if (.not. c_associated(main_window_ptr)) then
202 print *, "ERROR: Failed to create main window"
203 return
204 end if
205
206 ! Set up window properties
207 call gtk_window_set_title(main_window_ptr, APP_TITLE//c_null_char)
208 call gtk_window_set_default_size(main_window_ptr, &
209 int(DEFAULT_WIDTH, c_int), &
210 int(DEFAULT_HEIGHT, c_int))
211
212 ! Connect close-request signal to handle window close button (X)
213 call g_signal_connect(main_window_ptr, "close-request"//c_null_char, &
214 c_funloc(on_window_close_request), c_null_ptr)
215
216 ! Use global scan path or home directory
217 if (len_trim(global_scan_path) > 0) then
218 scan_path = global_scan_path
219 print *, "Using specified directory: ", trim(scan_path)
220 else
221 ! No directory specified - use home directory and prompt user
222 scan_path = get_home_directory()
223 print *, "No directory specified, using home directory: ", trim(scan_path)
224 print *, "Click the folder icon to select a different directory"
225 end if
226
227 ! Create first tab with initial scan path
228 first_tab_index = create_tab(scan_path)
229 if (first_tab_index < 0) then
230 print *, "ERROR: Failed to create initial tab"
231 return
232 end if
233 print *, "Created initial tab ", first_tab_index, " for: ", trim(scan_path)
234
235 ! Create main vertical box (toolbar + treemap)
236 main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0_c_int)
237
238 ! Create toolbar (horizontal box)
239 toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int)
240
241 ! Create Open Directory button with folder icon
242 open_dir_btn = gtk_button_new()
243 open_dir_btn_ptr = open_dir_btn ! Store for later access (pulsing on empty tabs)
244 call gtk_button_set_icon_name(open_dir_btn, "folder-open"//c_null_char)
245 call gtk_widget_set_tooltip_text(open_dir_btn, "Open Directory (Ctrl+O)"//c_null_char)
246 call g_signal_connect(open_dir_btn, "clicked"//c_null_char, &
247 c_funloc(on_open_dir_clicked), c_null_ptr)
248 call gtk_box_append(toolbar, open_dir_btn)
249
250 ! Create path display entry (read-only)
251 path_entry_ptr = gtk_entry_new()
252 call gtk_editable_set_editable(path_entry_ptr, 0_c_int) ! Make read-only
253 call gtk_widget_set_hexpand(path_entry_ptr, 1_c_int) ! Expand to fill space
254 call gtk_widget_set_tooltip_text(path_entry_ptr, "Current scan path"//c_null_char)
255 call gtk_box_append(toolbar, path_entry_ptr)
256
257 ! Create Scan button with refresh icon
258 scan_btn = gtk_button_new()
259 call gtk_button_set_icon_name(scan_btn, "view-refresh"//c_null_char)
260 call gtk_widget_set_tooltip_text(scan_btn, "Rescan Current Directory"//c_null_char)
261 call g_signal_connect(scan_btn, "clicked"//c_null_char, &
262 c_funloc(on_scan_clicked), c_null_ptr)
263 call gtk_box_append(toolbar, scan_btn)
264
265 ! Create Cancel Scan button with X icon
266 cancel_scan_btn = gtk_button_new()
267 call gtk_button_set_icon_name(cancel_scan_btn, "window-close"//c_null_char)
268 call gtk_widget_set_tooltip_text(cancel_scan_btn, "Cancel Scan"//c_null_char)
269 call g_signal_connect(cancel_scan_btn, "clicked"//c_null_char, &
270 c_funloc(on_cancel_scan_clicked), c_null_ptr)
271 call gtk_box_append(toolbar, cancel_scan_btn)
272 cancel_scan_btn_ptr = cancel_scan_btn ! Store for enabling/disabling
273
274 ! Create Back button (navigate to previous directory in history)
275 back_btn = gtk_button_new()
276 call gtk_button_set_icon_name(back_btn, "go-previous"//c_null_char)
277 call gtk_widget_set_tooltip_text(back_btn, "Navigate Back"//c_null_char)
278 call g_signal_connect(back_btn, "clicked"//c_null_char, &
279 c_funloc(on_back_clicked), c_null_ptr)
280 call gtk_box_append(toolbar, back_btn)
281 back_btn_ptr = back_btn ! Store for enabling/disabling
282
283 ! Create Forward button (navigate to next directory in history)
284 forward_btn = gtk_button_new()
285 call gtk_button_set_icon_name(forward_btn, "go-next"//c_null_char)
286 call gtk_widget_set_tooltip_text(forward_btn, "Navigate Forward"//c_null_char)
287 call g_signal_connect(forward_btn, "clicked"//c_null_char, &
288 c_funloc(on_forward_clicked), c_null_ptr)
289 call gtk_box_append(toolbar, forward_btn)
290 forward_btn_ptr = forward_btn ! Store for enabling/disabling
291
292 ! Create Up to Parent button (navigate to parent directory)
293 up_btn = gtk_button_new()
294 call gtk_button_set_icon_name(up_btn, "go-up"//c_null_char)
295 call gtk_widget_set_tooltip_text(up_btn, "Navigate to Parent Directory (Backspace)"//c_null_char)
296 call g_signal_connect(up_btn, "clicked"//c_null_char, &
297 c_funloc(on_up_clicked), c_null_ptr)
298 call gtk_box_append(toolbar, up_btn)
299
300 ! Initialize Back/Forward button states (disabled until history exists)
301 call update_history_buttons()
302
303 ! Initialize Cancel Scan button state (disabled and grey until scan starts)
304 call update_cancel_scan_button_state()
305
306 ! Initialize selection-dependent button states (disabled until selection exists)
307 call update_selection_buttons()
308
309 ! Create progress bar (always visible but starts at 0%)
310 ! Place it in toolbar, expanded to fill remaining space (pushes to right)
311 progress_bar_ptr = gtk_progress_bar_new()
312 call gtk_progress_bar_set_show_text(progress_bar_ptr, 1_c_int) ! Show percentage text
313 call gtk_widget_set_hexpand(progress_bar_ptr, 1_c_int) ! Expand horizontally to fill space
314 call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double) ! Start at 0%
315 call gtk_box_append(toolbar, progress_bar_ptr)
316
317 ! Create Open in Finder button (floated right after progress bar)
318 open_finder_btn = gtk_button_new()
319 call gtk_button_set_icon_name(open_finder_btn, "document-open"//c_null_char)
320 call gtk_widget_set_tooltip_text(open_finder_btn, "Open in Finder/File Manager"//c_null_char)
321 call g_signal_connect(open_finder_btn, "clicked"//c_null_char, &
322 c_funloc(on_open_finder_clicked), c_null_ptr)
323 call gtk_box_append(toolbar, open_finder_btn)
324 open_finder_btn_ptr = open_finder_btn ! Store for enabling/disabling
325
326 ! Create Copy Path button
327 copy_path_btn = gtk_button_new()
328 call gtk_button_set_icon_name(copy_path_btn, "edit-copy"//c_null_char)
329 call gtk_widget_set_tooltip_text(copy_path_btn, "Copy Path to Clipboard"//c_null_char)
330 call g_signal_connect(copy_path_btn, "clicked"//c_null_char, &
331 c_funloc(on_copy_path_clicked), c_null_ptr)
332 call gtk_box_append(toolbar, copy_path_btn)
333 copy_path_btn_ptr = copy_path_btn ! Store for enabling/disabling
334
335 ! Create Properties/Info button (opens macOS Get Info window)
336 info_btn = gtk_button_new()
337 call gtk_button_set_icon_name(info_btn, "dialog-information"//c_null_char)
338 call gtk_widget_set_tooltip_text(info_btn, "Show in Finder Info"//c_null_char)
339 call g_signal_connect(info_btn, "clicked"//c_null_char, &
340 c_funloc(on_info_clicked), c_null_ptr)
341 call gtk_box_append(toolbar, info_btn)
342 info_btn_ptr = info_btn ! Store for enabling/disabling
343
344 ! View Toggle Buttons (Phase 3 & 5 features)
345
346 ! Toggle Dotfiles button
347 toggle_dotfiles_btn = gtk_button_new()
348 call gtk_button_set_icon_name(toggle_dotfiles_btn, "view-reveal-symbolic"//c_null_char)
349 call gtk_widget_set_tooltip_text(toggle_dotfiles_btn, "Toggle Hidden Files/Dotfiles"//c_null_char)
350 call g_signal_connect(toggle_dotfiles_btn, "clicked"//c_null_char, &
351 c_funloc(on_toggle_dotfiles_clicked), c_null_ptr)
352 call gtk_box_append(toolbar, toggle_dotfiles_btn)
353
354 ! Toggle File Extensions button
355 toggle_ext_btn = gtk_button_new()
356 call gtk_button_set_icon_name(toggle_ext_btn, "text-x-generic-symbolic"//c_null_char)
357 call gtk_widget_set_tooltip_text(toggle_ext_btn, "Toggle File Extensions in Labels"//c_null_char)
358 call g_signal_connect(toggle_ext_btn, "clicked"//c_null_char, &
359 c_funloc(on_toggle_extensions_clicked), c_null_ptr)
360 call gtk_box_append(toolbar, toggle_ext_btn)
361
362 ! Toggle Render Mode button (Flat vs Cushioned)
363 toggle_render_btn = gtk_button_new()
364 call gtk_button_set_icon_name(toggle_render_btn, "view-grid-symbolic"//c_null_char)
365 call gtk_widget_set_tooltip_text(toggle_render_btn, "Toggle Flat/3D Rendering Mode"//c_null_char)
366 call g_signal_connect(toggle_render_btn, "clicked"//c_null_char, &
367 c_funloc(on_toggle_render_mode_clicked), c_null_ptr)
368 call gtk_box_append(toolbar, toggle_render_btn)
369
370 ! Create Delete button
371 delete_btn = gtk_button_new()
372 call gtk_button_set_icon_name(delete_btn, "user-trash"//c_null_char)
373 call gtk_widget_set_tooltip_text(delete_btn, "Delete to Trash (D key)"//c_null_char)
374 call g_signal_connect(delete_btn, "clicked"//c_null_char, &
375 c_funloc(on_delete_clicked), c_null_ptr)
376 call gtk_box_append(toolbar, delete_btn)
377 delete_btn_ptr = delete_btn ! Store for enabling/disabling
378
379 ! Add toolbar to main box
380 call gtk_box_append(main_box, toolbar)
381
382 ! Create horizontal box for breadcrumb row (breadcrumb + tab bar)
383 breadcrumb_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int)
384
385 ! Create custom Cairo breadcrumb widget
386 breadcrumb_widget = create_breadcrumb_widget()
387 if (.not. c_associated(breadcrumb_widget)) then
388 print *, "ERROR: Failed to create breadcrumb widget"
389 return
390 end if
391
392 ! Register navigation callback for breadcrumb
393 call set_navigation_callback(breadcrumb_callback)
394
395 ! Make breadcrumb expand to fill space (pushes tab bar to right)
396 call gtk_widget_set_hexpand(breadcrumb_widget, 1_c_int)
397
398 ! Add breadcrumb widget to breadcrumb row
399 call gtk_box_append(breadcrumb_row, breadcrumb_widget)
400
401 ! Create tab bar (on right side of breadcrumb row)
402 tab_bar = create_tab_bar()
403 if (c_associated(tab_bar)) then
404 call gtk_box_append(breadcrumb_row, tab_bar)
405 ! Populate with tabs
406 call refresh_tab_bar()
407 ! Register callback for tab switching to update UI
408 call register_tab_switch_callback(update_ui_for_active_tab)
409 print *, "Tab bar added to breadcrumb row"
410 else
411 print *, "ERROR: Failed to create tab bar"
412 end if
413
414 ! Add breadcrumb row to main box
415 call gtk_box_append(main_box, breadcrumb_row)
416
417 ! Create treemap drawing area widget
418 drawing_area = create_treemap_widget()
419
420 if (.not. c_associated(drawing_area)) then
421 print *, "ERROR: Failed to create treemap widget"
422 return
423 end if
424
425 ! Store pointer for later use (e.g., tab switching redraw)
426 drawing_area_ptr = drawing_area
427
428 ! Make drawing area expand to fill space
429 call gtk_widget_set_hexpand(drawing_area, 1_c_int)
430 call gtk_widget_set_vexpand(drawing_area, 1_c_int)
431
432 ! Register widget with renderer for progressive scan redraws
433 call set_redraw_widget(drawing_area)
434
435 ! Set the scan path
436 call set_scan_path(scan_path)
437
438 ! Update path entry to show initial scan path
439 call update_path_entry(scan_path)
440
441 ! Register navigation callback for breadcrumb updates
442 call register_navigation_callback(breadcrumb_callback)
443
444 ! Register quit callback
445 call register_quit_callback(quit_callback_wrapper)
446
447 ! Register delete callback
448 call register_delete_callback(delete_callback_wrapper)
449
450 ! Register force refresh callback
451 call register_refresh_callback(refresh_callback_wrapper)
452
453 ! Register selection change callback for button state updates
454 call register_selection_callback(update_selection_buttons)
455 print *, "Selection callback registered"
456
457 ! Register progress callbacks
458 call register_progress_callback(sniffly_show_progress, sniffly_hide_progress, &
459 sniffly_update_progress)
460
461 ! Register scan completion callback (wrapped to update button state)
462 call register_scan_completion_callback(scan_complete_callback_wrapper)
463 print *, "Scan completion callback registered"
464
465 ! Add drawing area to main box
466 call gtk_box_append(main_box, drawing_area)
467
468 ! Create status bar (horizontal box with label only)
469 status_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int)
470 status_label_ptr = gtk_label_new("Preparing to scan..."//c_null_char)
471 call gtk_widget_set_halign(status_label_ptr, GTK_ALIGN_START)
472 call gtk_box_append(status_bar, status_label_ptr)
473
474 ! Add status bar to main box
475 call gtk_box_append(main_box, status_bar)
476
477 ! Add main box to window
478 call gtk_window_set_child(main_window_ptr, main_box)
479
480 ! Register keyboard handler on window (not widget) for global keyboard capture
481 call register_key_handler(main_window_ptr)
482
483 ! Load custom CSS (including pulsing animation for suggested-action)
484 call load_custom_css()
485
486 ! Show the window first with "Scanning..." status
487 call gtk_window_present(main_window_ptr)
488
489 ! Store scan path for idle callback
490 pending_scan_path = scan_path
491
492 ! Schedule initial scan to run when GTK is idle (after window is shown)
493 idle_id = g_idle_add(c_funloc(perform_initial_scan), c_null_ptr)
494
495 print *, "Sniffly started successfully! Scan will begin shortly..."
496 print *, "Window size: ", DEFAULT_WIDTH, "x", DEFAULT_HEIGHT
497 end subroutine on_activate
498
499 ! Load custom CSS for animations and styling
500 subroutine load_custom_css()
501 type(c_ptr) :: css_provider, display
502 character(len=:), allocatable :: css_data
503
504 ! CSS with pulsing animation for suggested-action class (empty tabs)
505 css_data = &
506 "@keyframes pulse { " // &
507 "0% { opacity: 1.0; } " // &
508 "50% { opacity: 0.5; } " // &
509 "100% { opacity: 1.0; } " // &
510 "} " // &
511 ".suggested-action { " // &
512 "animation: pulse 1.5s ease-in-out infinite; " // &
513 "}"
514
515 ! Create CSS provider
516 css_provider = gtk_css_provider_new()
517
518 ! Load CSS from string
519 call gtk_css_provider_load_from_string(css_provider, trim(css_data)//c_null_char)
520
521 ! Get default display
522 display = gdk_display_get_default()
523
524 ! Add CSS provider to display (600 = GTK_STYLE_PROVIDER_PRIORITY_APPLICATION)
525 call gtk_style_context_add_provider_for_display(display, css_provider, 600_c_int)
526
527 print *, "Custom CSS loaded (pulsing animation for empty tabs)"
528 end subroutine load_custom_css
529
530 ! Callback when Open Directory button is clicked
531 ! NOTE: Uses system command for file picking until GTK4 file dialog bindings are available
532 subroutine on_open_dir_clicked(button, user_data) bind(c)
533 type(c_ptr), value :: button, user_data
534 type(tab_state), pointer :: tab
535 character(len=1024) :: selected_path
536 integer :: status
537
538 print *, "Open Directory button clicked!"
539
540 ! Get active tab
541 tab => get_active_tab()
542 if (.not. associated(tab)) then
543 print *, "ERROR: No active tab in on_open_dir_clicked"
544 return
545 end if
546
547 ! Call helper to show native file picker
548 call show_native_directory_picker(selected_path, status)
549
550 if (status == 0 .and. len_trim(selected_path) > 0) then
551 print *, "Selected directory: ", trim(selected_path)
552
553 ! Update tab scan path (but don't scan yet)
554 ! Remove trailing slash if present (C code doesn't like it)
555 if (len_trim(selected_path) > 1 .and. selected_path(len_trim(selected_path):len_trim(selected_path)) == '/') then
556 tab%scan_path = trim(selected_path(1:len_trim(selected_path)-1))
557 print *, "DEBUG: Removed trailing slash from path"
558 else
559 tab%scan_path = trim(selected_path)
560 end if
561 print *, "DEBUG: Set tab scan_path to: '", trim(tab%scan_path), "'"
562 call set_scan_path(trim(tab%scan_path))
563
564 ! Update path display entry
565 call update_path_entry(trim(tab%scan_path))
566
567 print *, "Path updated. Click Scan button to scan: ", trim(tab%scan_path)
568 else
569 print *, "Directory selection cancelled or failed"
570 end if
571 end subroutine on_open_dir_clicked
572
573 ! Callback when Scan button is clicked
574 subroutine on_scan_clicked(button, user_data) bind(c)
575 type(c_ptr), value :: button, user_data
576 type(tab_state), pointer :: tab
577
578 ! Get active tab
579 tab => get_active_tab()
580 if (.not. associated(tab)) then
581 print *, "ERROR: No active tab in on_scan_clicked"
582 return
583 end if
584
585 if (len_trim(tab%scan_path) == 0) then
586 call sniffly_show_error("No directory to scan")
587 return
588 end if
589
590 ! Trigger a rescan of the current path (uses cache for speed)
591 call sniffly_update_status("Rescanning...")
592 call trigger_rescan(tab%scan_path)
593 end subroutine on_scan_clicked
594
595 ! Callback when Cancel Scan button is clicked
596 subroutine on_cancel_scan_clicked(button, user_data) bind(c)
597 use progressive_scanner, only: stop_progressive_scan, is_scan_active
598 type(c_ptr), value :: button, user_data
599
600 print *, "Cancel scan button clicked!"
601
602 if (is_scan_active()) then
603 call sniffly_update_status("Cancelling scan...")
604 call stop_progressive_scan()
605 ! Update button states (cancel button disabled, back/forward re-enabled if history exists)
606 call update_cancel_scan_button_state()
607 call update_history_buttons()
608 call sniffly_update_status("Scan cancelled")
609 end if
610 end subroutine on_cancel_scan_clicked
611
612 ! Callback when Open in Finder button is clicked
613 subroutine on_open_finder_clicked(button, user_data) bind(c)
614 type(c_ptr), value :: button, user_data
615 type(tab_state), pointer :: tab
616 character(len=:), allocatable :: selected_path
617
618 print *, "Open in Finder button clicked!"
619
620 ! Check if there's a selection
621 if (has_selection()) then
622 ! Get the selected node path
623 selected_path = get_selected_node_path()
624 if (len_trim(selected_path) == 0) then
625 print *, "Invalid selection path"
626 return
627 end if
628 print *, "Opening selected item in Finder: ", trim(selected_path)
629 else
630 ! No selection - use current directory
631 ! Get active tab
632 tab => get_active_tab()
633 if (.not. associated(tab)) then
634 print *, "ERROR: No active tab in on_open_finder_clicked"
635 return
636 end if
637
638 if (len_trim(tab%scan_path) == 0) then
639 print *, "No current directory to open"
640 call sniffly_show_error("No directory to open in Finder")
641 return
642 end if
643 selected_path = trim(tab%scan_path)
644 print *, "Opening current directory in Finder: ", trim(selected_path)
645 end if
646
647 call open_in_file_manager(selected_path)
648 call sniffly_update_status("Opened in Finder: " // trim(selected_path))
649 end subroutine on_open_finder_clicked
650
651 ! Callback when Copy Path button is clicked
652 subroutine on_copy_path_clicked(button, user_data) bind(c)
653 type(c_ptr), value :: button, user_data
654 character(len=:), allocatable :: selected_path
655 type(c_ptr) :: display, clipboard
656
657 print *, "Copy Path button clicked!"
658
659 ! Check if there's a selection
660 if (.not. has_selection()) then
661 print *, "No selection - cannot copy path"
662 call sniffly_show_error("No selection to copy")
663 return
664 end if
665
666 ! Get the selected node path
667 selected_path = get_selected_node_path()
668
669 if (len_trim(selected_path) == 0) then
670 print *, "Invalid selection path"
671 call sniffly_show_error("Invalid selection path")
672 return
673 end if
674
675 print *, "Copying path to clipboard: ", trim(selected_path)
676
677 ! Get the default display
678 display = gdk_display_get_default()
679 if (.not. c_associated(display)) then
680 print *, "ERROR: Failed to get default display"
681 call sniffly_show_error("Failed to access clipboard")
682 return
683 end if
684
685 ! Get the clipboard from the display
686 clipboard = gdk_display_get_clipboard(display)
687 if (.not. c_associated(clipboard)) then
688 print *, "ERROR: Failed to get clipboard"
689 call sniffly_show_error("Failed to access clipboard")
690 return
691 end if
692
693 ! Convert Fortran string to C string and set clipboard
694 call gdk_clipboard_set_text(clipboard, trim(selected_path)//c_null_char)
695
696 ! Update status
697 call sniffly_update_status("Path copied to clipboard: " // trim(selected_path))
698 print *, "Path copied successfully!"
699 end subroutine on_copy_path_clicked
700
701 ! Helper: Build selection info string for status bar
702 function build_selection_info() result(info_text)
703 use iso_fortran_env, only: int64
704 use treemap_renderer, only: get_current_view_node
705 use treemap_widget, only: get_selected_index
706 use types, only: file_node
707 use file_system, only: list_directory
708 character(len=1024) :: info_text
709 character(len=20) :: size_str
710 type(file_node), pointer :: view_node
711 integer(int64) :: size_bytes
712 integer :: item_count, selected_idx
713 character(len=256), dimension(10000) :: entries
714
715 info_text = ""
716
717 ! Get the selected node
718 view_node => get_current_view_node()
719 if (.not. associated(view_node)) then
720 return
721 end if
722
723 ! Get the selected child index (1-based)
724 selected_idx = get_selected_index()
725 if (selected_idx < 1 .or. selected_idx > view_node%num_children) then
726 return
727 end if
728
729 ! Get details from selected child
730 size_bytes = view_node%children(selected_idx)%size
731
732 ! Check if this is a grouped "[N small files]" node
733 if (index(view_node%children(selected_idx)%name, '[') == 1 .and. &
734 index(view_node%children(selected_idx)%name, 'small files]') > 0) then
735 item_count = -1 ! Special marker for grouped nodes
736 else if (view_node%children(selected_idx)%is_directory) then
737 ! For regular directories, count entries on-demand
738 item_count = list_directory(view_node%children(selected_idx)%path, entries, 10000)
739 else
740 item_count = 0 ! Files don't have children
741 end if
742
743 ! Format size
744 if (size_bytes < 1024_int64) then
745 write(size_str, '(I0,A)') size_bytes, ' B'
746 else if (size_bytes < 1024_int64**2) then
747 write(size_str, '(F0.2,A)') real(size_bytes)/1024.0, ' KB'
748 else if (size_bytes < 1024_int64**3) then
749 write(size_str, '(F0.2,A)') real(size_bytes)/(1024.0**2), ' MB'
750 else
751 write(size_str, '(F0.2,A)') real(size_bytes)/(1024.0**3), ' GB'
752 end if
753
754 ! Build info text
755 if (item_count == -1) then
756 ! Grouped small files - name already contains the count
757 write(info_text, '(A,A,A)') &
758 trim(view_node%children(selected_idx)%name), ' | ', trim(size_str)
759 else if (view_node%children(selected_idx)%is_directory) then
760 ! Regular directory
761 write(info_text, '(A,A,A,A,I0,A)') &
762 trim(view_node%children(selected_idx)%name), ' | ', trim(size_str), ' | ', item_count, ' items'
763 else
764 ! Regular file
765 write(info_text, '(A,A,A,A)') &
766 trim(view_node%children(selected_idx)%name), ' | ', trim(size_str), ' | File'
767 end if
768 end function build_selection_info
769
770 ! Callback when Properties/Info button is clicked - opens macOS Get Info window
771 subroutine on_info_clicked(button, user_data) bind(c)
772 type(c_ptr), value :: button, user_data
773 character(len=512) :: selected_path
774 character(len=1024) :: applescript_cmd
775 integer :: exit_status
776
777 print *, "Info button clicked - opening macOS Get Info window"
778
779 ! Check if there's a selection
780 if (.not. has_selection()) then
781 call sniffly_show_error("No selection to show info for")
782 return
783 end if
784
785 ! Get the selected node path
786 selected_path = get_selected_node_path()
787 if (len_trim(selected_path) == 0) then
788 print *, "Invalid selection path"
789 call sniffly_show_error("Invalid selection")
790 return
791 end if
792
793 print *, "Opening Get Info for: ", trim(selected_path)
794
795 ! Build AppleScript command to open Get Info window
796 ! We escape single quotes in the path by replacing ' with '\''
797 write(applescript_cmd, '(A,A,A)') &
798 'osascript -e ''tell application "Finder" to open information window of (POSIX file "', &
799 trim(selected_path), '" as alias)'''
800
801 ! Execute the command
802 call execute_command_line(trim(applescript_cmd), exitstat=exit_status)
803
804 if (exit_status /= 0) then
805 print *, "ERROR: Failed to open Get Info window (exit status:", exit_status, ")"
806 call sniffly_show_error("Failed to open Get Info window")
807 else
808 print *, "Successfully opened Get Info window"
809 end if
810 end subroutine on_info_clicked
811
812 ! Helper: Update Back/Forward button states
813 subroutine update_history_buttons()
814 use gtk, only: gtk_widget_set_sensitive
815 use progressive_scanner, only: is_scan_active
816 type(tab_state), pointer :: tab
817 logical :: scan_active
818
819 ! Guard against accessing widgets during shutdown
820 if (app_is_shutting_down) return
821 if (.not. c_associated(back_btn_ptr) .or. .not. c_associated(forward_btn_ptr)) return
822
823 ! Get active tab
824 tab => get_active_tab()
825 if (.not. associated(tab)) then
826 ! No active tab - disable buttons
827 call gtk_widget_set_sensitive(back_btn_ptr, 0_c_int)
828 call gtk_widget_set_sensitive(forward_btn_ptr, 0_c_int)
829 return
830 end if
831
832 ! Check if scan is active - disable buttons during scan
833 scan_active = is_scan_active()
834
835 print *, "=== UPDATE_HISTORY_BUTTONS ==="
836 print *, " pos=", tab%nav_history_pos, " count=", tab%nav_history_count, " scan_active=", scan_active
837
838 ! Enable Back if we're not at the start of history AND scan is not active
839 if (tab%nav_history_pos > 1 .and. .not. scan_active) then
840 print *, " Enabling Back (pos > 1 and scan not active)"
841 call gtk_widget_set_sensitive(back_btn_ptr, 1_c_int)
842 else
843 print *, " Disabling Back (pos=", tab%nav_history_pos, " or scan active)"
844 call gtk_widget_set_sensitive(back_btn_ptr, 0_c_int)
845 end if
846
847 ! Enable Forward if we're not at the end of history AND scan is not active
848 if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count .and. .not. scan_active) then
849 print *, " Enabling Forward (pos < count and scan not active)"
850 call gtk_widget_set_sensitive(forward_btn_ptr, 1_c_int)
851 else
852 print *, " Disabling Forward (pos=", tab%nav_history_pos, " count=", tab%nav_history_count, " or scan active)"
853 call gtk_widget_set_sensitive(forward_btn_ptr, 0_c_int)
854 end if
855 end subroutine update_history_buttons
856
857 ! Helper: Update Cancel Scan button state based on scan status
858 subroutine update_cancel_scan_button_state()
859 use progressive_scanner, only: is_scan_active
860 use gtk, only: gtk_widget_set_sensitive
861
862 ! Guard against accessing widgets during shutdown
863 if (app_is_shutting_down) return
864 if (.not. c_associated(cancel_scan_btn_ptr)) return
865
866 print *, "DEBUG: Updating cancel button state, scan active =", is_scan_active()
867
868 if (is_scan_active()) then
869 ! Scan is active - enable button and make it red
870 print *, "DEBUG: Enabling cancel button (making it red)"
871 call gtk_widget_add_css_class(cancel_scan_btn_ptr, "destructive-action"//c_null_char)
872 call gtk_widget_set_sensitive(cancel_scan_btn_ptr, 1_c_int)
873 else
874 ! No scan active - remove red styling first, then disable button
875 print *, "DEBUG: Disabling cancel button (making it grey)"
876 call gtk_widget_remove_css_class(cancel_scan_btn_ptr, "destructive-action"//c_null_char)
877 call gtk_widget_set_sensitive(cancel_scan_btn_ptr, 0_c_int)
878 end if
879 end subroutine update_cancel_scan_button_state
880
881 ! Helper: Update selection-dependent button states and display selection info
882 subroutine update_selection_buttons()
883 use gtk, only: gtk_widget_set_sensitive
884 logical :: has_sel
885 character(len=1024) :: sel_info
886
887 ! Guard against accessing widgets during shutdown
888 if (app_is_shutting_down) return
889 if (.not. c_associated(info_btn_ptr)) return
890
891 ! Check if there's a selection
892 has_sel = has_selection()
893
894 print *, "DEBUG: update_selection_buttons() called, has_selection =", has_sel
895
896 ! Enable/disable selection-dependent buttons
897 if (has_sel) then
898 print *, "DEBUG: Enabling selection-dependent buttons"
899 call gtk_widget_set_sensitive(info_btn_ptr, 1_c_int)
900 call gtk_widget_set_sensitive(copy_path_btn_ptr, 1_c_int)
901 call gtk_widget_set_sensitive(delete_btn_ptr, 1_c_int)
902
903 ! Auto-display selection info in status bar
904 sel_info = build_selection_info()
905 if (len_trim(sel_info) > 0) then
906 call sniffly_update_status(trim(sel_info))
907 end if
908 else
909 print *, "DEBUG: Disabling selection-dependent buttons"
910 call gtk_widget_set_sensitive(info_btn_ptr, 0_c_int)
911 call gtk_widget_set_sensitive(copy_path_btn_ptr, 0_c_int)
912 call gtk_widget_set_sensitive(delete_btn_ptr, 0_c_int)
913
914 ! Clear selection info from status bar when deselected
915 call sniffly_update_status("")
916 end if
917
918 ! Open in Finder button is always enabled (defaults to current directory)
919 call gtk_widget_set_sensitive(open_finder_btn_ptr, 1_c_int)
920 end subroutine update_selection_buttons
921
922 ! Update UI to reflect the active tab's state (called when switching tabs)
923 subroutine update_ui_for_active_tab()
924 use gtk, only: gtk_widget_queue_draw
925 use types, only: file_node
926 type(tab_state), pointer :: tab
927 type(file_node), pointer :: current_view
928
929 print *, "=== UPDATE_UI_FOR_ACTIVE_TAB ==="
930
931 ! Get active tab
932 tab => get_active_tab()
933 if (.not. associated(tab)) then
934 print *, "ERROR: No active tab"
935 return
936 end if
937
938 print *, " Active tab index: ", active_tab_index
939 print *, " Tab has_data: ", tab%has_data
940 print *, " Tab scan_path: ", trim(tab%scan_path)
941
942 ! Sync treemap widget's scan path with active tab's scan path
943 call set_scan_path(trim(tab%scan_path))
944
945 ! Sync renderer state with active tab (CRITICAL for correct rendering)
946 call set_renderer_state_from_tab(tab%root_node, tab%current_view_node, tab%has_data)
947
948 ! Check if tab has data
949 if (.not. tab%has_data) then
950 print *, " Tab has no data yet - showing empty tab UI"
951
952 ! Add blue suggested-action class to open-dir button to draw attention
953 if (c_associated(open_dir_btn_ptr)) then
954 call gtk_widget_add_css_class(open_dir_btn_ptr, "suggested-action"//c_null_char)
955 end if
956
957 ! Clear breadcrumb display
958 call update_breadcrumb_cache("")
959
960 ! Clear path entry
961 call update_path_entry("")
962
963 ! Update status bar to guide user
964 call sniffly_update_status("No directory selected - click the folder icon to choose a directory")
965
966 ! Clear the treemap drawing (redraw with no data will show blank)
967 if (c_associated(drawing_area_ptr)) then
968 call gtk_widget_queue_draw(drawing_area_ptr)
969 end if
970
971 return
972 end if
973
974 ! Tab has data - remove suggested-action class from open-dir button
975 if (c_associated(open_dir_btn_ptr)) then
976 call gtk_widget_remove_css_class(open_dir_btn_ptr, "suggested-action"//c_null_char)
977 end if
978
979 ! Get the current view node
980 current_view => tab%current_view_node
981 if (.not. associated(current_view)) then
982 print *, "ERROR: Tab has_data=true but current_view_node not associated"
983 return
984 end if
985
986 print *, " Updating breadcrumb for: ", trim(current_view%path)
987
988 ! Update breadcrumb cache
989 call update_breadcrumb_cache(trim(current_view%path))
990
991 ! Trigger treemap redraw
992 if (c_associated(drawing_area_ptr)) then
993 call gtk_widget_queue_draw(drawing_area_ptr)
994 print *, " Triggered treemap redraw"
995 end if
996
997 ! Update navigation buttons
998 call update_history_buttons()
999
1000 ! Update path entry
1001 call update_path_entry(trim(current_view%path))
1002
1003 print *, "=== UI UPDATE COMPLETE ==="
1004 end subroutine update_ui_for_active_tab
1005
1006 ! Helper: Add path to navigation history
1007 subroutine add_to_history(path)
1008 character(len=*), intent(in) :: path
1009 type(tab_state), pointer :: tab
1010 integer :: i
1011
1012 print *, "=== ADD_TO_HISTORY CALLED ==="
1013 print *, " Path: ", trim(path)
1014
1015 ! Get active tab
1016 tab => get_active_tab()
1017 if (.not. associated(tab)) then
1018 print *, "ERROR: No active tab in add_to_history"
1019 return
1020 end if
1021
1022 print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count
1023 if (tab%nav_history_count > 0) then
1024 print *, " Current history:"
1025 do i = 1, tab%nav_history_count
1026 if (i == tab%nav_history_pos) then
1027 print *, " [", i, "] (CURRENT) ", trim(tab%nav_history(i))
1028 else
1029 print *, " [", i, "] ", trim(tab%nav_history(i))
1030 end if
1031 end do
1032 end if
1033
1034 ! Don't add if it's the same as current position
1035 if (tab%nav_history_pos > 0 .and. tab%nav_history_pos <= tab%nav_history_count) then
1036 if (trim(tab%nav_history(tab%nav_history_pos)) == trim(path)) then
1037 print *, " Path same as current position - not adding"
1038 return
1039 end if
1040 end if
1041
1042 ! If we're in the middle of history, discard forward history
1043 if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then
1044 print *, " In middle of history - truncating forward history"
1045 print *, " Truncating count from", tab%nav_history_count, "to", tab%nav_history_pos
1046 tab%nav_history_count = tab%nav_history_pos
1047 end if
1048
1049 ! Add to history
1050 if (tab%nav_history_count < MAX_HISTORY) then
1051 tab%nav_history_count = tab%nav_history_count + 1
1052 tab%nav_history(tab%nav_history_count) = trim(path)
1053 print *, " Added to history at position", tab%nav_history_count
1054 else
1055 ! Shift history left and add at end
1056 print *, " History full - shifting left"
1057 do i = 1, MAX_HISTORY - 1
1058 tab%nav_history(i) = tab%nav_history(i + 1)
1059 end do
1060 tab%nav_history(MAX_HISTORY) = trim(path)
1061 end if
1062
1063 tab%nav_history_pos = tab%nav_history_count
1064 print *, " After: pos=", tab%nav_history_pos, " count=", tab%nav_history_count
1065 print *, "=== END ADD_TO_HISTORY ==="
1066 call update_history_buttons()
1067 end subroutine add_to_history
1068
1069 ! Navigate to a synthetic path (grouped small files node) by tree traversal
1070 subroutine navigate_to_synthetic_path(synthetic_path)
1071 use treemap_renderer, only: scan_directory
1072 character(len=*), intent(in) :: synthetic_path
1073 character(len=512) :: parent_path, node_name
1074 integer :: last_sep, i
1075
1076 ! Extract parent path and node name
1077 last_sep = 0
1078 do i = len_trim(synthetic_path), 1, -1
1079 if (synthetic_path(i:i) == '/') then
1080 last_sep = i
1081 exit
1082 end if
1083 end do
1084
1085 if (last_sep > 0) then
1086 parent_path = synthetic_path(1:last_sep-1)
1087 node_name = synthetic_path(last_sep+1:len_trim(synthetic_path))
1088 else
1089 print *, "ERROR: Invalid synthetic path format"
1090 return
1091 end if
1092
1093 print *, " Parent path: ", trim(parent_path)
1094 print *, " Node name: ", trim(node_name)
1095
1096 ! Set up pending navigation
1097 pending_synthetic_nav = .true.
1098 pending_synthetic_child_name = trim(node_name)
1099
1100 ! Navigate to parent directory - after scan completes, breadcrumb_callback will handle navigation
1101 call set_scan_path(trim(parent_path))
1102 call update_path_entry(trim(parent_path))
1103 call trigger_rescan(parent_path)
1104 call sniffly_update_status("Navigating to grouped files...")
1105 end subroutine navigate_to_synthetic_path
1106
1107 ! Complete pending synthetic navigation (called after scan completes)
1108 subroutine complete_synthetic_navigation()
1109 use treemap_renderer, only: get_current_view_node, navigate_into_node, invalidate_layout
1110 use treemap_widget, only: get_widget_ptr
1111 use gtk, only: gtk_widget_queue_draw
1112 use types, only: file_node
1113 type(file_node), pointer :: view_node
1114 type(c_ptr) :: widget
1115 integer :: i
1116
1117 if (.not. pending_synthetic_nav) return
1118
1119 synthetic_nav_attempts = synthetic_nav_attempts + 1
1120 print *, "=== COMPLETING SYNTHETIC NAVIGATION (attempt ", synthetic_nav_attempts, ") ==="
1121 print *, " Looking for child: ", trim(pending_synthetic_child_name)
1122
1123 view_node => get_current_view_node()
1124 if (.not. associated(view_node)) then
1125 print *, "ERROR: No current view node"
1126 pending_synthetic_nav = .false.
1127 synthetic_nav_attempts = 0
1128 return
1129 end if
1130
1131 if (.not. allocated(view_node%children)) then
1132 print *, "ERROR: Current node has no children"
1133 pending_synthetic_nav = .false.
1134 synthetic_nav_attempts = 0
1135 return
1136 end if
1137
1138 print *, " Current view has ", view_node%num_children, " children"
1139
1140 ! If we have way too many children, grouping hasn't happened yet - wait for next callback
1141 if (view_node%num_children > 100 .and. synthetic_nav_attempts < 5) then
1142 print *, " Too many children (", view_node%num_children, ") - grouping not done yet, waiting..."
1143 return ! Keep pending flag true, will retry on next callback
1144 end if
1145
1146 ! Find child with matching name
1147 do i = 1, view_node%num_children
1148 if (allocated(view_node%children(i)%name)) then
1149 if (trim(view_node%children(i)%name) == trim(pending_synthetic_child_name)) then
1150 print *, " Found child at index ", i, ": '", trim(view_node%children(i)%name), "'"
1151 ! Navigate into the child
1152 call navigate_into_node(i)
1153
1154 ! Update UI
1155 call invalidate_layout()
1156 widget = get_widget_ptr()
1157 if (c_associated(widget)) then
1158 call gtk_widget_queue_draw(widget)
1159 end if
1160
1161 ! Trigger breadcrumb update
1162 call breadcrumb_callback()
1163
1164 pending_synthetic_nav = .false.
1165 synthetic_nav_attempts = 0
1166 call sniffly_update_status("Navigated to grouped files")
1167 return
1168 end if
1169 end if
1170 end do
1171
1172 ! Give up after several attempts
1173 if (synthetic_nav_attempts >= 5) then
1174 print *, "ERROR: Could not find child after ", synthetic_nav_attempts, " attempts"
1175 print *, " Searched for: '", trim(pending_synthetic_child_name), "'"
1176 pending_synthetic_nav = .false.
1177 synthetic_nav_attempts = 0
1178 call sniffly_show_error("Could not find grouped files node")
1179 else
1180 print *, " Child not found yet, will retry on next callback"
1181 end if
1182 end subroutine complete_synthetic_navigation
1183
1184 ! Callback when Back button is clicked
1185 subroutine on_back_clicked(button, user_data) bind(c)
1186 use progressive_scanner, only: is_scan_active
1187 type(c_ptr), value :: button, user_data
1188 type(tab_state), pointer :: tab
1189
1190 print *, "=== BACK BUTTON CLICKED ==="
1191
1192 ! Get active tab
1193 tab => get_active_tab()
1194 if (.not. associated(tab)) then
1195 print *, "ERROR: No active tab in on_back_clicked"
1196 return
1197 end if
1198
1199 print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count
1200
1201 ! Block navigation if scan is active
1202 if (is_scan_active()) then
1203 call sniffly_show_error("Cannot navigate: Scan in progress")
1204 return
1205 end if
1206
1207 if (tab%nav_history_pos > 1) then
1208 tab%nav_history_pos = tab%nav_history_pos - 1
1209 tab%scan_path = trim(tab%nav_history(tab%nav_history_pos))
1210 print *, " Moving back to pos=", tab%nav_history_pos
1211 print *, " Path: ", trim(tab%scan_path)
1212 tab%navigating_history = .true. ! Set flag before triggering rescan
1213 call set_scan_path(trim(tab%scan_path))
1214 call update_path_entry(trim(tab%scan_path))
1215 call trigger_rescan(tab%scan_path)
1216 call update_history_buttons()
1217 call sniffly_update_status("Navigated back to: " // trim(tab%scan_path))
1218 end if
1219 end subroutine on_back_clicked
1220
1221 ! Callback when Forward button is clicked
1222 subroutine on_forward_clicked(button, user_data) bind(c)
1223 use progressive_scanner, only: is_scan_active
1224 type(c_ptr), value :: button, user_data
1225 type(tab_state), pointer :: tab
1226 logical :: is_synthetic
1227
1228 print *, "=== FORWARD BUTTON CLICKED ==="
1229
1230 ! Get active tab
1231 tab => get_active_tab()
1232 if (.not. associated(tab)) then
1233 print *, "ERROR: No active tab in on_forward_clicked"
1234 return
1235 end if
1236
1237 print *, " Before: pos=", tab%nav_history_pos, " count=", tab%nav_history_count
1238
1239 ! Block navigation if scan is active
1240 if (is_scan_active()) then
1241 call sniffly_show_error("Cannot navigate: Scan in progress")
1242 return
1243 end if
1244
1245 if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then
1246 tab%nav_history_pos = tab%nav_history_pos + 1
1247 tab%scan_path = trim(tab%nav_history(tab%nav_history_pos))
1248 print *, " Moving forward to pos=", tab%nav_history_pos
1249 print *, " Path: ", trim(tab%scan_path)
1250
1251 tab%navigating_history = .true. ! Set flag before triggering rescan
1252
1253 ! Check if this is a synthetic path (grouped small files node)
1254 is_synthetic = (index(tab%scan_path, '[') > 0 .and. &
1255 index(tab%scan_path, 'small files]') > 0)
1256
1257 if (is_synthetic) then
1258 print *, " Synthetic path detected - navigating by tree traversal"
1259 call navigate_to_synthetic_path(tab%scan_path)
1260 else
1261 call set_scan_path(trim(tab%scan_path))
1262 call update_path_entry(trim(tab%scan_path))
1263 call trigger_rescan(tab%scan_path)
1264 end if
1265
1266 call update_history_buttons()
1267 call sniffly_update_status("Navigated forward to: " // trim(tab%scan_path))
1268 end if
1269 end subroutine on_forward_clicked
1270
1271 ! Callback when Up to Parent button is clicked
1272 subroutine on_up_clicked(button, user_data) bind(c)
1273 use treemap_renderer, only: navigate_up
1274 use gtk, only: gtk_widget_queue_draw
1275 type(c_ptr), value :: button, user_data
1276
1277 ! Use the existing navigate_up functionality from treemap_renderer
1278 call navigate_up()
1279
1280 ! Update breadcrumbs and history
1281 call breadcrumb_callback()
1282
1283 ! Trigger redraw
1284 if (c_associated(main_window_ptr)) then
1285 call gtk_widget_queue_draw(main_window_ptr)
1286 end if
1287
1288 call sniffly_update_status("Navigated to parent directory")
1289 end subroutine on_up_clicked
1290
1291 ! Callback when Delete button is clicked
1292 subroutine on_delete_clicked(button, user_data) bind(c)
1293 use treemap_widget, only: get_selected_index
1294 type(c_ptr), value :: button, user_data
1295 character(len=:), allocatable :: selected_path
1296 integer :: confirm_result, selected_idx
1297
1298 print *, "Delete button clicked!"
1299
1300 ! Check if there's a selection
1301 if (.not. has_selection()) then
1302 print *, "No selection - cannot delete"
1303 return
1304 end if
1305
1306 ! Get the selected node path and index
1307 selected_path = get_selected_node_path()
1308 selected_idx = get_selected_index()
1309
1310 if (len_trim(selected_path) == 0) then
1311 print *, "Invalid selection path"
1312 return
1313 end if
1314
1315 print *, "Preparing to delete: ", trim(selected_path)
1316
1317 ! Show confirmation dialog
1318 call show_delete_confirmation(selected_path, confirm_result)
1319
1320 if (confirm_result == 1) then
1321 print *, "Delete confirmed - proceeding"
1322 call delete_to_trash(selected_path, selected_idx)
1323 else
1324 print *, "Delete cancelled by user"
1325 end if
1326 end subroutine on_delete_clicked
1327
1328 ! Callback when Toggle Dotfiles button is clicked
1329 subroutine on_toggle_dotfiles_clicked(button, user_data) bind(c)
1330 use treemap_renderer, only: toggle_hidden_files
1331 type(c_ptr), value :: button, user_data
1332 type(tab_state), pointer :: tab
1333
1334 ! Get active tab
1335 tab => get_active_tab()
1336 if (.not. associated(tab)) then
1337 print *, "ERROR: No active tab in on_toggle_dotfiles_clicked"
1338 return
1339 end if
1340
1341 call toggle_hidden_files()
1342 call sniffly_update_status("Toggled hidden files visibility - rescanning...")
1343
1344 ! Trigger rescan to apply the filter
1345 if (len_trim(tab%scan_path) > 0) then
1346 call trigger_rescan(tab%scan_path)
1347 end if
1348 end subroutine on_toggle_dotfiles_clicked
1349
1350 ! Callback when Toggle File Extensions button is clicked
1351 subroutine on_toggle_extensions_clicked(button, user_data) bind(c)
1352 use treemap_renderer, only: toggle_file_extensions
1353 type(c_ptr), value :: button, user_data
1354
1355 call toggle_file_extensions()
1356 call sniffly_update_status("Toggled file extensions visibility")
1357 end subroutine on_toggle_extensions_clicked
1358
1359 ! Callback when Toggle Render Mode button is clicked
1360 subroutine on_toggle_render_mode_clicked(button, user_data) bind(c)
1361 use treemap_renderer, only: toggle_render_mode
1362 type(c_ptr), value :: button, user_data
1363
1364 call toggle_render_mode()
1365 call sniffly_update_status("Toggled render mode (flat vs cushioned)")
1366 end subroutine on_toggle_render_mode_clicked
1367
1368 ! Commented out unused helper function - was used by removed search/filter feature
1369 ! Uncomment if needed in future
1370
1371 ! ! Helper to convert C string to Fortran string
1372 ! subroutine c_f_string(c_str_ptr, f_str)
1373 ! type(c_ptr), intent(in) :: c_str_ptr
1374 ! character(len=*), intent(out) :: f_str
1375 ! character(len=1, kind=c_char), pointer :: c_chars(:)
1376 ! integer :: i, str_len
1377 !
1378 ! f_str = ""
1379 ! if (.not. c_associated(c_str_ptr)) return
1380 !
1381 ! ! Get string length
1382 ! str_len = 0
1383 ! do i = 1, len(f_str)
1384 ! call c_f_pointer(c_str_ptr, c_chars, [i])
1385 ! if (c_chars(i) == c_null_char) exit
1386 ! str_len = i
1387 ! end do
1388 !
1389 ! ! Copy characters
1390 ! if (str_len > 0) then
1391 ! call c_f_pointer(c_str_ptr, c_chars, [str_len])
1392 ! do i = 1, str_len
1393 ! f_str(i:i) = c_chars(i)
1394 ! end do
1395 ! end if
1396 ! end subroutine c_f_string
1397
1398 ! Detect if we're running on macOS
1399 function is_macos() result(is_mac)
1400 logical :: is_mac
1401 logical :: file_exists
1402
1403 ! Check for macOS-specific directory
1404 inquire(file='/Applications', exist=file_exists)
1405 is_mac = file_exists
1406 end function is_macos
1407
1408 ! Show native OS directory picker using system commands
1409 ! This is a workaround until GTK4 file dialog bindings are available
1410 subroutine show_native_directory_picker(path, status)
1411 character(len=*), intent(out) :: path
1412 integer, intent(out) :: status
1413 character(len=2048) :: command, temp_file
1414 integer :: unit, ios
1415 logical :: file_exists
1416
1417 path = ""
1418 status = -1
1419
1420 ! Create temp file for output
1421 temp_file = "/tmp/sniffly_picker.txt"
1422
1423 ! Platform-specific command - detect at runtime
1424 if (is_macos()) then
1425 ! macOS: Use osascript to show native folder picker
1426 command = 'osascript -e ''POSIX path of (choose folder with prompt "Select directory to scan:")'' > ' &
1427 // trim(temp_file) // ' 2>&1'
1428 else
1429 ! Linux: Try zenity, fallback to kdialog
1430 command = 'zenity --file-selection --directory > ' // trim(temp_file) // &
1431 ' 2>&1 || kdialog --getexistingdirectory . > ' // trim(temp_file) // ' 2>&1'
1432 end if
1433
1434 print *, "Executing: ", trim(command)
1435
1436 ! Execute command
1437 call execute_command_line(trim(command), exitstat=status)
1438
1439 ! Read result from temp file
1440 inquire(file=trim(temp_file), exist=file_exists)
1441 if (file_exists) then
1442 open(newunit=unit, file=trim(temp_file), status='old', action='read', iostat=ios)
1443 if (ios == 0) then
1444 read(unit, '(A)', iostat=ios) path
1445 close(unit)
1446
1447 ! Remove temp file
1448 call execute_command_line('rm -f ' // trim(temp_file))
1449
1450 ! Trim whitespace and check if valid
1451 path = trim(adjustl(path))
1452 if (len_trim(path) > 0) then
1453 status = 0
1454 print *, "Got path: ", trim(path)
1455 else
1456 status = 1
1457 end if
1458 else
1459 close(unit)
1460 status = 1
1461 end if
1462 else
1463 status = 1
1464 end if
1465 end subroutine show_native_directory_picker
1466
1467 ! Update the path display entry with a new path
1468 subroutine update_path_entry(path)
1469 character(len=*), intent(in) :: path
1470 type(c_ptr) :: buffer
1471
1472 ! Guard against accessing widgets during shutdown
1473 if (app_is_shutting_down) return
1474 if (.not. c_associated(path_entry_ptr)) return
1475
1476 ! Get the entry buffer and set the text
1477 buffer = gtk_entry_get_buffer(path_entry_ptr)
1478 call gtk_entry_buffer_set_text(buffer, trim(path)//c_null_char, &
1479 int(len_trim(path), c_int))
1480 end subroutine update_path_entry
1481
1482 ! Open a file or folder in the OS file manager (Finder on macOS, file browser on Linux)
1483 subroutine open_in_file_manager(path)
1484 character(len=*), intent(in) :: path
1485 character(len=2048) :: command
1486 integer :: status
1487
1488 if (is_macos()) then
1489 ! macOS: Use 'open -R' to reveal in Finder
1490 command = 'open -R "' // trim(path) // '"'
1491 else
1492 ! Linux: Use xdg-open to open in default file manager
1493 command = 'xdg-open "' // trim(path) // '"'
1494 end if
1495
1496 print *, "Executing: ", trim(command)
1497 call execute_command_line(trim(command), exitstat=status)
1498
1499 if (status /= 0) then
1500 print *, "Warning: Failed to open file manager (exit status: ", status, ")"
1501 else
1502 print *, "Successfully opened in file manager"
1503 end if
1504 end subroutine open_in_file_manager
1505
1506 ! Show native delete confirmation dialog using system commands
1507 subroutine show_delete_confirmation(path, result)
1508 character(len=*), intent(in) :: path
1509 integer, intent(out) :: result
1510 character(len=2048) :: command
1511 integer :: status
1512
1513 result = 0 ! Default to cancel
1514
1515 if (is_macos()) then
1516 ! macOS: Use osascript to show native dialog
1517 command = 'osascript -e ''display dialog "Are you sure you want to delete:\n' &
1518 // trim(path) // '\n\nThis will move the item to Trash." ' &
1519 // 'buttons {"Cancel", "Delete"} default button "Cancel" ' &
1520 // 'with icon caution'' > /dev/null 2>&1'
1521 else
1522 ! Linux: Use zenity for confirmation dialog
1523 command = 'zenity --question --title="Confirm Delete" --text="Are you sure you want to delete:\n' &
1524 // trim(path) // '\n\nThis will move the item to Trash." 2>&1'
1525 end if
1526
1527 print *, "Showing confirmation dialog for: ", trim(path)
1528 call execute_command_line(trim(command), exitstat=status)
1529
1530 ! Both macOS and Linux: exit status 0 means confirmed
1531 if (status == 0) then
1532 result = 1 ! Confirmed
1533 end if
1534
1535 print *, "Confirmation result: ", result
1536 end subroutine show_delete_confirmation
1537
1538 ! Delete file or folder to system trash (macOS/Linux)
1539 subroutine delete_to_trash(path, selected_idx)
1540 use gtk, only: gtk_widget_queue_draw
1541 use treemap_renderer, only: remove_selected_node_from_view, invalidate_layout
1542 use treemap_widget, only: clear_selection
1543 character(len=*), intent(in) :: path
1544 integer, intent(in) :: selected_idx
1545 character(len=2048) :: command
1546 integer :: status
1547
1548 if (is_macos()) then
1549 ! macOS: Use osascript to move to Trash via Finder
1550 command = 'osascript -e ''tell application "Finder" to delete POSIX file "' &
1551 // trim(path) // '"'' > /dev/null 2>&1'
1552 else
1553 ! Linux: Use gio trash (GNOME), fallback to trash-cli
1554 command = 'gio trash "' // trim(path) // '" 2>&1 || trash "' // trim(path) // '" 2>&1'
1555 end if
1556
1557 print *, "Deleting to trash: ", trim(path)
1558 call execute_command_line(trim(command), exitstat=status)
1559
1560 if (status == 0) then
1561 print *, "Successfully moved to trash: ", trim(path)
1562
1563 ! Clear the selection first (before modifying tree)
1564 call clear_selection()
1565
1566 ! Remove the node from the current view by marking it as deleted
1567 call remove_selected_node_from_view(selected_idx)
1568
1569 ! Force layout recalculation
1570 call invalidate_layout()
1571
1572 ! Trigger redraw to show the updated view
1573 if (c_associated(main_window_ptr)) then
1574 call gtk_widget_queue_draw(main_window_ptr)
1575 end if
1576
1577 print *, "View updated - deleted node removed"
1578 else
1579 print *, "ERROR: Failed to move to trash (exit status: ", status, ")"
1580 print *, "You may need to delete manually or check permissions"
1581 end if
1582 end subroutine delete_to_trash
1583
1584 ! Update status bar with scan information
1585 subroutine sniffly_update_status(message)
1586 character(len=*), intent(in) :: message
1587 ! Guard against accessing widgets during shutdown
1588 if (app_is_shutting_down) return
1589 if (c_associated(status_label_ptr)) then
1590 call gtk_label_set_text(status_label_ptr, trim(message)//c_null_char)
1591 end if
1592 end subroutine sniffly_update_status
1593
1594 ! Timeout callback to clear status message (called after 5 seconds)
1595 function clear_status_message(user_data) bind(c) result(continue)
1596 type(c_ptr), value :: user_data
1597 integer(c_int) :: continue
1598
1599 ! Clear the status message
1600 if (.not. app_is_shutting_down .and. c_associated(status_label_ptr)) then
1601 call gtk_label_set_text(status_label_ptr, ""//c_null_char)
1602 end if
1603
1604 ! Return 0 to indicate the timeout should not repeat (one-shot)
1605 continue = 0_c_int
1606 end function clear_status_message
1607
1608 ! Show error message in status bar, auto-dismiss after 5 seconds
1609 subroutine sniffly_show_error(message)
1610 character(len=*), intent(in) :: message
1611 integer(c_int) :: timeout_id
1612
1613 ! Display the error message
1614 call sniffly_update_status(message)
1615
1616 ! Schedule message to be cleared after 5 seconds
1617 ! Note: g_timeout_add_seconds_once is a one-shot timer that calls the callback once
1618 timeout_id = g_timeout_add_seconds_once(5_c_int, c_funloc(clear_status_message), c_null_ptr)
1619 end subroutine sniffly_show_error
1620
1621 ! Update status bar with file count and size statistics
1622 subroutine sniffly_update_status_bar_stats()
1623 use types, only: file_node
1624 use treemap_renderer, only: get_current_view_node, get_node_count
1625 use iso_fortran_env, only: int64
1626 type(file_node), pointer :: current_view
1627 integer :: item_count, total_files
1628 integer(int64) :: total_size
1629 character(len=256) :: status_text
1630 character(len=64) :: size_str
1631 real :: size_kb, size_mb, size_gb
1632
1633 ! Guard against accessing widgets during shutdown
1634 if (app_is_shutting_down) return
1635 if (.not. c_associated(status_label_ptr)) return
1636
1637 ! Get current view node
1638 current_view => get_current_view_node()
1639 if (.not. associated(current_view)) then
1640 call gtk_label_set_text(status_label_ptr, "No data"//c_null_char)
1641 return
1642 end if
1643
1644 ! Get statistics from current view
1645 item_count = get_node_count()
1646 total_size = current_view%size
1647
1648 ! Count total files recursively
1649 total_files = count_files_recursive(current_view)
1650
1651 ! Format size nicely
1652 if (total_size < 1024_int64) then
1653 write(size_str, '(I0,A)') total_size, ' B'
1654 else if (total_size < 1024_int64**2) then
1655 size_kb = real(total_size) / 1024.0
1656 write(size_str, '(F0.2,A)') size_kb, ' KB'
1657 else if (total_size < 1024_int64**3) then
1658 size_mb = real(total_size) / (1024.0**2)
1659 write(size_str, '(F0.2,A)') size_mb, ' MB'
1660 else
1661 size_gb = real(total_size) / (1024.0**3)
1662 write(size_str, '(F0.2,A)') size_gb, ' GB'
1663 end if
1664
1665 ! Build status text
1666 write(status_text, '(I0,A,I0,A,A)') item_count, ' items (', total_files, ' files) - ', trim(size_str)
1667
1668 ! Update status label
1669 call gtk_label_set_text(status_label_ptr, trim(status_text)//c_null_char)
1670 end subroutine sniffly_update_status_bar_stats
1671
1672 ! Helper function to recursively count all files in a tree
1673 recursive function count_files_recursive(node) result(count)
1674 use types, only: file_node
1675 type(file_node), intent(in) :: node
1676 integer :: count, i
1677
1678 count = 0
1679
1680 if (node%is_directory) then
1681 ! For directories, count all children recursively
1682 if (allocated(node%children)) then
1683 do i = 1, node%num_children
1684 count = count + count_files_recursive(node%children(i))
1685 end do
1686 end if
1687 else
1688 ! For files, count this file
1689 count = 1
1690 end if
1691 end function count_files_recursive
1692
1693 ! Get forward path (if we navigated backwards and there's a forward history)
1694 function get_forward_path() result(fwd_path)
1695 type(tab_state), pointer :: tab
1696 character(len=512) :: fwd_path
1697
1698 fwd_path = ""
1699
1700 ! Get active tab
1701 tab => get_active_tab()
1702 if (.not. associated(tab)) then
1703 return
1704 end if
1705
1706 if (tab%nav_history_pos > 0 .and. tab%nav_history_pos < tab%nav_history_count) then
1707 fwd_path = trim(tab%nav_history(tab%nav_history_pos + 1))
1708 print *, "Forward path available: ", trim(fwd_path)
1709 end if
1710 end function get_forward_path
1711
1712 ! Callback wrapper for navigation events (no arguments)
1713 subroutine breadcrumb_callback()
1714 use treemap_renderer, only: get_current_view_node
1715 use types, only: file_node
1716 type(file_node), pointer :: current_view
1717 type(tab_state), pointer :: tab
1718 character(len=512) :: fwd_path, prev_breadcrumb_path
1719 integer :: i, matched_pos, current_len
1720 logical :: was_navigating_history, is_breadcrumb_lookahead
1721
1722 print *, "=== BREADCRUMB_CALLBACK ==="
1723
1724 ! Get active tab
1725 tab => get_active_tab()
1726 if (.not. associated(tab)) then
1727 print *, "ERROR: No active tab in breadcrumb_callback"
1728 return
1729 end if
1730
1731 print *, " navigating_history flag at entry: ", tab%navigating_history
1732
1733 ! Initialize variables
1734 fwd_path = ""
1735 is_breadcrumb_lookahead = .false.
1736
1737 ! Save flag state
1738 was_navigating_history = tab%navigating_history
1739
1740 ! Sync tab scan_path with the current view node's path
1741 current_view => get_current_view_node()
1742 if (associated(current_view) .and. allocated(current_view%path)) then
1743 tab%scan_path = trim(current_view%path)
1744
1745 ! Update tab label to reflect new path
1746 tab%label = get_path_basename(trim(tab%scan_path))
1747
1748 print *, " Synced tab scan_path to: ", trim(tab%scan_path)
1749 print *, " Updated tab label to: ", trim(tab%label)
1750 print *, " Current nav_history_pos: ", tab%nav_history_pos, " nav_history_count: ", tab%nav_history_count
1751
1752 ! Check for breadcrumb-based lookahead first (when clicking up in breadcrumb)
1753 prev_breadcrumb_path = get_previous_breadcrumb_path()
1754 if (len_trim(prev_breadcrumb_path) > 0) then
1755 print *, " Previous breadcrumb path: ", trim(prev_breadcrumb_path)
1756 ! Check if current path is a prefix of previous path (navigating up)
1757 current_len = len_trim(tab%scan_path)
1758 if (len_trim(prev_breadcrumb_path) > current_len) then
1759 if (prev_breadcrumb_path(1:current_len) == tab%scan_path(1:current_len)) then
1760 ! We navigated to a parent directory via breadcrumb
1761 fwd_path = trim(prev_breadcrumb_path)
1762 is_breadcrumb_lookahead = .true.
1763 print *, " Breadcrumb-based lookahead detected: ", trim(fwd_path)
1764 ! Clear the saved path
1765 call clear_previous_breadcrumb_path()
1766 end if
1767 else
1768 ! Not a parent navigation, clear the saved path
1769 call clear_previous_breadcrumb_path()
1770 end if
1771 end if
1772
1773 ! Check if this path matches any entry in history (for breadcrumb clicks)
1774 ! This syncs nav_history_pos with breadcrumb navigation
1775 ! Only do this if we're NOT already in a history navigation (back/forward button)
1776 if (.not. was_navigating_history .and. tab%nav_history_count > 0) then
1777 matched_pos = 0
1778 do i = 1, tab%nav_history_count
1779 if (trim(tab%nav_history(i)) == trim(tab%scan_path)) then
1780 matched_pos = i
1781 print *, " Found path in history at position ", i
1782 exit
1783 end if
1784 end do
1785
1786 if (matched_pos > 0 .and. matched_pos /= tab%nav_history_pos) then
1787 print *, " Breadcrumb navigation: syncing history pos from ", tab%nav_history_pos, " to ", matched_pos
1788 tab%nav_history_pos = matched_pos
1789 tab%navigating_history = .true. ! Mark as history navigation to skip add_to_history
1790 else if (matched_pos > 0) then
1791 print *, " Path matches current history position - no sync needed"
1792 else
1793 print *, " Path not found in history - will add as new entry"
1794 end if
1795 end if
1796
1797 ! Get history-based forward path (if available and no breadcrumb lookahead)
1798 if (len_trim(fwd_path) == 0) then
1799 fwd_path = get_forward_path()
1800 if (len_trim(fwd_path) > 0) then
1801 print *, " History-based forward path available: ", trim(fwd_path)
1802 else
1803 print *, " No forward path available"
1804 end if
1805 end if
1806
1807 ! Update breadcrumb widget with new path and forward lookahead
1808 if (len_trim(fwd_path) > 0) then
1809 call update_breadcrumb_cache(trim(current_view%path), trim(fwd_path))
1810 else
1811 call update_breadcrumb_cache(trim(current_view%path))
1812 end if
1813 end if
1814
1815 call sniffly_update_status_bar_stats()
1816
1817 ! Add to history if this is a new navigation
1818 ! Skip only if: (1) using back/forward buttons OR (2) history-based forward path exists
1819 ! BUT: breadcrumb-based lookahead IS a new navigation and should be added!
1820 if (.not. tab%navigating_history .and. (len_trim(fwd_path) == 0 .or. is_breadcrumb_lookahead)) then
1821 ! New navigation (including breadcrumb navigation with lookahead)
1822 if (len_trim(tab%scan_path) > 0) then
1823 print *, " Calling add_to_history (new navigation, breadcrumb_lookahead=", is_breadcrumb_lookahead, ")"
1824 call add_to_history(tab%scan_path)
1825 end if
1826 else
1827 if (tab%navigating_history) then
1828 print *, " Skipping add_to_history (history navigation mode)"
1829 else
1830 print *, " Skipping add_to_history (history-based forward context exists)"
1831 end if
1832 end if
1833
1834 ! ALWAYS reset the flag at the end (ensure it doesn't stick)
1835 print *, " Resetting navigating_history flag to false"
1836 tab%navigating_history = .false.
1837
1838 ! Update button states now that history may have changed
1839 call update_history_buttons()
1840
1841 ! Refresh tab bar to show updated label
1842 call refresh_tab_bar()
1843 call update_tab_visual_states()
1844 end subroutine breadcrumb_callback
1845
1846 ! Show progress bar (now just resets to prepare for updates)
1847 subroutine sniffly_show_progress()
1848 ! Guard against accessing widgets during shutdown
1849 if (app_is_shutting_down) return
1850 if (c_associated(progress_bar_ptr)) then
1851 call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double)
1852 call gtk_progress_bar_set_text(progress_bar_ptr, "0%"//c_null_char)
1853 end if
1854 ! Update cancel button when progress bar is shown (scan starting)
1855 call update_cancel_scan_button_state()
1856 end subroutine sniffly_show_progress
1857
1858 ! Hide progress bar (now just resets to 0%)
1859 subroutine sniffly_hide_progress()
1860 ! Guard against accessing widgets during shutdown
1861 if (app_is_shutting_down) return
1862 if (c_associated(progress_bar_ptr)) then
1863 call gtk_progress_bar_set_fraction(progress_bar_ptr, 0.0_c_double)
1864 call gtk_progress_bar_set_text(progress_bar_ptr, ""//c_null_char)
1865 end if
1866 ! Update cancel button when progress bar is hidden (scan likely stopped)
1867 call update_cancel_scan_button_state()
1868 end subroutine sniffly_hide_progress
1869
1870 ! Update progress bar and status text
1871 ! fraction: 0.0 to 1.0
1872 ! message: status text to show
1873 subroutine sniffly_update_progress(fraction, message)
1874 real(c_double), intent(in) :: fraction
1875 character(len=*), intent(in) :: message
1876 character(len=32) :: percent_str
1877 integer :: percent_int
1878
1879 ! Guard against accessing widgets during shutdown
1880 if (app_is_shutting_down) return
1881
1882 if (c_associated(progress_bar_ptr)) then
1883 ! Update progress bar fraction
1884 call gtk_progress_bar_set_fraction(progress_bar_ptr, fraction)
1885
1886 ! Set percentage text
1887 percent_int = int(fraction * 100.0_c_double)
1888 write(percent_str, '(I0,A)') percent_int, '%'
1889 call gtk_progress_bar_set_text(progress_bar_ptr, trim(percent_str)//c_null_char)
1890 end if
1891
1892 ! Update status label
1893 if (c_associated(status_label_ptr)) then
1894 call gtk_label_set_text(status_label_ptr, trim(message)//c_null_char)
1895 end if
1896
1897 ! Note: We update widgets but GTK will handle rendering in its own event loop
1898 ! Frequent calls to this function will keep the UI updated
1899 end subroutine sniffly_update_progress
1900
1901 ! Callback wrapper for quit events (no arguments)
1902 subroutine quit_callback_wrapper()
1903 use progressive_scanner, only: stop_progressive_scan
1904 ! Stop any active scans before quitting
1905 call stop_progressive_scan()
1906 call sniffly_app_quit()
1907 end subroutine quit_callback_wrapper
1908
1909 ! Callback wrapper for delete events (no arguments)
1910 subroutine delete_callback_wrapper()
1911 use treemap_widget, only: get_selected_index
1912 character(len=:), allocatable :: selected_path
1913 integer :: confirm_result, selected_idx
1914
1915 print *, "Delete callback triggered from keyboard"
1916
1917 ! Check if there's a selection
1918 if (.not. has_selection()) then
1919 print *, "No selection - cannot delete"
1920 return
1921 end if
1922
1923 ! Get the selected node path and index
1924 selected_path = get_selected_node_path()
1925 selected_idx = get_selected_index()
1926
1927 if (len_trim(selected_path) == 0) then
1928 print *, "Invalid selection path"
1929 return
1930 end if
1931
1932 print *, "Preparing to delete: ", trim(selected_path)
1933
1934 ! Show confirmation dialog
1935 call show_delete_confirmation(selected_path, confirm_result)
1936
1937 if (confirm_result == 1) then
1938 print *, "Delete confirmed - proceeding"
1939 call delete_to_trash(selected_path, selected_idx)
1940 else
1941 print *, "Delete cancelled by user"
1942 end if
1943 end subroutine delete_callback_wrapper
1944
1945 ! Callback wrapper for force refresh events (clear cache and rescan)
1946 subroutine refresh_callback_wrapper()
1947 use treemap_renderer, only: clear_cache, invalidate_layout
1948 type(tab_state), pointer :: tab
1949
1950 print *, "Force refresh triggered from keyboard shortcut"
1951
1952 ! Get active tab
1953 tab => get_active_tab()
1954 if (.not. associated(tab)) then
1955 print *, "ERROR: No active tab in refresh_callback_wrapper"
1956 return
1957 end if
1958
1959 ! Clear the directory cache
1960 call clear_cache()
1961 call invalidate_layout()
1962
1963 ! Update status
1964 call sniffly_update_status("Clearing cache and rescanning...")
1965
1966 ! Trigger rescan if we have a path
1967 if (len_trim(tab%scan_path) > 0) then
1968 call trigger_rescan(tab%scan_path)
1969 else
1970 call sniffly_show_error("No directory to scan")
1971 end if
1972 end subroutine refresh_callback_wrapper
1973
1974 ! Callback wrapper for scan completion
1975 subroutine scan_complete_callback_wrapper()
1976 use treemap_widget, only: mark_initial_scan_complete
1977 use treemap_renderer, only: get_root_node, get_current_view_node
1978 use types, only: file_node
1979 type(tab_state), pointer :: tab
1980 type(file_node), pointer :: root, current_view
1981
1982 print *, "=== SCAN COMPLETE CALLBACK FIRED ==="
1983
1984 ! Call the original completion callback
1985 call mark_initial_scan_complete()
1986
1987 ! Sync active tab's state with renderer (tab now has data)
1988 tab => get_active_tab()
1989 if (associated(tab)) then
1990 root => get_root_node()
1991 current_view => get_current_view_node()
1992 if (associated(root)) then
1993 tab%has_data = .true.
1994 tab%root_node => root
1995 tab%current_view_node => current_view
1996 print *, "=== SYNCED TAB STATE AFTER SCAN COMPLETE ==="
1997 end if
1998 end if
1999
2000 ! Update cancel button (scan is done, should be disabled and grey)
2001 print *, "=== UPDATING CANCEL BUTTON FROM COMPLETION CALLBACK ==="
2002 call update_cancel_scan_button_state()
2003
2004 ! Re-enable back/forward buttons if there's history
2005 print *, "=== RE-ENABLING NAVIGATION BUTTONS ==="
2006 call update_history_buttons()
2007
2008 ! Update UI for active tab (removes blue pulsing if tab now has data)
2009 print *, "=== UPDATING UI FOR ACTIVE TAB (SCAN COMPLETE) ==="
2010 call update_ui_for_active_tab()
2011
2012 ! Complete any pending synthetic navigation
2013 if (pending_synthetic_nav) then
2014 print *, "=== PENDING SYNTHETIC NAV - COMPLETING ==="
2015 call complete_synthetic_navigation()
2016 end if
2017 end subroutine scan_complete_callback_wrapper
2018
2019 ! Trigger a rescan of the given directory (for UI buttons)
2020 subroutine trigger_rescan(path)
2021 use gtk, only: gtk_widget_queue_draw
2022 use g, only: g_main_context_default, g_main_context_iteration
2023 use treemap_renderer, only: invalidate_layout
2024 character(len=*), intent(in) :: path
2025 character(len=:), allocatable :: normalized_path
2026 type(c_ptr) :: context
2027 integer :: i
2028 integer :: path_len
2029
2030 print *, "=== TRIGGER_RESCAN ENTERED ==="
2031 print *, "Triggering rescan of: '", trim(path), "'"
2032 print *, "Path length: ", len_trim(path)
2033
2034 ! Remove trailing slash if present (C code doesn't like it)
2035 path_len = len_trim(path)
2036 if (path_len > 1 .and. path(path_len:path_len) == '/') then
2037 normalized_path = trim(path(1:path_len-1))
2038 print *, "DEBUG: Removed trailing slash. New path: '", normalized_path, "'"
2039 else
2040 normalized_path = trim(path)
2041 end if
2042
2043 ! Process pending GTK events before starting scan
2044 context = g_main_context_default()
2045 do i = 1, 10
2046 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
2047 end do
2048 end do
2049
2050 print *, "=== ABOUT TO CALL scan_directory ==="
2051 ! Scan the directory (this will show progress via callbacks)
2052 call scan_directory(normalized_path)
2053 print *, "=== RETURNED FROM scan_directory ==="
2054
2055 ! Update button states (cancel button enabled, navigation disabled during scan)
2056 call update_cancel_scan_button_state()
2057 call update_history_buttons()
2058
2059 ! Process events after scan to update UI
2060 do i = 1, 10
2061 do while (g_main_context_iteration(context, 0_c_int) /= 0_c_int)
2062 end do
2063 end do
2064
2065 ! Invalidate layout to force recalculation
2066 call invalidate_layout()
2067
2068 ! Update breadcrumbs and status via callback (handles forward path lookahead)
2069 call breadcrumb_callback()
2070
2071 ! Trigger redraw to show the scanned data
2072 if (c_associated(main_window_ptr)) then
2073 call gtk_widget_queue_draw(main_window_ptr)
2074 end if
2075
2076 print *, "=== RESCAN COMPLETE ==="
2077 end subroutine trigger_rescan
2078
2079 ! Idle callback for async initial scan
2080 function perform_initial_scan(user_data) bind(c) result(continue)
2081 use gtk, only: gtk_widget_queue_draw
2082 use treemap_renderer, only: invalidate_layout
2083 type(c_ptr), value :: user_data
2084 integer(c_int) :: continue
2085
2086 print *, "Performing initial scan in idle callback..."
2087 call scan_directory(pending_scan_path)
2088
2089 ! Note: mark_initial_scan_complete() is now called by the progressive scanner
2090 ! when the scan actually completes (not immediately when it starts)
2091
2092 ! Invalidate layout to force recalculation
2093 call invalidate_layout()
2094
2095 ! Update breadcrumbs after scan (use pending_scan_path)
2096 if (len_trim(pending_scan_path) > 0) then
2097 call update_breadcrumb_cache(trim(pending_scan_path))
2098 end if
2099
2100 ! Update status bar with file statistics
2101 call sniffly_update_status_bar_stats()
2102
2103 ! Trigger redraw to show the scanned data
2104 if (c_associated(main_window_ptr)) then
2105 call gtk_widget_queue_draw(main_window_ptr)
2106 end if
2107
2108 ! Return 0 to indicate this callback should not be called again
2109 continue = 0_c_int
2110 end function perform_initial_scan
2111
2112 ! Get user's home directory
2113 function get_home_directory() result(home_path)
2114 character(len=512) :: home_path
2115 character(len=512) :: env_value
2116 integer :: status
2117
2118 ! Try to get HOME environment variable
2119 call get_environment_variable("HOME", env_value, status=status)
2120 if (status == 0) then
2121 home_path = trim(env_value)
2122 else
2123 ! Fallback to /Users/username on macOS or /home/username on Linux
2124 home_path = "/Users"
2125 end if
2126 end function get_home_directory
2127
2128 end module gtk_app
2129