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