@@ -0,0 +1,377 @@ |
| | 1 | +module lsp_server_installer_panel_module |
| | 2 | + use terminal_io_module |
| | 3 | + use server_detection_module, only: detected_server_t, detect_all_servers, check_server_installed |
| | 4 | + use server_installer_module, only: run_install_command, install_result_t |
| | 5 | + implicit none |
| | 6 | + private |
| | 7 | + |
| | 8 | + public :: lsp_server_installer_panel_t |
| | 9 | + public :: init_lsp_server_installer_panel, cleanup_lsp_server_installer_panel |
| | 10 | + public :: show_lsp_server_installer_panel, hide_lsp_server_installer_panel |
| | 11 | + public :: is_lsp_server_installer_panel_visible |
| | 12 | + public :: lsp_server_installer_panel_handle_key |
| | 13 | + public :: render_lsp_server_installer_panel |
| | 14 | + public :: refresh_server_status |
| | 15 | + |
| | 16 | + integer, parameter :: PANEL_WIDTH = 62 |
| | 17 | + integer, parameter :: MAX_VISIBLE = 8 |
| | 18 | + |
| | 19 | + type :: lsp_server_installer_panel_t |
| | 20 | + logical :: visible = .false. |
| | 21 | + integer :: selected_index = 1 |
| | 22 | + integer :: scroll_offset = 0 |
| | 23 | + type(detected_server_t), allocatable :: servers(:) |
| | 24 | + integer :: num_servers = 0 |
| | 25 | + logical :: confirm_mode = .false. |
| | 26 | + integer :: confirm_server_index = 0 |
| | 27 | + logical :: installing = .false. |
| | 28 | + character(len=256) :: status_message = '' |
| | 29 | + end type lsp_server_installer_panel_t |
| | 30 | + |
| | 31 | +contains |
| | 32 | + |
| | 33 | + subroutine init_lsp_server_installer_panel(panel) |
| | 34 | + type(lsp_server_installer_panel_t), intent(out) :: panel |
| | 35 | + |
| | 36 | + panel%visible = .false. |
| | 37 | + panel%selected_index = 1 |
| | 38 | + panel%scroll_offset = 0 |
| | 39 | + panel%num_servers = 0 |
| | 40 | + panel%confirm_mode = .false. |
| | 41 | + panel%confirm_server_index = 0 |
| | 42 | + panel%installing = .false. |
| | 43 | + panel%status_message = '' |
| | 44 | + end subroutine init_lsp_server_installer_panel |
| | 45 | + |
| | 46 | + subroutine cleanup_lsp_server_installer_panel(panel) |
| | 47 | + type(lsp_server_installer_panel_t), intent(inout) :: panel |
| | 48 | + |
| | 49 | + if (allocated(panel%servers)) deallocate(panel%servers) |
| | 50 | + panel%num_servers = 0 |
| | 51 | + end subroutine cleanup_lsp_server_installer_panel |
| | 52 | + |
| | 53 | + subroutine show_lsp_server_installer_panel(panel) |
| | 54 | + type(lsp_server_installer_panel_t), intent(inout) :: panel |
| | 55 | + |
| | 56 | + panel%visible = .true. |
| | 57 | + panel%selected_index = 1 |
| | 58 | + panel%scroll_offset = 0 |
| | 59 | + panel%confirm_mode = .false. |
| | 60 | + panel%status_message = '' |
| | 61 | + |
| | 62 | + ! Detect servers if not already done |
| | 63 | + if (panel%num_servers == 0) then |
| | 64 | + call refresh_server_status(panel) |
| | 65 | + end if |
| | 66 | + end subroutine show_lsp_server_installer_panel |
| | 67 | + |
| | 68 | + subroutine hide_lsp_server_installer_panel(panel) |
| | 69 | + type(lsp_server_installer_panel_t), intent(inout) :: panel |
| | 70 | + panel%visible = .false. |
| | 71 | + panel%confirm_mode = .false. |
| | 72 | + end subroutine hide_lsp_server_installer_panel |
| | 73 | + |
| | 74 | + function is_lsp_server_installer_panel_visible(panel) result(visible) |
| | 75 | + type(lsp_server_installer_panel_t), intent(in) :: panel |
| | 76 | + logical :: visible |
| | 77 | + visible = panel%visible |
| | 78 | + end function is_lsp_server_installer_panel_visible |
| | 79 | + |
| | 80 | + subroutine refresh_server_status(panel) |
| | 81 | + type(lsp_server_installer_panel_t), intent(inout) :: panel |
| | 82 | + |
| | 83 | + if (allocated(panel%servers)) deallocate(panel%servers) |
| | 84 | + call detect_all_servers(panel%servers, panel%num_servers) |
| | 85 | + panel%status_message = 'Server status refreshed' |
| | 86 | + end subroutine refresh_server_status |
| | 87 | + |
| | 88 | + function lsp_server_installer_panel_handle_key(panel, key) result(handled) |
| | 89 | + type(lsp_server_installer_panel_t), intent(inout) :: panel |
| | 90 | + character(len=*), intent(in) :: key |
| | 91 | + logical :: handled |
| | 92 | + type(install_result_t) :: result |
| | 93 | + |
| | 94 | + handled = .true. |
| | 95 | + |
| | 96 | + ! Handle confirm mode separately |
| | 97 | + if (panel%confirm_mode) then |
| | 98 | + select case(trim(key)) |
| | 99 | + case('y', 'Y') |
| | 100 | + ! Execute installation |
| | 101 | + panel%installing = .true. |
| | 102 | + panel%status_message = 'Installing ' // trim(panel%servers(panel%confirm_server_index)%name) // '...' |
| | 103 | + |
| | 104 | + result = run_install_command(trim(panel%servers(panel%confirm_server_index)%install_cmd)) |
| | 105 | + |
| | 106 | + panel%installing = .false. |
| | 107 | + if (result%success) then |
| | 108 | + panel%status_message = 'Successfully installed ' // trim(panel%servers(panel%confirm_server_index)%name) |
| | 109 | + ! Refresh to update status |
| | 110 | + call refresh_server_status(panel) |
| | 111 | + else |
| | 112 | + panel%status_message = 'Installation failed. Try manually: ' // & |
| | 113 | + trim(panel%servers(panel%confirm_server_index)%install_cmd) |
| | 114 | + end if |
| | 115 | + panel%confirm_mode = .false. |
| | 116 | + |
| | 117 | + case('n', 'N', 'esc', 'escape') |
| | 118 | + panel%confirm_mode = .false. |
| | 119 | + panel%status_message = '' |
| | 120 | + |
| | 121 | + case default |
| | 122 | + ! Ignore other keys in confirm mode |
| | 123 | + end select |
| | 124 | + return |
| | 125 | + end if |
| | 126 | + |
| | 127 | + ! Normal mode key handling |
| | 128 | + select case(trim(key)) |
| | 129 | + case('j', 'down') |
| | 130 | + if (panel%selected_index < panel%num_servers) then |
| | 131 | + panel%selected_index = panel%selected_index + 1 |
| | 132 | + ! Scroll if needed |
| | 133 | + if (panel%selected_index > panel%scroll_offset + MAX_VISIBLE) then |
| | 134 | + panel%scroll_offset = panel%selected_index - MAX_VISIBLE |
| | 135 | + end if |
| | 136 | + end if |
| | 137 | + |
| | 138 | + case('k', 'up') |
| | 139 | + if (panel%selected_index > 1) then |
| | 140 | + panel%selected_index = panel%selected_index - 1 |
| | 141 | + ! Scroll if needed |
| | 142 | + if (panel%selected_index <= panel%scroll_offset) then |
| | 143 | + panel%scroll_offset = panel%selected_index - 1 |
| | 144 | + end if |
| | 145 | + end if |
| | 146 | + |
| | 147 | + case('enter') |
| | 148 | + ! Only allow install for non-installed servers |
| | 149 | + if (panel%num_servers > 0 .and. panel%selected_index <= panel%num_servers) then |
| | 150 | + if (.not. panel%servers(panel%selected_index)%is_installed) then |
| | 151 | + panel%confirm_mode = .true. |
| | 152 | + panel%confirm_server_index = panel%selected_index |
| | 153 | + else |
| | 154 | + panel%status_message = trim(panel%servers(panel%selected_index)%name) // ' is already installed' |
| | 155 | + end if |
| | 156 | + end if |
| | 157 | + |
| | 158 | + case('r', 'R') |
| | 159 | + ! Refresh server status |
| | 160 | + call refresh_server_status(panel) |
| | 161 | + |
| | 162 | + case('esc', 'escape', 'q') |
| | 163 | + call hide_lsp_server_installer_panel(panel) |
| | 164 | + |
| | 165 | + case default |
| | 166 | + handled = .false. |
| | 167 | + end select |
| | 168 | + end function lsp_server_installer_panel_handle_key |
| | 169 | + |
| | 170 | + subroutine render_lsp_server_installer_panel(panel, screen_rows, screen_cols) |
| | 171 | + type(lsp_server_installer_panel_t), intent(in) :: panel |
| | 172 | + integer, intent(in) :: screen_rows, screen_cols |
| | 173 | + integer :: start_col, start_row, row, i, visible_end |
| | 174 | + integer :: content_width, visible_len, status_len, padding |
| | 175 | + character(len=:), allocatable :: border_top, border_mid, border_bottom |
| | 176 | + character(len=128) :: visible_text, status_text |
| | 177 | + character(len=*), parameter :: ESC = char(27) |
| | 178 | + character(len=*), parameter :: GREEN = ESC // '[32m' |
| | 179 | + character(len=*), parameter :: RED = ESC // '[31m' |
| | 180 | + character(len=*), parameter :: CYAN = ESC // '[36m' |
| | 181 | + character(len=*), parameter :: YELLOW = ESC // '[33m' |
| | 182 | + character(len=*), parameter :: DIM = ESC // '[90m' |
| | 183 | + character(len=*), parameter :: INVERSE = ESC // '[7m' |
| | 184 | + character(len=*), parameter :: RESET = ESC // '[0m' |
| | 185 | + |
| | 186 | + if (.not. panel%visible) return |
| | 187 | + |
| | 188 | + ! Calculate centering |
| | 189 | + content_width = min(PANEL_WIDTH, screen_cols - 4) |
| | 190 | + start_col = max(1, (screen_cols - content_width) / 2) |
| | 191 | + start_row = 2 |
| | 192 | + |
| | 193 | + ! Build borders |
| | 194 | + border_top = '┌' // repeat('─', content_width - 2) // '┐' |
| | 195 | + border_mid = '├' // repeat('─', content_width - 2) // '┤' |
| | 196 | + border_bottom = '└' // repeat('─', content_width - 2) // '┘' |
| | 197 | + |
| | 198 | + ! Render confirm dialog if in confirm mode |
| | 199 | + if (panel%confirm_mode) then |
| | 200 | + call render_confirm_dialog(panel, screen_rows, screen_cols) |
| | 201 | + return |
| | 202 | + end if |
| | 203 | + |
| | 204 | + ! Draw top border |
| | 205 | + call terminal_move_cursor(start_row, start_col) |
| | 206 | + call terminal_write(border_top) |
| | 207 | + |
| | 208 | + ! Draw header |
| | 209 | + row = start_row + 1 |
| | 210 | + call terminal_move_cursor(row, start_col) |
| | 211 | + call terminal_write('│' // CYAN // ' Language Server Manager' // RESET) |
| | 212 | + call terminal_write(repeat(' ', content_width - 31) // DIM // 'Alt+M' // RESET // ' │') |
| | 213 | + |
| | 214 | + ! Draw separator |
| | 215 | + row = row + 1 |
| | 216 | + call terminal_move_cursor(row, start_col) |
| | 217 | + call terminal_write(border_mid) |
| | 218 | + |
| | 219 | + ! Draw server list |
| | 220 | + visible_end = min(panel%scroll_offset + MAX_VISIBLE, panel%num_servers) |
| | 221 | + do i = panel%scroll_offset + 1, visible_end |
| | 222 | + row = row + 1 |
| | 223 | + call terminal_move_cursor(row, start_col) |
| | 224 | + call terminal_write('│') |
| | 225 | + |
| | 226 | + ! Build line content (visible text only for width calculation) |
| | 227 | + ! Format: " ✓ servername (Language) <spaces> status" |
| | 228 | + visible_text = ' ✓ ' // trim(panel%servers(i)%name) // ' (' // & |
| | 229 | + trim(panel%servers(i)%language) // ')' |
| | 230 | + |
| | 231 | + ! Calculate visible length (icon + space + name + space + language + parens) |
| | 232 | + visible_len = len_trim(visible_text) |
| | 233 | + |
| | 234 | + ! Add status text length |
| | 235 | + if (panel%servers(i)%is_installed) then |
| | 236 | + status_text = 'installed' |
| | 237 | + status_len = 9 |
| | 238 | + else |
| | 239 | + status_text = 'Enter to install' |
| | 240 | + status_len = 16 |
| | 241 | + end if |
| | 242 | + |
| | 243 | + ! Calculate padding needed (content_width - 2 for borders, minus visible text, minus status) |
| | 244 | + padding = max(1, content_width - 2 - visible_len - status_len) |
| | 245 | + |
| | 246 | + ! Highlight selected row |
| | 247 | + if (i == panel%selected_index) then |
| | 248 | + call terminal_write(INVERSE) |
| | 249 | + end if |
| | 250 | + |
| | 251 | + ! Write status icon with color |
| | 252 | + if (panel%servers(i)%is_installed) then |
| | 253 | + call terminal_write(' ' // GREEN // '✓' // RESET // ' ') |
| | 254 | + else |
| | 255 | + call terminal_write(' ' // RED // '✗' // RESET // ' ') |
| | 256 | + end if |
| | 257 | + |
| | 258 | + ! Write server name and language |
| | 259 | + call terminal_write(trim(panel%servers(i)%name) // ' (' // & |
| | 260 | + trim(panel%servers(i)%language) // ')') |
| | 261 | + |
| | 262 | + ! Write padding |
| | 263 | + call terminal_write(repeat(' ', padding)) |
| | 264 | + |
| | 265 | + ! Write status text with color |
| | 266 | + if (panel%servers(i)%is_installed) then |
| | 267 | + call terminal_write(DIM // trim(status_text) // RESET) |
| | 268 | + else |
| | 269 | + call terminal_write(YELLOW // trim(status_text) // RESET) |
| | 270 | + end if |
| | 271 | + |
| | 272 | + ! Reset highlighting |
| | 273 | + if (i == panel%selected_index) then |
| | 274 | + call terminal_write(RESET) |
| | 275 | + end if |
| | 276 | + |
| | 277 | + call terminal_write('│') |
| | 278 | + end do |
| | 279 | + |
| | 280 | + ! Fill remaining rows if needed |
| | 281 | + do i = visible_end + 1, panel%scroll_offset + MAX_VISIBLE |
| | 282 | + row = row + 1 |
| | 283 | + call terminal_move_cursor(row, start_col) |
| | 284 | + call terminal_write('│' // repeat(' ', content_width - 2) // '│') |
| | 285 | + end do |
| | 286 | + |
| | 287 | + ! Draw separator before footer |
| | 288 | + row = row + 1 |
| | 289 | + call terminal_move_cursor(row, start_col) |
| | 290 | + call terminal_write(border_mid) |
| | 291 | + |
| | 292 | + ! Draw status message or help |
| | 293 | + row = row + 1 |
| | 294 | + call terminal_move_cursor(row, start_col) |
| | 295 | + if (len_trim(panel%status_message) > 0) then |
| | 296 | + call terminal_write('│ ' // YELLOW // trim(panel%status_message) // RESET) |
| | 297 | + call terminal_write(repeat(' ', content_width - len_trim(panel%status_message) - 4) // ' │') |
| | 298 | + else |
| | 299 | + call terminal_write('│' // DIM // ' ↑↓ Navigate Enter Install r Refresh Esc Close' // RESET) |
| | 300 | + call terminal_write(repeat(' ', content_width - 52) // '│') |
| | 301 | + end if |
| | 302 | + |
| | 303 | + ! Draw bottom border |
| | 304 | + row = row + 1 |
| | 305 | + call terminal_move_cursor(row, start_col) |
| | 306 | + call terminal_write(border_bottom) |
| | 307 | + |
| | 308 | + ! Hide cursor while panel is shown |
| | 309 | + call terminal_hide_cursor() |
| | 310 | + end subroutine render_lsp_server_installer_panel |
| | 311 | + |
| | 312 | + subroutine render_confirm_dialog(panel, screen_rows, screen_cols) |
| | 313 | + type(lsp_server_installer_panel_t), intent(in) :: panel |
| | 314 | + integer, intent(in) :: screen_rows, screen_cols |
| | 315 | + integer :: start_col, start_row, row, content_width |
| | 316 | + character(len=:), allocatable :: border_top, border_bottom |
| | 317 | + character(len=256) :: server_name, install_cmd |
| | 318 | + character(len=*), parameter :: ESC = char(27) |
| | 319 | + character(len=*), parameter :: CYAN = ESC // '[36m' |
| | 320 | + character(len=*), parameter :: YELLOW = ESC // '[33m' |
| | 321 | + character(len=*), parameter :: GREEN = ESC // '[32m' |
| | 322 | + character(len=*), parameter :: RED = ESC // '[31m' |
| | 323 | + character(len=*), parameter :: RESET = ESC // '[0m' |
| | 324 | + |
| | 325 | + content_width = min(PANEL_WIDTH, screen_cols - 4) |
| | 326 | + start_col = max(1, (screen_cols - content_width) / 2) |
| | 327 | + start_row = 5 |
| | 328 | + |
| | 329 | + border_top = '┌' // repeat('─', content_width - 2) // '┐' |
| | 330 | + border_bottom = '└' // repeat('─', content_width - 2) // '┘' |
| | 331 | + |
| | 332 | + server_name = panel%servers(panel%confirm_server_index)%name |
| | 333 | + install_cmd = panel%servers(panel%confirm_server_index)%install_cmd |
| | 334 | + |
| | 335 | + ! Top border |
| | 336 | + call terminal_move_cursor(start_row, start_col) |
| | 337 | + call terminal_write(border_top) |
| | 338 | + |
| | 339 | + ! Title |
| | 340 | + row = start_row + 1 |
| | 341 | + call terminal_move_cursor(row, start_col) |
| | 342 | + call terminal_write('│' // CYAN // ' Install ' // trim(server_name) // '?' // RESET) |
| | 343 | + call terminal_write(repeat(' ', content_width - 12 - len_trim(server_name)) // '│') |
| | 344 | + |
| | 345 | + ! Blank line |
| | 346 | + row = row + 1 |
| | 347 | + call terminal_move_cursor(row, start_col) |
| | 348 | + call terminal_write('│' // repeat(' ', content_width - 2) // '│') |
| | 349 | + |
| | 350 | + ! Command |
| | 351 | + row = row + 1 |
| | 352 | + call terminal_move_cursor(row, start_col) |
| | 353 | + call terminal_write('│ Command: ' // YELLOW // trim(install_cmd) // RESET) |
| | 354 | + call terminal_write(repeat(' ', content_width - 12 - len_trim(install_cmd)) // '│') |
| | 355 | + |
| | 356 | + ! Blank line |
| | 357 | + row = row + 1 |
| | 358 | + call terminal_move_cursor(row, start_col) |
| | 359 | + call terminal_write('│' // repeat(' ', content_width - 2) // '│') |
| | 360 | + |
| | 361 | + ! Yes/No buttons |
| | 362 | + row = row + 1 |
| | 363 | + call terminal_move_cursor(row, start_col) |
| | 364 | + call terminal_write('│' // repeat(' ', (content_width - 20) / 2)) |
| | 365 | + call terminal_write('[' // GREEN // 'Y' // RESET // ']es ') |
| | 366 | + call terminal_write('[' // RED // 'N' // RESET // ']o') |
| | 367 | + call terminal_write(repeat(' ', (content_width - 20) / 2) // '│') |
| | 368 | + |
| | 369 | + ! Bottom border |
| | 370 | + row = row + 1 |
| | 371 | + call terminal_move_cursor(row, start_col) |
| | 372 | + call terminal_write(border_bottom) |
| | 373 | + |
| | 374 | + call terminal_hide_cursor() |
| | 375 | + end subroutine render_confirm_dialog |
| | 376 | + |
| | 377 | +end module lsp_server_installer_panel_module |