| 1 | ! ============================================================================== |
| 2 | ! Module: config |
| 3 | ! Purpose: Shell configuration file handling (.fshrc) |
| 4 | ! ============================================================================== |
| 5 | module shell_config |
| 6 | use shell_types |
| 7 | use system_interface |
| 8 | use variables |
| 9 | use iso_fortran_env, only: input_unit, output_unit, error_unit |
| 10 | implicit none |
| 11 | |
| 12 | ! Forward declaration to avoid circular dependency |
| 13 | abstract interface |
| 14 | subroutine parse_and_execute_interface(input_line, shell) |
| 15 | import :: shell_state_t |
| 16 | character(len=*), intent(in) :: input_line |
| 17 | type(shell_state_t), intent(inout) :: shell |
| 18 | end subroutine |
| 19 | end interface |
| 20 | |
| 21 | procedure(parse_and_execute_interface), pointer :: parse_and_execute_proc => null() |
| 22 | |
| 23 | contains |
| 24 | |
| 25 | ! Main entry point - loads configs based on shell type |
| 26 | subroutine load_config_file(shell) |
| 27 | type(shell_state_t), intent(inout) :: shell |
| 28 | |
| 29 | ! Check if this is first run and prompt for config creation |
| 30 | if (shell%is_interactive) then |
| 31 | call check_first_run_and_prompt(shell) |
| 32 | end if |
| 33 | |
| 34 | if (shell%is_login_shell) then |
| 35 | ! Login shell: load profile files |
| 36 | call load_login_configs(shell) |
| 37 | else if (shell%is_interactive) then |
| 38 | ! Interactive non-login shell: load rc files |
| 39 | call load_interactive_configs(shell) |
| 40 | else |
| 41 | ! Non-interactive shell: check ENV variable |
| 42 | call load_noninteractive_configs(shell) |
| 43 | end if |
| 44 | end subroutine |
| 45 | |
| 46 | ! Check if this is first run (no config files) and prompt user |
| 47 | subroutine check_first_run_and_prompt(shell) |
| 48 | type(shell_state_t), intent(inout) :: shell |
| 49 | character(len=MAX_PATH_LEN) :: home_dir |
| 50 | logical :: fortshrc_exists, fortsh_profile_exists |
| 51 | character(len=10) :: response |
| 52 | character(len=16) :: test_mode |
| 53 | integer :: stat |
| 54 | |
| 55 | ! Skip first-run prompt in test mode |
| 56 | call get_environment_variable('FORTSH_TEST_MODE', test_mode, status=stat) |
| 57 | if (stat == 0 .and. len_trim(test_mode) > 0) return |
| 58 | |
| 59 | if (.false.) print *, shell%cwd ! Silence unused warning - shell kept for future use |
| 60 | |
| 61 | ! Get home directory using intrinsic |
| 62 | home_dir = '' |
| 63 | call get_environment_variable('HOME', home_dir) |
| 64 | if (len_trim(home_dir) == 0) return |
| 65 | |
| 66 | ! Check if config files exist |
| 67 | inquire(file=trim(home_dir)//'/.fortshrc', exist=fortshrc_exists) |
| 68 | inquire(file=trim(home_dir)//'/.fortsh_profile', exist=fortsh_profile_exists) |
| 69 | |
| 70 | ! If at least one exists, assume not first run |
| 71 | if (fortshrc_exists .or. fortsh_profile_exists) return |
| 72 | |
| 73 | ! First run detected - prompt user |
| 74 | write(output_unit, '(a)') '' |
| 75 | write(output_unit, '(a)') '===================================================================' |
| 76 | write(output_unit, '(a)') 'Welcome to Fortran Shell (fortsh)!' |
| 77 | write(output_unit, '(a)') 'It looks like this is your first time running fortsh.' |
| 78 | write(output_unit, '(a)') '' |
| 79 | write(output_unit, '(a)') 'Would you like to create default configuration files?' |
| 80 | write(output_unit, '(a)') ' - ~/.fortshrc (interactive shell config)' |
| 81 | write(output_unit, '(a)') ' - ~/.fortsh_profile (login shell config)' |
| 82 | write(output_unit, '(a)') ' - ~/.fortsh_logout (logout script)' |
| 83 | write(output_unit, '(a)') '===================================================================' |
| 84 | write(output_unit, '(a)', advance='no') 'Create default configs? [Y/n]: ' |
| 85 | |
| 86 | ! Read user response |
| 87 | read(*, '(a)') response |
| 88 | |
| 89 | ! Check response (default to yes) |
| 90 | if (len_trim(response) == 0 .or. & |
| 91 | response(1:1) == 'Y' .or. response(1:1) == 'y') then |
| 92 | write(output_unit, '(a)') '' |
| 93 | write(output_unit, '(a)') 'Creating default configuration files...' |
| 94 | call create_default_config() |
| 95 | write(output_unit, '(a)') '' |
| 96 | write(output_unit, '(a)') 'Configuration files created successfully!' |
| 97 | write(output_unit, '(a)') 'You can customize them by editing the files in your home directory.' |
| 98 | write(output_unit, '(a)') 'Type "config show" to view the current configuration.' |
| 99 | write(output_unit, '(a)') '' |
| 100 | else |
| 101 | write(output_unit, '(a)') '' |
| 102 | write(output_unit, '(a)') 'Skipping config creation.' |
| 103 | write(output_unit, '(a)') 'You can create them later by running: config create' |
| 104 | write(output_unit, '(a)') '' |
| 105 | end if |
| 106 | end subroutine |
| 107 | |
| 108 | ! Load configuration for login shells |
| 109 | subroutine load_login_configs(shell) |
| 110 | type(shell_state_t), intent(inout) :: shell |
| 111 | character(len=MAX_PATH_LEN) :: home_dir |
| 112 | logical :: file_exists |
| 113 | |
| 114 | ! 1. System-wide profile |
| 115 | call source_if_exists('/etc/fortsh/profile', shell, .false.) |
| 116 | |
| 117 | ! 2. User profile (try in order) |
| 118 | home_dir = ''; call get_environment_variable('HOME', home_dir) |
| 119 | if (len(home_dir) > 0) then |
| 120 | ! Try ~/.fortsh_profile first |
| 121 | inquire(file=trim(home_dir)//'/.fortsh_profile', exist=file_exists) |
| 122 | if (file_exists) then |
| 123 | call source_if_exists(trim(home_dir)//'/.fortsh_profile', shell, .true.) |
| 124 | return |
| 125 | end if |
| 126 | |
| 127 | ! Fall back to ~/.profile (POSIX compatibility) |
| 128 | call source_if_exists(trim(home_dir)//'/.profile', shell, .true.) |
| 129 | end if |
| 130 | end subroutine |
| 131 | |
| 132 | ! Load configuration for interactive non-login shells |
| 133 | subroutine load_interactive_configs(shell) |
| 134 | type(shell_state_t), intent(inout) :: shell |
| 135 | character(len=MAX_PATH_LEN) :: home_dir, rc_file |
| 136 | |
| 137 | ! Check for FORTSH_RC_FILE environment variable |
| 138 | rc_file = '' |
| 139 | call get_environment_variable('FORTSH_RC_FILE', rc_file) |
| 140 | |
| 141 | if (len_trim(rc_file) > 0) then |
| 142 | ! Use specified rc file (or skip if /dev/null) |
| 143 | if (trim(rc_file) /= '/dev/null') then |
| 144 | call source_if_exists(trim(rc_file), shell, .true.) |
| 145 | end if |
| 146 | return |
| 147 | end if |
| 148 | |
| 149 | ! 1. System-wide rc file |
| 150 | call source_if_exists('/etc/fortsh/fortshrc', shell, .false.) |
| 151 | |
| 152 | ! 2. User rc file |
| 153 | home_dir = ''; call get_environment_variable('HOME', home_dir) |
| 154 | if (len(home_dir) > 0) then |
| 155 | ! Try ~/.fortshrc (new style) |
| 156 | call source_if_exists(trim(home_dir)//'/.fortshrc', shell, .true.) |
| 157 | |
| 158 | ! Also try legacy ~/.fshrc for backward compatibility |
| 159 | call source_if_exists(trim(home_dir)//'/.fshrc', shell, .false.) |
| 160 | end if |
| 161 | end subroutine |
| 162 | |
| 163 | ! Load configuration for non-interactive shells |
| 164 | subroutine load_noninteractive_configs(shell) |
| 165 | type(shell_state_t), intent(inout) :: shell |
| 166 | character(len=MAX_PATH_LEN) :: env_file |
| 167 | |
| 168 | ! Check ENV variable |
| 169 | env_file = ''; call get_environment_variable('ENV', env_file) |
| 170 | if (len(env_file) > 0) then |
| 171 | call source_if_exists(env_file, shell, .false.) |
| 172 | end if |
| 173 | end subroutine |
| 174 | |
| 175 | ! Helper: source a file if it exists |
| 176 | subroutine source_if_exists(filepath, shell, verbose) |
| 177 | character(len=*), intent(in) :: filepath |
| 178 | type(shell_state_t), intent(inout) :: shell |
| 179 | logical, intent(in) :: verbose |
| 180 | logical :: file_exists |
| 181 | |
| 182 | inquire(file=filepath, exist=file_exists) |
| 183 | if (.not. file_exists) return |
| 184 | |
| 185 | if (verbose) then |
| 186 | write(output_unit, '(a)') 'Loading ' // trim(filepath) // '...' |
| 187 | end if |
| 188 | |
| 189 | ! Set up to source the file |
| 190 | shell%source_file = filepath |
| 191 | shell%should_source = .true. |
| 192 | end subroutine |
| 193 | |
| 194 | ! Legacy load function for backward compatibility |
| 195 | subroutine load_legacy_config(shell) |
| 196 | type(shell_state_t), intent(inout) :: shell |
| 197 | character(len=MAX_PATH_LEN) :: home_dir, config_file |
| 198 | character(len=4096) :: line |
| 199 | integer :: unit, iostat |
| 200 | logical :: file_exists |
| 201 | |
| 202 | ! Get home directory |
| 203 | home_dir = ''; call get_environment_variable('HOME', home_dir) |
| 204 | if (len(home_dir) == 0) then |
| 205 | return ! No HOME directory, skip config |
| 206 | end if |
| 207 | |
| 208 | ! Construct config file path |
| 209 | config_file = trim(home_dir) // '/.fshrc' |
| 210 | |
| 211 | ! Check if config file exists |
| 212 | inquire(file=config_file, exist=file_exists) |
| 213 | if (.not. file_exists) then |
| 214 | return ! No config file, continue normally |
| 215 | end if |
| 216 | |
| 217 | ! Try to open and read the config file |
| 218 | open(newunit=unit, file=config_file, status='old', action='read', iostat=iostat) |
| 219 | if (iostat /= 0) then |
| 220 | write(error_unit, '(a)') 'fortsh: warning: could not read .fshrc' |
| 221 | return |
| 222 | end if |
| 223 | |
| 224 | write(output_unit, '(a)') 'Loading .fshrc...' |
| 225 | |
| 226 | ! Read and execute each line (simplified approach for now) |
| 227 | do |
| 228 | read(unit, '(a)', iostat=iostat) line |
| 229 | if (iostat /= 0) exit ! End of file or error |
| 230 | |
| 231 | ! Skip empty lines and comments |
| 232 | line = adjustl(line) |
| 233 | if (len_trim(line) == 0 .or. line(1:1) == '#') cycle |
| 234 | |
| 235 | ! For now, just execute simple variable assignments |
| 236 | if (index(line, '=') > 0 .and. index(line, ' ') == 0) then |
| 237 | call process_config_assignment(line, shell) |
| 238 | end if |
| 239 | end do |
| 240 | |
| 241 | close(unit) |
| 242 | write(output_unit, '(a)') '.fshrc loaded successfully' |
| 243 | end subroutine |
| 244 | |
| 245 | subroutine process_config_assignment(line, shell) |
| 246 | character(len=*), intent(in) :: line |
| 247 | type(shell_state_t), intent(inout) :: shell |
| 248 | integer :: eq_pos |
| 249 | character(len=256) :: var_name, var_value |
| 250 | |
| 251 | eq_pos = index(line, '=') |
| 252 | if (eq_pos > 1) then |
| 253 | var_name = line(:eq_pos-1) |
| 254 | var_value = line(eq_pos+1:) |
| 255 | |
| 256 | ! Set as shell variable |
| 257 | call set_shell_variable(shell, trim(var_name), trim(var_value)) |
| 258 | end if |
| 259 | end subroutine |
| 260 | |
| 261 | ! Create default configuration files |
| 262 | subroutine create_default_config() |
| 263 | character(len=MAX_PATH_LEN) :: home_dir |
| 264 | |
| 265 | ! Get home directory |
| 266 | home_dir = ''; call get_environment_variable('HOME', home_dir) |
| 267 | if (len(home_dir) == 0) then |
| 268 | write(output_unit, '(a)') 'fortsh: warning: HOME not set, cannot create config files' |
| 269 | return |
| 270 | end if |
| 271 | |
| 272 | ! Create all default config files |
| 273 | call create_fortshrc(home_dir) |
| 274 | call create_fortsh_profile(home_dir) |
| 275 | call create_fortsh_logout(home_dir) |
| 276 | end subroutine |
| 277 | |
| 278 | ! Create default ~/.fortshrc |
| 279 | subroutine create_fortshrc(home_dir) |
| 280 | character(len=*), intent(in) :: home_dir |
| 281 | character(len=MAX_PATH_LEN) :: config_file |
| 282 | integer :: unit, iostat |
| 283 | logical :: file_exists |
| 284 | |
| 285 | config_file = trim(home_dir) // '/.fortshrc' |
| 286 | |
| 287 | inquire(file=config_file, exist=file_exists) |
| 288 | if (file_exists) then |
| 289 | write(output_unit, '(a)') 'fortsh: ~/.fortshrc already exists' |
| 290 | return |
| 291 | end if |
| 292 | |
| 293 | open(newunit=unit, file=config_file, status='new', action='write', iostat=iostat) |
| 294 | if (iostat /= 0) then |
| 295 | write(error_unit, '(a)') 'fortsh: error: could not create ~/.fortshrc' |
| 296 | return |
| 297 | end if |
| 298 | |
| 299 | ! Write comprehensive default configuration |
| 300 | write(unit, '(a)') '# ~/.fortshrc - Fortsh interactive shell configuration' |
| 301 | write(unit, '(a)') '# This file is sourced by interactive non-login shells' |
| 302 | write(unit, '(a)') '' |
| 303 | write(unit, '(a)') '# ===== Prompt Configuration =====' |
| 304 | write(unit, '(a)') '# Default: 2-line prompt with colors (zsh-style %F{color} or bash-style \[\e[...m\])' |
| 305 | write(unit, '(a)') '# Line 1: user@host :: path:branch status tracking venv' |
| 306 | write(unit, '(a)') '# Line 2: prompt character' |
| 307 | write(unit, '(a)') 'PS1=''%F{green}\u@\h%f :: %F{blue}\w%f%F{yellow}:\g%f %F{green}\G%f%F{cyan}\p%f \P' |
| 308 | write(unit, '(a)') '> ''' |
| 309 | write(unit, '(a)') '' |
| 310 | write(unit, '(a)') '# RPROMPT: Right-side prompt (like zsh)' |
| 311 | write(unit, '(a)') 'RPROMPT=''%F{240}\A%f''' |
| 312 | write(unit, '(a)') '' |
| 313 | write(unit, '(a)') '# Prompt escape sequences:' |
| 314 | write(unit, '(a)') '# \u user \h host \w path \W basename \$ #/$ by uid' |
| 315 | write(unit, '(a)') '# \g git branch \G git status (checkmark/x/+) \p git up/down tracking' |
| 316 | write(unit, '(a)') '# \P venv name (.venv) \j jobs \! history# \# cmd#' |
| 317 | write(unit, '(a)') '# \t 24h time \T 12h time \d date \S epoch seconds' |
| 318 | write(unit, '(a)') '' |
| 319 | write(unit, '(a)') '# Alternative prompts (uncomment to use)' |
| 320 | write(unit, '(a)') '# Minimal: PS1=''\w> ''' |
| 321 | write(unit, '(a)') '# Classic: PS1=''\u@\h :: \w > ''' |
| 322 | write(unit, '(a)') '# No git: PS1=''%F{green}\u@\h%f :: %F{blue}\w%f' |
| 323 | write(unit, '(a)') '#> ''' |
| 324 | write(unit, '(a)') '' |
| 325 | write(unit, '(a)') '# ===== Environment Variables =====' |
| 326 | write(unit, '(a)') 'export EDITOR=vim' |
| 327 | write(unit, '(a)') 'export PAGER=less' |
| 328 | write(unit, '(a)') '' |
| 329 | write(unit, '(a)') '# ===== Aliases =====' |
| 330 | write(unit, '(a)') 'alias ll=''ls -lah''' |
| 331 | write(unit, '(a)') 'alias la=''ls -A''' |
| 332 | write(unit, '(a)') 'alias ..=''cd ..''' |
| 333 | write(unit, '(a)') 'alias ...=''cd ../..''' |
| 334 | write(unit, '(a)') '' |
| 335 | write(unit, '(a)') '# ===== Shell Options =====' |
| 336 | write(unit, '(a)') '# set -o emacs # Emacs editing mode' |
| 337 | |
| 338 | close(unit) |
| 339 | write(output_unit, '(a)') 'Created: ~/.fortshrc' |
| 340 | end subroutine |
| 341 | |
| 342 | ! Create default ~/.fortsh_profile |
| 343 | subroutine create_fortsh_profile(home_dir) |
| 344 | character(len=*), intent(in) :: home_dir |
| 345 | character(len=MAX_PATH_LEN) :: config_file |
| 346 | integer :: unit, iostat |
| 347 | logical :: file_exists |
| 348 | |
| 349 | config_file = trim(home_dir) // '/.fortsh_profile' |
| 350 | |
| 351 | inquire(file=config_file, exist=file_exists) |
| 352 | if (file_exists) then |
| 353 | write(output_unit, '(a)') 'fortsh: ~/.fortsh_profile already exists' |
| 354 | return |
| 355 | end if |
| 356 | |
| 357 | open(newunit=unit, file=config_file, status='new', action='write', iostat=iostat) |
| 358 | if (iostat /= 0) then |
| 359 | write(error_unit, '(a)') 'fortsh: error: could not create ~/.fortsh_profile' |
| 360 | return |
| 361 | end if |
| 362 | |
| 363 | write(unit, '(a)') '# ~/.fortsh_profile - Fortsh login shell configuration' |
| 364 | write(unit, '(a)') '# This file is sourced by login shells' |
| 365 | write(unit, '(a)') '' |
| 366 | write(unit, '(a)') '# ===== PATH Configuration =====' |
| 367 | write(unit, '(a)') 'export PATH="$HOME/bin:$HOME/.local/bin:$PATH"' |
| 368 | write(unit, '(a)') '' |
| 369 | write(unit, '(a)') '# ===== Source interactive config if shell is interactive =====' |
| 370 | write(unit, '(a)') '# This ensures ~/.fortshrc is loaded in login shells too' |
| 371 | write(unit, '(a)') 'if [ -f ~/.fortshrc ]; then' |
| 372 | write(unit, '(a)') ' source ~/.fortshrc' |
| 373 | write(unit, '(a)') 'fi' |
| 374 | write(unit, '(a)') '' |
| 375 | write(unit, '(a)') '# ===== Login-specific setup =====' |
| 376 | write(unit, '(a)') '# Set umask' |
| 377 | write(unit, '(a)') '# umask 022' |
| 378 | write(unit, '(a)') '' |
| 379 | write(unit, '(a)') '# Display login information' |
| 380 | write(unit, '(a)') 'echo "Logged in as $USER on $HOSTNAME"' |
| 381 | write(unit, '(a)') 'echo "Today is $(date)"' |
| 382 | |
| 383 | close(unit) |
| 384 | write(output_unit, '(a)') 'Created: ~/.fortsh_profile' |
| 385 | end subroutine |
| 386 | |
| 387 | ! Create default ~/.fortsh_logout |
| 388 | subroutine create_fortsh_logout(home_dir) |
| 389 | character(len=*), intent(in) :: home_dir |
| 390 | character(len=MAX_PATH_LEN) :: config_file |
| 391 | integer :: unit, iostat |
| 392 | logical :: file_exists |
| 393 | |
| 394 | config_file = trim(home_dir) // '/.fortsh_logout' |
| 395 | |
| 396 | inquire(file=config_file, exist=file_exists) |
| 397 | if (file_exists) then |
| 398 | write(output_unit, '(a)') 'fortsh: ~/.fortsh_logout already exists' |
| 399 | return |
| 400 | end if |
| 401 | |
| 402 | open(newunit=unit, file=config_file, status='new', action='write', iostat=iostat) |
| 403 | if (iostat /= 0) then |
| 404 | write(error_unit, '(a)') 'fortsh: error: could not create ~/.fortsh_logout' |
| 405 | return |
| 406 | end if |
| 407 | |
| 408 | write(unit, '(a)') '# ~/.fortsh_logout - Fortsh logout script' |
| 409 | write(unit, '(a)') '# This file is executed when a login shell exits' |
| 410 | write(unit, '(a)') '' |
| 411 | write(unit, '(a)') '# ===== Cleanup tasks =====' |
| 412 | write(unit, '(a)') '# Clear the screen' |
| 413 | write(unit, '(a)') '# clear' |
| 414 | write(unit, '(a)') '' |
| 415 | write(unit, '(a)') '# Display logout message' |
| 416 | write(unit, '(a)') 'echo "Logging out from fortsh..."' |
| 417 | write(unit, '(a)') 'echo "Session ended at $(date)"' |
| 418 | |
| 419 | close(unit) |
| 420 | write(output_unit, '(a)') 'Created: ~/.fortsh_logout' |
| 421 | end subroutine |
| 422 | |
| 423 | ! Show the current config file content |
| 424 | subroutine show_config() |
| 425 | character(len=MAX_PATH_LEN) :: home_dir, config_file |
| 426 | character(len=4096) :: line |
| 427 | integer :: unit, iostat |
| 428 | logical :: file_exists |
| 429 | |
| 430 | ! Get home directory |
| 431 | home_dir = ''; call get_environment_variable('HOME', home_dir) |
| 432 | if (len(home_dir) == 0) then |
| 433 | write(output_unit, '(a)') 'fortsh: warning: HOME not set' |
| 434 | return |
| 435 | end if |
| 436 | |
| 437 | ! Construct config file path |
| 438 | config_file = trim(home_dir) // '/.fshrc' |
| 439 | |
| 440 | ! Check if config file exists |
| 441 | inquire(file=config_file, exist=file_exists) |
| 442 | if (.not. file_exists) then |
| 443 | write(output_unit, '(a)') 'fortsh: no .fshrc file found' |
| 444 | return |
| 445 | end if |
| 446 | |
| 447 | ! Open and display the config file |
| 448 | open(newunit=unit, file=config_file, status='old', action='read', iostat=iostat) |
| 449 | if (iostat /= 0) then |
| 450 | write(error_unit, '(a)') 'fortsh: error: could not read .fshrc' |
| 451 | return |
| 452 | end if |
| 453 | |
| 454 | write(output_unit, '(a)') 'Contents of .fshrc:' |
| 455 | write(output_unit, '(a)') '==================' |
| 456 | |
| 457 | do |
| 458 | read(unit, '(a)', iostat=iostat) line |
| 459 | if (iostat /= 0) exit |
| 460 | write(output_unit, '(a)') trim(line) |
| 461 | end do |
| 462 | |
| 463 | close(unit) |
| 464 | end subroutine |
| 465 | |
| 466 | end module shell_config |