| 1 | module file_tree_renderer_module |
| 2 | use iso_fortran_env, only: int32 |
| 3 | use file_tree_module |
| 4 | use terminal_io_module |
| 5 | implicit none |
| 6 | private |
| 7 | |
| 8 | public :: render_file_tree |
| 9 | |
| 10 | ! Simple expansion indicators (no box-drawing) |
| 11 | character(len=*), parameter :: EXPANDED_DIR = '-' |
| 12 | character(len=*), parameter :: COLLAPSED_DIR = '+' |
| 13 | character(len=1), parameter :: ESC = achar(27) |
| 14 | |
| 15 | contains |
| 16 | |
| 17 | subroutine render_file_tree(state, start_row, end_row, start_col, width, hints_expanded, git_prefix_active) |
| 18 | type(tree_state_t), intent(in) :: state |
| 19 | integer, intent(in) :: start_row, end_row, start_col, width |
| 20 | logical, intent(in) :: hints_expanded |
| 21 | logical, intent(in), optional :: git_prefix_active |
| 22 | logical :: git_mode |
| 23 | integer :: current_row, item_idx, row |
| 24 | character(len=512) :: status_line |
| 25 | character(len=:), allocatable :: padding |
| 26 | |
| 27 | ! Handle optional git_prefix_active parameter |
| 28 | if (present(git_prefix_active)) then |
| 29 | git_mode = git_prefix_active |
| 30 | else |
| 31 | git_mode = .false. |
| 32 | end if |
| 33 | |
| 34 | ! First, clear all rows in the tree pane |
| 35 | padding = repeat(' ', width) |
| 36 | do row = start_row, end_row |
| 37 | call terminal_move_cursor(row, start_col) |
| 38 | call terminal_write(padding) |
| 39 | end do |
| 40 | |
| 41 | current_row = start_row |
| 42 | |
| 43 | ! Display repo:branch info at top if available |
| 44 | if (len_trim(state%repo_name) > 0 .and. len_trim(state%branch_name) > 0) then |
| 45 | call terminal_move_cursor(current_row, start_col) |
| 46 | write(status_line, '(A,A,A,A,A,A,A)') & |
| 47 | ESC // '[1;36m', trim(state%repo_name), ESC // '[0m', & |
| 48 | ':', & |
| 49 | ESC // '[1;33m', trim(state%branch_name), ESC // '[0m' |
| 50 | call terminal_write(trim(status_line)) |
| 51 | current_row = current_row + 2 ! Skip a line |
| 52 | end if |
| 53 | |
| 54 | ! Display root |
| 55 | if (current_row <= end_row) then |
| 56 | call terminal_move_cursor(current_row, start_col) |
| 57 | call terminal_write('.') |
| 58 | current_row = current_row + 1 |
| 59 | end if |
| 60 | |
| 61 | ! Display tree (leave space for legend - 1 or 4 rows) |
| 62 | if (associated(state%root)) then |
| 63 | item_idx = 0 |
| 64 | if (hints_expanded) then |
| 65 | call render_tree_node(state%root, '', .true., & |
| 66 | state, item_idx, current_row, end_row - 4, start_col, width) |
| 67 | else |
| 68 | call render_tree_node(state%root, '', .true., & |
| 69 | state, item_idx, current_row, end_row - 1, start_col, width) |
| 70 | end if |
| 71 | end if |
| 72 | |
| 73 | ! Display legend at bottom (either minimal or expanded) |
| 74 | if (hints_expanded) then |
| 75 | ! Expanded legend (four rows) |
| 76 | if (end_row >= start_row + 4) then |
| 77 | ! First row: navigation |
| 78 | call terminal_move_cursor(end_row - 3, start_col) |
| 79 | call terminal_write(ESC // '[90m') ! Gray |
| 80 | call terminal_write('j/k:nav →:in ←:up o:open .:hide spc:toggle') |
| 81 | call terminal_write(ESC // '[0m') |
| 82 | |
| 83 | ! Second and third rows: git operations (only shown when git mode active) |
| 84 | if (git_mode) then |
| 85 | ! Git mode active - show git bindings in yellow |
| 86 | call terminal_move_cursor(end_row - 2, start_col) |
| 87 | call terminal_write(ESC // '[1;33m') ! Bright yellow |
| 88 | call terminal_write('a:stage u:unstage d:diff m:commit') |
| 89 | call terminal_write(ESC // '[0m') |
| 90 | |
| 91 | call terminal_move_cursor(end_row - 1, start_col) |
| 92 | call terminal_write(ESC // '[1;33m') ! Bright yellow |
| 93 | call terminal_write('p:push f:fetch l:pull t:tag esc:cancel') |
| 94 | call terminal_write(ESC // '[0m') |
| 95 | else |
| 96 | ! Normal mode - show shortcuts and fuzzy search hint |
| 97 | call terminal_move_cursor(end_row - 2, start_col) |
| 98 | call terminal_write(ESC // '[90m') ! Gray |
| 99 | call terminal_write('alt-v:vsplit alt-s:hsplit ctrl-g:git') |
| 100 | call terminal_write(ESC // '[0m') |
| 101 | |
| 102 | call terminal_move_cursor(end_row - 1, start_col) |
| 103 | call terminal_write(ESC // '[90m') ! Gray |
| 104 | call terminal_write('type to fuzzy search files') |
| 105 | call terminal_write(ESC // '[0m') |
| 106 | end if |
| 107 | |
| 108 | ! Fourth row: exit |
| 109 | call terminal_move_cursor(end_row, start_col) |
| 110 | call terminal_write(ESC // '[90m') ! Gray |
| 111 | call terminal_write('ctrl-/:collapse esc/F3:close') |
| 112 | call terminal_write(ESC // '[0m') |
| 113 | end if |
| 114 | else |
| 115 | ! Minimal legend (one row) |
| 116 | if (end_row >= start_row + 1) then |
| 117 | call terminal_move_cursor(end_row, start_col) |
| 118 | call terminal_write(ESC // '[90m') ! Gray |
| 119 | call terminal_write('.:hide ctrl-/:hints esc/F3:close') |
| 120 | call terminal_write(ESC // '[0m') |
| 121 | end if |
| 122 | end if |
| 123 | end subroutine render_file_tree |
| 124 | |
| 125 | recursive subroutine render_tree_node(node, prefix, is_root, & |
| 126 | state, item_idx, current_row, end_row, start_col, width) |
| 127 | type(tree_node_t), pointer, intent(in) :: node |
| 128 | character(len=*), intent(in) :: prefix |
| 129 | logical, intent(in) :: is_root |
| 130 | type(tree_state_t), intent(in) :: state |
| 131 | integer, intent(inout) :: item_idx, current_row |
| 132 | integer, intent(in) :: end_row, start_col, width |
| 133 | |
| 134 | character(len=:), allocatable :: line, new_prefix |
| 135 | type(tree_node_t), pointer :: child |
| 136 | logical :: is_selected, is_last_child |
| 137 | |
| 138 | ! Don't print root node |
| 139 | if (.not. is_root) then |
| 140 | ! Skip hidden files when hide_dotfiles is enabled (dotfiles or gitignored) |
| 141 | if (state%hide_dotfiles .and. node%is_file .and. (node%is_dotfile .or. node%is_gitignored)) then |
| 142 | return |
| 143 | end if |
| 144 | |
| 145 | ! Increment item_idx for both files and directories (all selectable items) |
| 146 | item_idx = item_idx + 1 |
| 147 | is_selected = (item_idx == state%selected_index) |
| 148 | |
| 149 | ! Only render if within viewport and current_row fits |
| 150 | if (item_idx >= state%viewport_offset .and. current_row <= end_row) then |
| 151 | ! Build line with simple +/- indicators and indentation |
| 152 | if (.not. node%is_file) then |
| 153 | ! Directory - add expand/collapse indicator and / suffix |
| 154 | if (associated(node%first_child)) then |
| 155 | ! Directory with children |
| 156 | if (node%expanded) then |
| 157 | line = prefix // EXPANDED_DIR // ' ' // trim(node%name) // '/' |
| 158 | else |
| 159 | line = prefix // COLLAPSED_DIR // ' ' // trim(node%name) // '/' |
| 160 | end if |
| 161 | else |
| 162 | ! Empty directory - no expand/collapse indicator |
| 163 | line = prefix // ' ' // trim(node%name) // '/' |
| 164 | end if |
| 165 | else |
| 166 | ! File - just indentation and name |
| 167 | line = prefix // ' ' // trim(node%name) |
| 168 | end if |
| 169 | |
| 170 | ! Add status indicators for files only |
| 171 | if (node%is_file) then |
| 172 | if (node%is_staged) then |
| 173 | line = line // ' ' // ESC // '[32m↑' // ESC // '[0m' ! Green up arrow |
| 174 | end if |
| 175 | if (node%is_unstaged) then |
| 176 | line = line // ' ' // ESC // '[31m✗' // ESC // '[0m' ! Red X |
| 177 | end if |
| 178 | if (node%is_untracked) then |
| 179 | line = line // ' ' // ESC // '[90m✗' // ESC // '[0m' ! Gray X |
| 180 | end if |
| 181 | if (node%has_incoming) then |
| 182 | line = line // ' ' // ESC // '[34m↓' // ESC // '[0m' ! Blue down arrow |
| 183 | end if |
| 184 | end if |
| 185 | |
| 186 | ! Render with selection highlight for files only |
| 187 | call terminal_move_cursor(current_row, start_col) |
| 188 | if (is_selected) then |
| 189 | ! Selection highlight (reverse video) |
| 190 | call terminal_write(ESC // '[7m' // line // ESC // '[0m') |
| 191 | else |
| 192 | ! Grey out directories that only contain hidden files |
| 193 | if (.not. node%is_file .and. node%all_children_hidden) then |
| 194 | call terminal_write(ESC // '[90m' // line // ESC // '[0m') ! Grey |
| 195 | else |
| 196 | call terminal_write(line) |
| 197 | end if |
| 198 | end if |
| 199 | |
| 200 | current_row = current_row + 1 |
| 201 | end if |
| 202 | end if |
| 203 | |
| 204 | ! Render children (only if directory is expanded or root) |
| 205 | ! Files don't have children, so skip if is_file. For directories, check if expanded or root. |
| 206 | if ((.not. node%is_file .and. node%expanded) .or. is_root) then |
| 207 | child => node%first_child |
| 208 | do while (associated(child)) |
| 209 | ! Determine if this is the last sibling |
| 210 | is_last_child = .not. associated(child%next_sibling) |
| 211 | |
| 212 | ! Build prefix for child based on current node's position |
| 213 | if (is_root) then |
| 214 | ! Root's children start with no prefix |
| 215 | new_prefix = '' |
| 216 | else |
| 217 | ! Non-root children inherit prefix and add 2-space indentation |
| 218 | new_prefix = prefix // ' ' |
| 219 | end if |
| 220 | |
| 221 | call render_tree_node(child, new_prefix, .false., & |
| 222 | state, item_idx, current_row, end_row, start_col, width) |
| 223 | child => child%next_sibling |
| 224 | end do |
| 225 | end if |
| 226 | end subroutine render_tree_node |
| 227 | |
| 228 | end module file_tree_renderer_module |
| 229 |