program facsimile use iso_fortran_env, only: error_unit, input_unit, output_unit, int64 use version_module use terminal_io_module use input_handler_module, only: get_key_input use editor_state_module use text_buffer_module use renderer_module use command_handler_module, only: handle_key_command, init_command_handler, cleanup_command_handler, & save_initial_state_for_undo, search_pattern, match_case_sensitive, & g_lsp_modified_buffer, g_lsp_ui_changed, g_cursor_only_move use workspace_module use backup_module use save_prompt_module use command_palette_module, only: register_command use welcome_menu_module, only: show_welcome_menu use fortress_navigator_module, only: open_fortress_navigator use binary_prompt_module, only: binary_file_prompt use lsp_server_manager_module, only: notify_file_opened, notify_file_changed, & notify_file_closed, process_server_messages, & set_diagnostics_handler, set_lsp_workspace_root use lsp_protocol_module, only: lsp_message_t use app_state_module, only: is_first_run, mark_first_run_complete use lsp_server_installer_panel_module, only: show_lsp_server_installer_panel implicit none type(editor_state_t) :: editor type(buffer_t) :: buffer character(len=32) :: key_input character(len=512) :: filename, arg, workspace_dir, lsp_workspace logical :: running, should_quit, is_workspace_mode, workspace_success logical :: welcome_cancelled, is_browse, nav_cancelled, is_directory logical :: explicit_lsp_workspace character(len=:), allocatable :: selected_path integer :: status, argc, rows, cols, i ! Get command line arguments argc = command_argument_count() is_workspace_mode = .false. workspace_dir = "" filename = "" lsp_workspace = "" explicit_lsp_workspace = .false. ! First pass: look for -w/--workspace flag i = 1 do while (i <= argc) call get_command_argument(i, arg) if (trim(arg) == '-w' .or. trim(arg) == '--workspace') then if (i < argc) then call get_command_argument(i + 1, lsp_workspace) explicit_lsp_workspace = .true. i = i + 2 else write(error_unit, '(A)') 'Error: -w/--workspace requires a directory argument' stop 1 end if else i = i + 1 end if end do ! Second pass: handle other arguments i = 1 do while (i <= argc) call get_command_argument(i, arg) ! Skip -w and its argument (already processed) if (trim(arg) == '-w' .or. trim(arg) == '--workspace') then i = i + 2 cycle end if ! Handle version flags if (trim(arg) == '--version' .or. trim(arg) == '-v') then write(output_unit, '(A,A)') 'fac version ', VERSION stop end if ! Handle help flags if (trim(arg) == '--help' .or. trim(arg) == '-h') then call print_help() stop end if ! This must be the file/directory argument ! Check if argument is a directory (workspace mode) ! Use test -d which is POSIX compliant (works on Linux, macOS, BSD) call execute_command_line("test -d '" // trim(arg) // & "' && echo 'Directory' > /tmp/.fac_filetype || " // & "echo 'File' > /tmp/.fac_filetype", wait=.true.) call read_file_type(status) if (status == 0) then ! Directory - workspace mode is_workspace_mode = .true. call workspace_get_path(trim(arg), workspace_dir) else ! File - check if parent directory has a workspace workspace_dir = workspace_detect_from_file(trim(arg)) if (len_trim(workspace_dir) > 0) then is_workspace_mode = .true. end if filename = arg end if i = i + 1 end do if (argc == 0) then ! No arguments - launch Fortress welcome menu (Phase 5) call terminal_init() call show_welcome_menu(selected_path, welcome_cancelled) if (welcome_cancelled) then ! Check if user wants to browse filesystem is_browse = .false. if (allocated(selected_path) .and. selected_path == "BROWSE") then is_browse = .true. end if call terminal_cleanup() if (is_browse) then ! Launch fortress navigator call terminal_init() call open_fortress_navigator(selected_path, is_directory, nav_cancelled) call terminal_cleanup() if (nav_cancelled .or. .not. allocated(selected_path)) then ! User cancelled navigation too stop end if ! Handle the selection from navigator if (is_directory) then ! Directory - open as workspace is_workspace_mode = .true. call workspace_get_path(trim(selected_path), workspace_dir) else ! File - open in single-file mode (like `fac filename`) filename = selected_path ! Check if parent directory has a workspace workspace_dir = workspace_detect_from_file(trim(filename)) if (len_trim(workspace_dir) > 0) then is_workspace_mode = .true. end if end if else ! User just cancelled stop end if end if ! User selected a workspace from welcome menu (not browse) ! This handles favorites, recents, and CURRENT DIRECTORY if (allocated(selected_path) .and. .not. is_browse) then ! Check if user selected CURRENT DIRECTORY option if (selected_path == "CWD") then ! Get actual current working directory call get_workspace_path(selected_path) arg = selected_path else arg = selected_path end if ! Check if it's a directory call execute_command_line("test -d '" // trim(arg) // & "' && echo 'Directory' > /tmp/.fac_filetype || " // & "echo 'File' > /tmp/.fac_filetype", wait=.true.) call read_file_type(status) if (status == 0) then ! Directory - workspace mode is_workspace_mode = .true. call workspace_get_path(trim(arg), workspace_dir) else ! Invalid selection (favorites/recents should only have directories) write(error_unit, '(A)') 'Error: Selected path is not a directory' stop 1 end if end if end if ! Handle workspace mode if (is_workspace_mode) then ! Check if workspace exists, create if not if (.not. workspace_exists(workspace_dir)) then call workspace_init(workspace_dir, workspace_success) if (.not. workspace_success) then write(error_unit, '(A)') 'Error: Failed to create workspace' stop 1 end if else ! Load existing workspace call workspace_load(workspace_dir, workspace_success) if (.not. workspace_success) then write(error_unit, '(A)') 'Error: Failed to load workspace' stop 1 end if end if end if ! Initialize editor call init_editor(editor) running = .true. ! Set LSP workspace root if explicit -w flag was provided if (explicit_lsp_workspace) then call set_lsp_workspace_root(editor%lsp_manager, trim(lsp_workspace)) end if ! Set up diagnostics handler for LSP call set_diagnostics_handler(editor%lsp_manager, handle_diagnostics) ! Register all commands for command palette call register_all_commands() ! Initialize terminal early (needed for workspace restoration warnings) call terminal_init() call terminal_clear_screen() ! Initialize main buffer early (needed for workspace restoration) call init_buffer(buffer) ! Set workspace path if (is_workspace_mode) then ! Use detected/created workspace directory allocate(character(len=len_trim(workspace_dir)) :: editor%workspace_path) editor%workspace_path = trim(workspace_dir) ! Restore workspace state (tabs, cursor positions, etc.) call workspace_restore_state(editor, editor%workspace_path, workspace_success) ! Sync restored active tab's buffer to main buffer if (workspace_success .and. allocated(editor%tabs) .and. editor%active_tab_index > 0) then if (editor%active_tab_index <= size(editor%tabs)) then if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then ! Copy active pane's buffer to main buffer (replaces the empty init) call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(1)%buffer) end if end if end if else ! Single-file mode - use current directory call get_workspace_path(editor%workspace_path) end if ! Check for backups and offer restoration (after workspace load, after terminal init) if (is_workspace_mode .and. backup_detect(editor%workspace_path)) then call handle_backup_restoration(editor, buffer) end if ! Get terminal size call terminal_get_size(rows, cols) editor%screen_rows = rows editor%screen_cols = cols ! Initialize renderer (pass filename for syntax highlighting detection) if (len_trim(filename) > 0) then call init_renderer(rows, cols, trim(filename)) else call init_renderer(rows, cols) end if ! Initialize command handler (for yank stack) call init_command_handler() ! Initialize buffer and load file if specified if (len_trim(filename) > 0) then ! Create a tab for the initial file call create_tab(editor, trim(filename)) ! Load file into tab's buffer and first pane's buffer if (editor%active_tab_index > 0) then call buffer_load_file(editor%tabs(editor%active_tab_index)%buffer, trim(filename), status) ! Also load into first pane's buffer if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then call buffer_load_file(editor%tabs(editor%active_tab_index)%panes(1)%buffer, trim(filename), status) ! Copy first pane's buffer to main buffer call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(1)%buffer) else ! Copy tab's buffer to main buffer call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer) end if else call buffer_load_file(buffer, trim(filename), status) end if if (status == 0) then if (allocated(editor%filename)) deallocate(editor%filename) allocate(character(len=len_trim(filename)) :: editor%filename) editor%filename = trim(filename) ! Send LSP didOpen notification to ALL active servers if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then if (editor%tabs(editor%active_tab_index)%num_lsp_servers > 0) then block integer :: srv_i do srv_i = 1, editor%tabs(editor%active_tab_index)%num_lsp_servers call notify_file_opened(editor%lsp_manager, & editor%tabs(editor%active_tab_index)%lsp_server_indices(srv_i), & trim(filename), buffer_to_string(buffer)) end do end block end if end if else if (status == -2) then ! Binary file detected - prompt user if (binary_file_prompt(trim(filename))) then ! User wants to view in hex mode call buffer_load_file_as_hex(buffer, trim(filename), status) if (status == 0) then ! Deallocate if already allocated if (allocated(editor%filename)) deallocate(editor%filename) allocate(character(len=len_trim(filename) + 6) :: editor%filename) editor%filename = trim(filename) // ' [HEX]' ! Copy hex buffer to tab and pane buffers if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer) if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then call copy_buffer(editor%tabs(editor%active_tab_index)%panes(1)%buffer, buffer) end if end if ! TODO: Mark as read-only in future enhancement else ! Failed to load hex view call terminal_cleanup() write(error_unit, '(A)') 'Error: Failed to load binary file' stop 1 end if else ! User cancelled - cleanup and exit call terminal_cleanup() stop end if else ! If file doesn't exist, create empty buffer for new file call init_buffer(buffer) if (allocated(editor%filename)) deallocate(editor%filename) allocate(character(len=len_trim(filename)) :: editor%filename) editor%filename = trim(filename) end if else ! Only initialize empty buffer if we don't have restored tabs if (.not. (allocated(editor%tabs) .and. editor%active_tab_index > 0)) then call init_buffer(buffer) end if end if ! Save initial file state for undo (position 0) call save_initial_state_for_undo(buffer, editor) ! First-run experience: show LSP server installer panel if (is_first_run()) then call show_lsp_server_installer_panel(editor%lsp_installer_panel) call mark_first_run_complete() end if ! Initial render call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) ! Main event loop do while (running) ! Process any LSP messages call process_server_messages(editor%lsp_manager) ! Sync local buffer from tab after LSP processing (in case LSP modified it) block logical :: should_render should_render = .false. ! Check if LSP set the UI changed flag (e.g., code actions panel shown) if (g_lsp_ui_changed) then should_render = .true. g_lsp_ui_changed = .false. end if if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then ! Check if LSP set the modified flag if (g_lsp_modified_buffer) then should_render = .true. g_lsp_modified_buffer = .false. end if call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%buffer) ! Also sync to active pane buffer if panes exist (so pane doesn't overwrite LSP changes) if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then status = editor%tabs(editor%active_tab_index)%active_pane_index if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer) end if end if end if ! Render immediately if LSP modified the buffer (do this OUTSIDE the if block) if (should_render) then if (editor%fuss_mode_active) then call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive) else call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) end if end if end block ! Flush any pending document changes to LSP call flush_pending_document_changes(editor) ! Get input call get_key_input(key_input, status) if (status == 0) then ! Sync buffer before and after input when using panes ! Before: copy active pane's buffer -> global buffer ! After: copy global buffer -> active pane's buffer if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then ! Get active pane index status = editor%tabs(editor%active_tab_index)%active_pane_index if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then ! Copy active pane's buffer to main buffer call copy_buffer(buffer, editor%tabs(editor%active_tab_index)%panes(status)%buffer) end if end if end if ! Process input call handle_key_command(key_input, editor, buffer, should_quit) ! Sync back to active pane and other instances ! Skip buffer sync for cursor-only moves (buffer content unchanged) if (.not. g_cursor_only_move) then if (editor%active_tab_index > 0 .and. editor%active_tab_index <= size(editor%tabs)) then if (allocated(editor%tabs(editor%active_tab_index)%panes) .and. & size(editor%tabs(editor%active_tab_index)%panes) > 0) then ! Get active pane index status = editor%tabs(editor%active_tab_index)%active_pane_index if (status > 0 .and. status <= size(editor%tabs(editor%active_tab_index)%panes)) then ! Copy main buffer back to active pane's buffer call copy_buffer(editor%tabs(editor%active_tab_index)%panes(status)%buffer, buffer) ! Sync to all instances of this file if (allocated(editor%tabs(editor%active_tab_index)%panes(status)%filename)) then call sync_buffer_to_all_instances(editor, & editor%tabs(editor%active_tab_index)%panes(status)%filename, buffer) end if end if ! Also update tab buffer for backwards compatibility call copy_buffer(editor%tabs(editor%active_tab_index)%buffer, buffer) ! Sync modified flag from buffer to tab editor%tabs(editor%active_tab_index)%modified = buffer%modified end if end if end if if (should_quit) then running = .false. else ! Re-render screen after each command if (editor%fuss_mode_active) then call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive) else call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) end if end if end if end do ! IMPORTANT: Save workspace state FIRST, before any prompts or cleanup ! This ensures we capture the current state before tabs might be closed if (should_quit .and. allocated(editor%workspace_path)) then call workspace_save_state(editor, editor%workspace_path, workspace_success) end if ! Handle unsaved files - prompt for save/backup if (allocated(editor%tabs) .and. allocated(editor%workspace_path)) then ! Workspace mode - handle all modified tabs call handle_unsaved_files_on_quit(editor, buffer, should_quit) ! If user cancelled (should_quit = .false.), skip cleanup and restart loop else if (buffer%modified .and. allocated(editor%workspace_path) .and. allocated(editor%filename)) then ! Single-file mode - handle the current buffer if modified call handle_single_file_on_quit(buffer, editor, should_quit) end if ! Only proceed with cleanup if actually quitting if (should_quit) then ! Cleanup call cleanup_renderer() call cleanup_command_handler() call terminal_cleanup() call cleanup_editor(editor) call cleanup_buffer(buffer) else ! User cancelled quit - re-render and continue running = .true. call terminal_clear_screen() if (editor%fuss_mode_active) then call render_screen_with_tree(buffer, editor, allocated(search_pattern), match_case_sensitive) else call render_screen(buffer, editor, allocated(search_pattern), match_case_sensitive) end if end if contains ! Handler for LSP diagnostics notifications (with server attribution) subroutine handle_diagnostics(notification, server_index) use lsp_protocol_module, only: lsp_message_t use diagnostics_module, only: parse_diagnostics_from_params_with_server use terminal_io_module, only: terminal_write type(lsp_message_t), intent(in) :: notification integer, intent(in) :: server_index ! Parse and store diagnostics with server attribution (for multi-LSP) ! This keeps diagnostics from different servers separate call parse_diagnostics_from_params_with_server(editor%diagnostics, notification%params, server_index) end subroutine handle_diagnostics ! Flush pending document changes for all tabs subroutine flush_pending_document_changes(editor) use document_sync_module, only: flush_pending_changes type(editor_state_t), intent(inout) :: editor integer :: i ! Check all tabs for pending changes if (allocated(editor%tabs)) then do i = 1, size(editor%tabs) if (editor%tabs(i)%num_lsp_servers > 0) then call flush_pending_changes(editor%tabs(i)%document_sync, & editor%lsp_manager, .false.) end if end do end if end subroutine flush_pending_document_changes subroutine read_file_type(is_directory) integer, intent(out) :: is_directory character(len=20) :: file_type integer :: unit, ios is_directory = 1 ! Default to not a directory open(newunit=unit, file='/tmp/.fac_filetype', status='old', iostat=ios) if (ios == 0) then read(unit, '(A)', iostat=ios) file_type close(unit) call execute_command_line('rm -f /tmp/.fac_filetype', wait=.true.) if (ios == 0) then if (trim(file_type) == 'Directory') then is_directory = 0 end if end if end if end subroutine read_file_type subroutine get_workspace_path(path) character(len=:), allocatable, intent(out) :: path character(len=1024) :: buffer integer :: status ! Use execute_command_line to get current directory call execute_command_line('pwd > /tmp/fac_pwd.txt', wait=.true., exitstat=status) if (status == 0) then open(unit=99, file='/tmp/fac_pwd.txt', status='old', action='read', iostat=status) if (status == 0) then read(99, '(A)', iostat=status) buffer close(99, status='delete') if (status == 0) then path = trim(buffer) return end if end if end if ! Fallback if command fails path = '.' end subroutine get_workspace_path subroutine print_help() write(output_unit, '(A)') 'fac - Fortran text editor' write(output_unit, '(A,A)') 'Version: ', VERSION write(output_unit, '(A)') '' write(output_unit, '(A)') 'Usage:' write(output_unit, '(A)') ' fac [filename] Open a file for editing' write(output_unit, '(A)') ' fac [directory] Open directory in workspace mode' write(output_unit, '(A)') ' fac Start with empty buffer' write(output_unit, '(A)') ' fac --version, -v Show version information' write(output_unit, '(A)') ' fac --help, -h Show this help message' write(output_unit, '(A)') ' fac -w