Configuration Files Specification
Version: 1.0 Last Updated: 2025-01-05
Overview
This document specifies the format and location of fac's user configuration files. These files are stored in the user's config directory and track global state across all workspaces.
Purpose:
- Track recently opened workspaces
- Store user-marked favorite workspaces
- Manage backup metadata for dirty buffers
Location: Follows XDG Base Directory Specification
- Linux/macOS:
~/.config/fac/ - Fallback:
~/.fac/if XDG not available
Directory Structure
~/.config/fac/
├── favorites.json # User-marked favorite workspaces
├── recents.json # Auto-tracked recent workspaces
└── README.txt # Optional: explain what this directory is
favorites.json Specification
Purpose
Store user-manually marked favorite workspaces for quick access via Fortress welcome menu.
Location
~/.config/fac/favorites.json
Schema
{
"version": "1.0",
"favorites": [
{
"path": "/absolute/path/to/workspace",
"label": "My Project",
"added": "2025-01-05T10:30:00Z",
"pinned": false
}
]
}
Field Descriptions
Root Object
version (string, required)
- Schema version for compatibility
- Format: "major.minor"
- Current: "1.0"
favorites (array of objects, required)
- Array of favorite workspace objects
- Order matters (display order in Fortress)
- Can be empty
Favorite Object
path (string, required)
- Absolute path to workspace root
- Must be a directory (verified on load)
- Example: "/home/user/projects/myapp"
label (string, required)
- User-friendly display name
- Defaults to directory basename
- User can customize
- Example: "My Fortran Project"
added (string, required)
- ISO 8601 timestamp when favorite was added
- Format: "YYYY-MM-DDTHH:MM:SSZ"
- Used for sorting (if needed)
- Example: "2025-01-05T10:30:00Z"
pinned (boolean, required)
- Whether this favorite should stay at top
- Pinned favorites shown first (Phase 7 feature)
- Default: false
Example favorites.json
{
"version": "1.0",
"favorites": [
{
"path": "/home/user/projects/facsimile",
"label": "fac Editor",
"added": "2025-01-01T08:00:00Z",
"pinned": true
},
{
"path": "/home/user/projects/fortress",
"label": "Fortress Navigator",
"added": "2025-01-02T09:15:00Z",
"pinned": false
},
{
"path": "/home/user/Documents/thesis",
"label": "PhD Thesis",
"added": "2025-01-03T14:30:00Z",
"pinned": false
}
]
}
Operations
Add Favorite
- Trigger: Press 'f' in Fortress navigator
- Action: Add current directory to favorites list
- Duplicate Check: If path already exists, don't add again (show message)
- Label: Default to basename, allow edit later (Phase 7)
Remove Favorite
- Trigger: Press 'r' or 'x' in Fortress favorites view
- Action: Remove selected favorite from list
- Confirmation: Prompt "Remove favorite? [y/n]"
Reorder Favorites (Phase 7)
- Trigger: Drag/drop or move commands
- Action: Change order in array
- Persistence: Save immediately
recents.json Specification
Purpose
Automatically track recently opened workspaces for quick access. Similar to "Recent Files" in most editors.
Location
~/.config/fac/recents.json
Schema
{
"version": "1.0",
"max_recents": 20,
"recents": [
{
"path": "/absolute/path/to/workspace",
"label": "Auto-generated label",
"last_opened": "2025-01-05T14:45:00Z",
"open_count": 5
}
]
}
Field Descriptions
Root Object
version (string, required)
- Schema version
- Current: "1.0"
max_recents (integer, required)
- Maximum number of recents to track
- Default: 20
- When exceeded, oldest entries are removed
- User configurable (Phase 7)
recents (array of objects, required)
- Array of recent workspace objects
- Sorted by
last_opened(most recent first) - Can be empty
Recent Object
path (string, required)
- Absolute path to workspace root
- Example: "/home/user/projects/myapp"
label (string, required)
- Display name (typically basename)
- Auto-generated from path
- Example: "myapp"
last_opened (string, required)
- ISO 8601 timestamp of last open
- Format: "YYYY-MM-DDTHH:MM:SSZ"
- Updated every time workspace is opened
- Example: "2025-01-05T14:45:00Z"
open_count (integer, required)
- Number of times this workspace has been opened
- Used for statistics (Phase 7)
- Incremented on each open
- Default: 1
Example recents.json
{
"version": "1.0",
"max_recents": 20,
"recents": [
{
"path": "/home/user/projects/facsimile",
"label": "facsimile",
"last_opened": "2025-01-05T15:30:00Z",
"open_count": 47
},
{
"path": "/home/user/projects/fortress",
"label": "fortress",
"last_opened": "2025-01-05T10:15:00Z",
"open_count": 23
},
{
"path": "/tmp/scratch",
"label": "scratch",
"last_opened": "2025-01-04T16:45:00Z",
"open_count": 3
}
]
}
Operations
Add/Update Recent
- Trigger: Every time a workspace is opened
- Action:
- If path exists in recents: update
last_opened, incrementopen_count - If path doesn't exist: add new entry
- Re-sort by
last_opened(most recent first) - If count >
max_recents: remove oldest entry
- If path exists in recents: update
Clear Recents
- Trigger: User command (Phase 7)
- Action: Empty recents array
- File: Keep file, just empty the array
Remove Recent
- Trigger: Press 'r' or 'x' in Fortress recents view
- Action: Remove selected entry
- No confirmation: Recents are auto-tracked, safe to remove
Backup Metadata Specification
Purpose
Track backup files for dirty buffers when user quits without saving.
Location
<workspace_root>/.fac/backups/.backup-metadata.json
Schema
{
"version": "1.0",
"backups": [
{
"original_file": "src/main.f90",
"backup_file": "src/main.f90.bak",
"timestamp": "2025-01-05T10:30:00Z",
"reason": "quit_without_save"
}
]
}
Field Descriptions
Root Object
version (string, required)
- Schema version
- Current: "1.0"
backups (array of objects, required)
- Array of backup entries
- Can be empty
- Cleaned up after restore or ignore
Backup Object
original_file (string, required)
- Relative path to original file (within workspace)
- Example: "src/main.f90"
backup_file (string, required)
- Relative path to backup file
- Typically
{original_file}.bak - Example: "src/main.f90.bak"
timestamp (string, required)
- ISO 8601 timestamp when backup was created
- Format: "YYYY-MM-DDTHH:MM:SSZ"
- Example: "2025-01-05T10:30:00Z"
reason (string, required)
- Why backup was created
- Values:
"quit_without_save"- User quit with dirty buffer"crash"- Editor crashed (Phase 7)"switch_workspace"- Switched workspace with dirty buffer (Phase 6)
Example .backup-metadata.json
{
"version": "1.0",
"backups": [
{
"original_file": "src/main.f90",
"backup_file": "src/main.f90.bak",
"timestamp": "2025-01-05T10:30:00Z",
"reason": "quit_without_save"
},
{
"original_file": "tests/test.f90",
"backup_file": "tests/test.f90.bak",
"timestamp": "2025-01-05T10:30:00Z",
"reason": "quit_without_save"
}
]
}
Operations
Create Backup
- Trigger: Quit with dirty buffers, user chooses 'n' (don't save)
- Action:
- For each dirty buffer, copy to
{filename}.bak - Add entry to
.backup-metadata.json - Write metadata atomically
- For each dirty buffer, copy to
Detect Backups
- Trigger: Open workspace
- Action:
- Check if
.fac/backups/.backup-metadata.jsonexists - If exists and has entries, prompt user
- Check if
Restore Backup
- Trigger: User selects 'r' (restore) in backup prompt
- Action:
- Copy backup file to original location (overwrite)
- Remove backup file
- Remove entry from metadata
- Load file in editor
Ignore Backup
- Trigger: User selects 'i' (ignore) in backup prompt
- Action:
- Keep backup file (don't delete)
- Remove entry from metadata
- Load original file in editor
Show Diff
- Trigger: User selects 'd' (diff) in backup prompt
- Action:
- Run
diffcommand (or internal diff) - Display in pager or split view
- Return to prompt (user can then choose r/i)
- Run
Cleanup
- Trigger: After restore or ignore all backups
- Action:
- If metadata.backups array is empty, delete
.backup-metadata.json - If
.fac/backups/directory is empty, delete directory
- If metadata.backups array is empty, delete
File Operations
Creating Config Directory
On first run:
- Check if
~/.config/fac/exists - If not, create directory with 0755 permissions
- Create empty
favorites.jsonandrecents.json - Optionally create
README.txtexplaining purpose
Fortran Example:
subroutine ensure_config_directory()
character(len=:), allocatable :: config_path
logical :: exists
config_path = get_config_directory() ! Returns ~/.config/fac/
inquire(file=config_path, exist=exists)
if (.not. exists) then
call create_directory(config_path)
call create_empty_config_files(config_path)
end if
end subroutine
Atomic Writes
Critical: Always use atomic writes for config files to avoid corruption.
Pattern:
- Write to temporary file:
favorites.json.tmp - Verify write succeeded (check iostat)
- Rename temp to final:
rename(tmp, final) - Rename is atomic on POSIX systems
Fortran Example:
subroutine save_favorites(favorites)
type(favorites_t), intent(in) :: favorites
character(len=:), allocatable :: path, tmp_path
integer :: iostat
path = get_config_directory() // 'favorites.json'
tmp_path = path // '.tmp'
! Write to temp file
call write_json_to_file(tmp_path, favorites, iostat)
if (iostat == 0) then
! Rename temp to final (atomic)
call rename_file(tmp_path, path)
else
! Handle error
call delete_file(tmp_path)
end if
end subroutine
Reading Config Files
Strategy: Graceful fallback on errors
subroutine load_favorites(favorites)
type(favorites_t), intent(out) :: favorites
character(len=:), allocatable :: path
logical :: exists
integer :: iostat
path = get_config_directory() // 'favorites.json'
inquire(file=path, exist=exists)
if (.not. exists) then
! Create empty favorites
call initialize_empty_favorites(favorites)
call save_favorites(favorites)
return
end if
call parse_json_file(path, favorites, iostat)
if (iostat /= 0) then
! Corrupted file: backup and recreate
call backup_corrupt_file(path)
call initialize_empty_favorites(favorites)
call save_favorites(favorites)
end if
end subroutine
Error Handling
File Not Found
- Action: Create with default/empty content
- Log: No error message (expected on first run)
Permission Denied
- Action: Warn user, run in read-only mode
- Message: "Cannot write to config directory. Favorites/recents will not persist."
Corrupted JSON
- Action: Backup corrupt file, create new empty config
- Backup: Rename to
{file}.corrupted.{timestamp} - Log: Warning message with backup location
Disk Full
- Action: Warn user, skip update
- Message: "Cannot save config: disk full"
Invalid Data
- Action: Skip invalid entries, log warning
- Example: Path doesn't exist → skip that favorite
- Message: "Skipped invalid favorite: /nonexistent/path"
Validation Rules
On Load
favorites.json
- Verify
versionis supported - For each favorite:
- Verify
pathis absolute - Verify
pathexists (if not, skip with warning) - Verify
addedis valid ISO 8601 - Clamp
pinnedto boolean
- Verify
recents.json
- Verify
versionis supported - Verify
max_recentsis positive integer (default 20 if invalid) - For each recent:
- Verify
pathis absolute - If
pathdoesn't exist, remove entry (cleaned up automatically) - Verify
last_openedis valid ISO 8601 - Verify
open_countis non-negative integer
- Verify
.backup-metadata.json
- Verify
versionis supported - For each backup:
- Verify
backup_fileexists (if not, skip entry) - Verify
timestampis valid ISO 8601 - Verify
reasonis valid enum value
- Verify
On Save
Path Normalization
- Always store absolute paths (no relative paths)
- Resolve symlinks before storing
- Remove trailing slashes
Timestamp Format
- Always use ISO 8601: "YYYY-MM-DDTHH:MM:SSZ"
- Always use UTC timezone (Z suffix)
Array Limits
favorites: No hard limit (but UX degrades after ~50)recents: Enforcemax_recentslimit
Configuration Directory Detection
XDG Base Directory Specification
Linux/macOS:
- Check
$XDG_CONFIG_HOMEenvironment variable - If set: use
$XDG_CONFIG_HOME/fac/ - If not set: use
~/.config/fac/
Fallback:
- If
~/.config/doesn't exist and can't be created: use~/.fac/
Windows (Future):
- Use
%APPDATA%\fac\(e.g.,C:\Users\User\AppData\Roaming\fac\)
Fortran Example:
function get_config_directory() result(path)
character(len=:), allocatable :: path
character(len=1024) :: xdg_config_home, home
integer :: status
! Try XDG_CONFIG_HOME
call get_environment_variable('XDG_CONFIG_HOME', xdg_config_home, status=status)
if (status == 0 .and. len_trim(xdg_config_home) > 0) then
path = trim(xdg_config_home) // '/fac/'
return
end if
! Try ~/.config/fac/
call get_environment_variable('HOME', home, status=status)
if (status == 0) then
path = trim(home) // '/.config/fac/'
return
end if
! Fallback to ~/.fac/
path = trim(home) // '/.fac/'
end function
Migration Strategy
Version 1.0 → 2.0 (Hypothetical)
If schema changes in future:
subroutine load_favorites(favorites)
type(favorites_t), intent(out) :: favorites
character(len=64) :: version
integer :: iostat
! Parse JSON and get version
call parse_json_version(path, version, iostat)
if (version == '1.0') then
call load_favorites_v1(favorites)
else if (version == '2.0') then
call load_favorites_v2(favorites)
else
! Unknown version: try to load as latest, or fail gracefully
call load_favorites_v2(favorites)
end if
end subroutine
subroutine migrate_favorites_v1_to_v2(favorites_v1, favorites_v2)
! Convert old format to new format
end subroutine
Backward Compatibility:
- Keep support for old versions for at least 2 major releases
- Automatically migrate on load, save in new format
Security Considerations
Path Validation
- Verify paths are absolute: Reject relative paths
- Prevent path traversal: No
..components in stored paths - Resolve symlinks: Store canonical paths
- Don't follow untrusted symlinks: Check ownership
File Permissions
- Config directory: 0755 (rwxr-xr-x)
- Config files: 0644 (rw-r--r--)
- Backup files: 0644 (rw-r--r--)
- Respect umask: Use system default permissions
Arbitrary Code Execution
- No eval: Don't execute any code from config files
- JSON only: Only parse JSON, no shell commands
- Sanitize paths: Validate before using in file operations
Performance Considerations
Lazy Loading
- Don't load favorites/recents until needed (Fortress welcome menu)
- Cache in memory during session
- Only write on changes
Update Frequency
- recents.json: Update once per workspace open (not every save)
- favorites.json: Update only when user adds/removes favorite
- Batch updates: If multiple changes, write once
File Size Limits
- favorites.json: Reasonable limit ~1000 entries (~100KB)
- recents.json: Hard limit
max_recents(default 20) - backup-metadata.json: Limit ~100 backups per workspace (~10KB)
Testing Strategy
Test Cases
favorites.json
- Create empty favorites file on first run
- Add favorite, verify saved correctly
- Remove favorite, verify saved correctly
- Load favorites on startup
- Handle corrupted JSON (backup and recreate)
- Handle nonexistent paths (skip with warning)
- Handle duplicate paths (don't add duplicate)
recents.json
- Create empty recents on first run
- Open workspace, verify added to recents
- Open same workspace again, verify
last_openedupdated - Open
max_recents + 1workspaces, verify oldest removed - Load recents on startup
- Handle corrupted JSON (backup and recreate)
- Verify sorting (most recent first)
backup-metadata.json
- Quit with dirty buffer, verify backup created
- Open workspace, detect backup, prompt user
- Restore backup, verify file restored
- Ignore backup, verify metadata cleaned up
- Show diff, verify diff displayed
- Handle missing backup file (skip entry)
- Cleanup empty metadata file
Edge Cases
- Config directory doesn't exist (create it)
- Config directory not writable (read-only mode)
- Disk full during save (handle gracefully)
- Home directory not set (fallback to /tmp?)
- JSON with BOM (byte order mark) - handle or reject
- JSON with comments (non-standard) - reject
Implementation Notes
JSON Parsing
Option 1: Use external library (e.g., json-fortran)
- Pros: Robust, well-tested, full JSON support
- Cons: External dependency
Option 2: Write simple parser for our specific format
- Pros: No dependencies, full control
- Cons: More work, potential bugs
Recommendation: Use json-fortran (fpm package)
Alternative: If no dependencies allowed, write minimal parser for our specific schema (we only need basic key-value parsing, not full JSON)
Timestamp Generation
Fortran 2003+:
use iso_fortran_env, only: int64
function get_iso8601_timestamp() result(timestamp)
character(len=24) :: timestamp
integer :: values(8)
call date_and_time(values=values)
write(timestamp, '(I4.4,"-",I2.2,"-",I2.2,"T",I2.2,":",I2.2,":",I2.2,"Z")') &
values(1), values(2), values(3), values(5), values(6), values(7)
end function
Path Operations
Use existing fac modules:
src/fortress/filesystem/path_utils_module.f90- Path manipulationsrc/fortress/filesystem/directory_module.f90- Directory operations
Or POSIX C bindings:
interface
function c_realpath(path, resolved) bind(C, name="realpath")
use iso_c_binding
type(c_ptr), value :: path
type(c_ptr), value :: resolved
type(c_ptr) :: c_realpath
end function
end interface
Related Documents
WORKSPACE_VISION.md- Overall design visionworkspace_spec.md- workspace.json formatfortress_integration.md- Fortress integration detailsWORKSPACE_ROADMAP.md- Implementation phases
Appendix: JSON Examples
Empty Configuration (First Run)
favorites.json:
{
"version": "1.0",
"favorites": []
}
recents.json:
{
"version": "1.0",
"max_recents": 20,
"recents": []
}
Populated Configuration
favorites.json:
{
"version": "1.0",
"favorites": [
{
"path": "/home/user/projects/facsimile",
"label": "fac Editor",
"added": "2025-01-01T08:00:00Z",
"pinned": true
},
{
"path": "/home/user/projects/fortress",
"label": "Fortress",
"added": "2025-01-02T09:15:00Z",
"pinned": false
}
]
}
recents.json:
{
"version": "1.0",
"max_recents": 20,
"recents": [
{
"path": "/home/user/projects/facsimile",
"label": "facsimile",
"last_opened": "2025-01-05T15:30:00Z",
"open_count": 47
}
]
}
End of Specification
View source
| 1 | # Configuration Files Specification |
| 2 | |
| 3 | **Version**: 1.0 |
| 4 | **Last Updated**: 2025-01-05 |
| 5 | |
| 6 | --- |
| 7 | |
| 8 | ## Overview |
| 9 | |
| 10 | This document specifies the format and location of fac's user configuration files. These files are stored in the user's config directory and track global state across all workspaces. |
| 11 | |
| 12 | **Purpose**: |
| 13 | - Track recently opened workspaces |
| 14 | - Store user-marked favorite workspaces |
| 15 | - Manage backup metadata for dirty buffers |
| 16 | |
| 17 | **Location**: Follows XDG Base Directory Specification |
| 18 | - **Linux/macOS**: `~/.config/fac/` |
| 19 | - **Fallback**: `~/.fac/` if XDG not available |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## Directory Structure |
| 24 | |
| 25 | ``` |
| 26 | ~/.config/fac/ |
| 27 | ├── favorites.json # User-marked favorite workspaces |
| 28 | ├── recents.json # Auto-tracked recent workspaces |
| 29 | └── README.txt # Optional: explain what this directory is |
| 30 | ``` |
| 31 | |
| 32 | --- |
| 33 | |
| 34 | ## favorites.json Specification |
| 35 | |
| 36 | ### Purpose |
| 37 | Store user-manually marked favorite workspaces for quick access via Fortress welcome menu. |
| 38 | |
| 39 | ### Location |
| 40 | `~/.config/fac/favorites.json` |
| 41 | |
| 42 | ### Schema |
| 43 | |
| 44 | ```json |
| 45 | { |
| 46 | "version": "1.0", |
| 47 | "favorites": [ |
| 48 | { |
| 49 | "path": "/absolute/path/to/workspace", |
| 50 | "label": "My Project", |
| 51 | "added": "2025-01-05T10:30:00Z", |
| 52 | "pinned": false |
| 53 | } |
| 54 | ] |
| 55 | } |
| 56 | ``` |
| 57 | |
| 58 | ### Field Descriptions |
| 59 | |
| 60 | #### Root Object |
| 61 | |
| 62 | ##### `version` (string, required) |
| 63 | - Schema version for compatibility |
| 64 | - Format: "major.minor" |
| 65 | - Current: "1.0" |
| 66 | |
| 67 | ##### `favorites` (array of objects, required) |
| 68 | - Array of favorite workspace objects |
| 69 | - Order matters (display order in Fortress) |
| 70 | - Can be empty |
| 71 | |
| 72 | #### Favorite Object |
| 73 | |
| 74 | ##### `path` (string, required) |
| 75 | - Absolute path to workspace root |
| 76 | - Must be a directory (verified on load) |
| 77 | - Example: "/home/user/projects/myapp" |
| 78 | |
| 79 | ##### `label` (string, required) |
| 80 | - User-friendly display name |
| 81 | - Defaults to directory basename |
| 82 | - User can customize |
| 83 | - Example: "My Fortran Project" |
| 84 | |
| 85 | ##### `added` (string, required) |
| 86 | - ISO 8601 timestamp when favorite was added |
| 87 | - Format: "YYYY-MM-DDTHH:MM:SSZ" |
| 88 | - Used for sorting (if needed) |
| 89 | - Example: "2025-01-05T10:30:00Z" |
| 90 | |
| 91 | ##### `pinned` (boolean, required) |
| 92 | - Whether this favorite should stay at top |
| 93 | - Pinned favorites shown first (Phase 7 feature) |
| 94 | - Default: false |
| 95 | |
| 96 | ### Example favorites.json |
| 97 | |
| 98 | ```json |
| 99 | { |
| 100 | "version": "1.0", |
| 101 | "favorites": [ |
| 102 | { |
| 103 | "path": "/home/user/projects/facsimile", |
| 104 | "label": "fac Editor", |
| 105 | "added": "2025-01-01T08:00:00Z", |
| 106 | "pinned": true |
| 107 | }, |
| 108 | { |
| 109 | "path": "/home/user/projects/fortress", |
| 110 | "label": "Fortress Navigator", |
| 111 | "added": "2025-01-02T09:15:00Z", |
| 112 | "pinned": false |
| 113 | }, |
| 114 | { |
| 115 | "path": "/home/user/Documents/thesis", |
| 116 | "label": "PhD Thesis", |
| 117 | "added": "2025-01-03T14:30:00Z", |
| 118 | "pinned": false |
| 119 | } |
| 120 | ] |
| 121 | } |
| 122 | ``` |
| 123 | |
| 124 | ### Operations |
| 125 | |
| 126 | #### Add Favorite |
| 127 | - **Trigger**: Press 'f' in Fortress navigator |
| 128 | - **Action**: Add current directory to favorites list |
| 129 | - **Duplicate Check**: If path already exists, don't add again (show message) |
| 130 | - **Label**: Default to basename, allow edit later (Phase 7) |
| 131 | |
| 132 | #### Remove Favorite |
| 133 | - **Trigger**: Press 'r' or 'x' in Fortress favorites view |
| 134 | - **Action**: Remove selected favorite from list |
| 135 | - **Confirmation**: Prompt "Remove favorite? [y/n]" |
| 136 | |
| 137 | #### Reorder Favorites (Phase 7) |
| 138 | - **Trigger**: Drag/drop or move commands |
| 139 | - **Action**: Change order in array |
| 140 | - **Persistence**: Save immediately |
| 141 | |
| 142 | --- |
| 143 | |
| 144 | ## recents.json Specification |
| 145 | |
| 146 | ### Purpose |
| 147 | Automatically track recently opened workspaces for quick access. Similar to "Recent Files" in most editors. |
| 148 | |
| 149 | ### Location |
| 150 | `~/.config/fac/recents.json` |
| 151 | |
| 152 | ### Schema |
| 153 | |
| 154 | ```json |
| 155 | { |
| 156 | "version": "1.0", |
| 157 | "max_recents": 20, |
| 158 | "recents": [ |
| 159 | { |
| 160 | "path": "/absolute/path/to/workspace", |
| 161 | "label": "Auto-generated label", |
| 162 | "last_opened": "2025-01-05T14:45:00Z", |
| 163 | "open_count": 5 |
| 164 | } |
| 165 | ] |
| 166 | } |
| 167 | ``` |
| 168 | |
| 169 | ### Field Descriptions |
| 170 | |
| 171 | #### Root Object |
| 172 | |
| 173 | ##### `version` (string, required) |
| 174 | - Schema version |
| 175 | - Current: "1.0" |
| 176 | |
| 177 | ##### `max_recents` (integer, required) |
| 178 | - Maximum number of recents to track |
| 179 | - Default: 20 |
| 180 | - When exceeded, oldest entries are removed |
| 181 | - User configurable (Phase 7) |
| 182 | |
| 183 | ##### `recents` (array of objects, required) |
| 184 | - Array of recent workspace objects |
| 185 | - Sorted by `last_opened` (most recent first) |
| 186 | - Can be empty |
| 187 | |
| 188 | #### Recent Object |
| 189 | |
| 190 | ##### `path` (string, required) |
| 191 | - Absolute path to workspace root |
| 192 | - Example: "/home/user/projects/myapp" |
| 193 | |
| 194 | ##### `label` (string, required) |
| 195 | - Display name (typically basename) |
| 196 | - Auto-generated from path |
| 197 | - Example: "myapp" |
| 198 | |
| 199 | ##### `last_opened` (string, required) |
| 200 | - ISO 8601 timestamp of last open |
| 201 | - Format: "YYYY-MM-DDTHH:MM:SSZ" |
| 202 | - Updated every time workspace is opened |
| 203 | - Example: "2025-01-05T14:45:00Z" |
| 204 | |
| 205 | ##### `open_count` (integer, required) |
| 206 | - Number of times this workspace has been opened |
| 207 | - Used for statistics (Phase 7) |
| 208 | - Incremented on each open |
| 209 | - Default: 1 |
| 210 | |
| 211 | ### Example recents.json |
| 212 | |
| 213 | ```json |
| 214 | { |
| 215 | "version": "1.0", |
| 216 | "max_recents": 20, |
| 217 | "recents": [ |
| 218 | { |
| 219 | "path": "/home/user/projects/facsimile", |
| 220 | "label": "facsimile", |
| 221 | "last_opened": "2025-01-05T15:30:00Z", |
| 222 | "open_count": 47 |
| 223 | }, |
| 224 | { |
| 225 | "path": "/home/user/projects/fortress", |
| 226 | "label": "fortress", |
| 227 | "last_opened": "2025-01-05T10:15:00Z", |
| 228 | "open_count": 23 |
| 229 | }, |
| 230 | { |
| 231 | "path": "/tmp/scratch", |
| 232 | "label": "scratch", |
| 233 | "last_opened": "2025-01-04T16:45:00Z", |
| 234 | "open_count": 3 |
| 235 | } |
| 236 | ] |
| 237 | } |
| 238 | ``` |
| 239 | |
| 240 | ### Operations |
| 241 | |
| 242 | #### Add/Update Recent |
| 243 | - **Trigger**: Every time a workspace is opened |
| 244 | - **Action**: |
| 245 | 1. If path exists in recents: update `last_opened`, increment `open_count` |
| 246 | 2. If path doesn't exist: add new entry |
| 247 | 3. Re-sort by `last_opened` (most recent first) |
| 248 | 4. If count > `max_recents`: remove oldest entry |
| 249 | |
| 250 | #### Clear Recents |
| 251 | - **Trigger**: User command (Phase 7) |
| 252 | - **Action**: Empty recents array |
| 253 | - **File**: Keep file, just empty the array |
| 254 | |
| 255 | #### Remove Recent |
| 256 | - **Trigger**: Press 'r' or 'x' in Fortress recents view |
| 257 | - **Action**: Remove selected entry |
| 258 | - **No confirmation**: Recents are auto-tracked, safe to remove |
| 259 | |
| 260 | --- |
| 261 | |
| 262 | ## Backup Metadata Specification |
| 263 | |
| 264 | ### Purpose |
| 265 | Track backup files for dirty buffers when user quits without saving. |
| 266 | |
| 267 | ### Location |
| 268 | `<workspace_root>/.fac/backups/.backup-metadata.json` |
| 269 | |
| 270 | ### Schema |
| 271 | |
| 272 | ```json |
| 273 | { |
| 274 | "version": "1.0", |
| 275 | "backups": [ |
| 276 | { |
| 277 | "original_file": "src/main.f90", |
| 278 | "backup_file": "src/main.f90.bak", |
| 279 | "timestamp": "2025-01-05T10:30:00Z", |
| 280 | "reason": "quit_without_save" |
| 281 | } |
| 282 | ] |
| 283 | } |
| 284 | ``` |
| 285 | |
| 286 | ### Field Descriptions |
| 287 | |
| 288 | #### Root Object |
| 289 | |
| 290 | ##### `version` (string, required) |
| 291 | - Schema version |
| 292 | - Current: "1.0" |
| 293 | |
| 294 | ##### `backups` (array of objects, required) |
| 295 | - Array of backup entries |
| 296 | - Can be empty |
| 297 | - Cleaned up after restore or ignore |
| 298 | |
| 299 | #### Backup Object |
| 300 | |
| 301 | ##### `original_file` (string, required) |
| 302 | - Relative path to original file (within workspace) |
| 303 | - Example: "src/main.f90" |
| 304 | |
| 305 | ##### `backup_file` (string, required) |
| 306 | - Relative path to backup file |
| 307 | - Typically `{original_file}.bak` |
| 308 | - Example: "src/main.f90.bak" |
| 309 | |
| 310 | ##### `timestamp` (string, required) |
| 311 | - ISO 8601 timestamp when backup was created |
| 312 | - Format: "YYYY-MM-DDTHH:MM:SSZ" |
| 313 | - Example: "2025-01-05T10:30:00Z" |
| 314 | |
| 315 | ##### `reason` (string, required) |
| 316 | - Why backup was created |
| 317 | - Values: |
| 318 | - `"quit_without_save"` - User quit with dirty buffer |
| 319 | - `"crash"` - Editor crashed (Phase 7) |
| 320 | - `"switch_workspace"` - Switched workspace with dirty buffer (Phase 6) |
| 321 | |
| 322 | ### Example .backup-metadata.json |
| 323 | |
| 324 | ```json |
| 325 | { |
| 326 | "version": "1.0", |
| 327 | "backups": [ |
| 328 | { |
| 329 | "original_file": "src/main.f90", |
| 330 | "backup_file": "src/main.f90.bak", |
| 331 | "timestamp": "2025-01-05T10:30:00Z", |
| 332 | "reason": "quit_without_save" |
| 333 | }, |
| 334 | { |
| 335 | "original_file": "tests/test.f90", |
| 336 | "backup_file": "tests/test.f90.bak", |
| 337 | "timestamp": "2025-01-05T10:30:00Z", |
| 338 | "reason": "quit_without_save" |
| 339 | } |
| 340 | ] |
| 341 | } |
| 342 | ``` |
| 343 | |
| 344 | ### Operations |
| 345 | |
| 346 | #### Create Backup |
| 347 | - **Trigger**: Quit with dirty buffers, user chooses 'n' (don't save) |
| 348 | - **Action**: |
| 349 | 1. For each dirty buffer, copy to `{filename}.bak` |
| 350 | 2. Add entry to `.backup-metadata.json` |
| 351 | 3. Write metadata atomically |
| 352 | |
| 353 | #### Detect Backups |
| 354 | - **Trigger**: Open workspace |
| 355 | - **Action**: |
| 356 | 1. Check if `.fac/backups/.backup-metadata.json` exists |
| 357 | 2. If exists and has entries, prompt user |
| 358 | |
| 359 | #### Restore Backup |
| 360 | - **Trigger**: User selects 'r' (restore) in backup prompt |
| 361 | - **Action**: |
| 362 | 1. Copy backup file to original location (overwrite) |
| 363 | 2. Remove backup file |
| 364 | 3. Remove entry from metadata |
| 365 | 4. Load file in editor |
| 366 | |
| 367 | #### Ignore Backup |
| 368 | - **Trigger**: User selects 'i' (ignore) in backup prompt |
| 369 | - **Action**: |
| 370 | 1. Keep backup file (don't delete) |
| 371 | 2. Remove entry from metadata |
| 372 | 3. Load original file in editor |
| 373 | |
| 374 | #### Show Diff |
| 375 | - **Trigger**: User selects 'd' (diff) in backup prompt |
| 376 | - **Action**: |
| 377 | 1. Run `diff` command (or internal diff) |
| 378 | 2. Display in pager or split view |
| 379 | 3. Return to prompt (user can then choose r/i) |
| 380 | |
| 381 | #### Cleanup |
| 382 | - **Trigger**: After restore or ignore all backups |
| 383 | - **Action**: |
| 384 | 1. If metadata.backups array is empty, delete `.backup-metadata.json` |
| 385 | 2. If `.fac/backups/` directory is empty, delete directory |
| 386 | |
| 387 | --- |
| 388 | |
| 389 | ## File Operations |
| 390 | |
| 391 | ### Creating Config Directory |
| 392 | |
| 393 | **On first run**: |
| 394 | 1. Check if `~/.config/fac/` exists |
| 395 | 2. If not, create directory with 0755 permissions |
| 396 | 3. Create empty `favorites.json` and `recents.json` |
| 397 | 4. Optionally create `README.txt` explaining purpose |
| 398 | |
| 399 | **Fortran Example**: |
| 400 | ```fortran |
| 401 | subroutine ensure_config_directory() |
| 402 | character(len=:), allocatable :: config_path |
| 403 | logical :: exists |
| 404 | |
| 405 | config_path = get_config_directory() ! Returns ~/.config/fac/ |
| 406 | inquire(file=config_path, exist=exists) |
| 407 | |
| 408 | if (.not. exists) then |
| 409 | call create_directory(config_path) |
| 410 | call create_empty_config_files(config_path) |
| 411 | end if |
| 412 | end subroutine |
| 413 | ``` |
| 414 | |
| 415 | ### Atomic Writes |
| 416 | |
| 417 | **Critical**: Always use atomic writes for config files to avoid corruption. |
| 418 | |
| 419 | **Pattern**: |
| 420 | 1. Write to temporary file: `favorites.json.tmp` |
| 421 | 2. Verify write succeeded (check iostat) |
| 422 | 3. Rename temp to final: `rename(tmp, final)` |
| 423 | 4. Rename is atomic on POSIX systems |
| 424 | |
| 425 | **Fortran Example**: |
| 426 | ```fortran |
| 427 | subroutine save_favorites(favorites) |
| 428 | type(favorites_t), intent(in) :: favorites |
| 429 | character(len=:), allocatable :: path, tmp_path |
| 430 | integer :: iostat |
| 431 | |
| 432 | path = get_config_directory() // 'favorites.json' |
| 433 | tmp_path = path // '.tmp' |
| 434 | |
| 435 | ! Write to temp file |
| 436 | call write_json_to_file(tmp_path, favorites, iostat) |
| 437 | |
| 438 | if (iostat == 0) then |
| 439 | ! Rename temp to final (atomic) |
| 440 | call rename_file(tmp_path, path) |
| 441 | else |
| 442 | ! Handle error |
| 443 | call delete_file(tmp_path) |
| 444 | end if |
| 445 | end subroutine |
| 446 | ``` |
| 447 | |
| 448 | ### Reading Config Files |
| 449 | |
| 450 | **Strategy**: Graceful fallback on errors |
| 451 | |
| 452 | ```fortran |
| 453 | subroutine load_favorites(favorites) |
| 454 | type(favorites_t), intent(out) :: favorites |
| 455 | character(len=:), allocatable :: path |
| 456 | logical :: exists |
| 457 | integer :: iostat |
| 458 | |
| 459 | path = get_config_directory() // 'favorites.json' |
| 460 | inquire(file=path, exist=exists) |
| 461 | |
| 462 | if (.not. exists) then |
| 463 | ! Create empty favorites |
| 464 | call initialize_empty_favorites(favorites) |
| 465 | call save_favorites(favorites) |
| 466 | return |
| 467 | end if |
| 468 | |
| 469 | call parse_json_file(path, favorites, iostat) |
| 470 | |
| 471 | if (iostat /= 0) then |
| 472 | ! Corrupted file: backup and recreate |
| 473 | call backup_corrupt_file(path) |
| 474 | call initialize_empty_favorites(favorites) |
| 475 | call save_favorites(favorites) |
| 476 | end if |
| 477 | end subroutine |
| 478 | ``` |
| 479 | |
| 480 | --- |
| 481 | |
| 482 | ## Error Handling |
| 483 | |
| 484 | ### File Not Found |
| 485 | - **Action**: Create with default/empty content |
| 486 | - **Log**: No error message (expected on first run) |
| 487 | |
| 488 | ### Permission Denied |
| 489 | - **Action**: Warn user, run in read-only mode |
| 490 | - **Message**: "Cannot write to config directory. Favorites/recents will not persist." |
| 491 | |
| 492 | ### Corrupted JSON |
| 493 | - **Action**: Backup corrupt file, create new empty config |
| 494 | - **Backup**: Rename to `{file}.corrupted.{timestamp}` |
| 495 | - **Log**: Warning message with backup location |
| 496 | |
| 497 | ### Disk Full |
| 498 | - **Action**: Warn user, skip update |
| 499 | - **Message**: "Cannot save config: disk full" |
| 500 | |
| 501 | ### Invalid Data |
| 502 | - **Action**: Skip invalid entries, log warning |
| 503 | - **Example**: Path doesn't exist → skip that favorite |
| 504 | - **Message**: "Skipped invalid favorite: /nonexistent/path" |
| 505 | |
| 506 | --- |
| 507 | |
| 508 | ## Validation Rules |
| 509 | |
| 510 | ### On Load |
| 511 | |
| 512 | #### favorites.json |
| 513 | 1. Verify `version` is supported |
| 514 | 2. For each favorite: |
| 515 | - Verify `path` is absolute |
| 516 | - Verify `path` exists (if not, skip with warning) |
| 517 | - Verify `added` is valid ISO 8601 |
| 518 | - Clamp `pinned` to boolean |
| 519 | |
| 520 | #### recents.json |
| 521 | 1. Verify `version` is supported |
| 522 | 2. Verify `max_recents` is positive integer (default 20 if invalid) |
| 523 | 3. For each recent: |
| 524 | - Verify `path` is absolute |
| 525 | - If `path` doesn't exist, remove entry (cleaned up automatically) |
| 526 | - Verify `last_opened` is valid ISO 8601 |
| 527 | - Verify `open_count` is non-negative integer |
| 528 | |
| 529 | #### .backup-metadata.json |
| 530 | 1. Verify `version` is supported |
| 531 | 2. For each backup: |
| 532 | - Verify `backup_file` exists (if not, skip entry) |
| 533 | - Verify `timestamp` is valid ISO 8601 |
| 534 | - Verify `reason` is valid enum value |
| 535 | |
| 536 | ### On Save |
| 537 | |
| 538 | #### Path Normalization |
| 539 | - Always store absolute paths (no relative paths) |
| 540 | - Resolve symlinks before storing |
| 541 | - Remove trailing slashes |
| 542 | |
| 543 | #### Timestamp Format |
| 544 | - Always use ISO 8601: "YYYY-MM-DDTHH:MM:SSZ" |
| 545 | - Always use UTC timezone (Z suffix) |
| 546 | |
| 547 | #### Array Limits |
| 548 | - `favorites`: No hard limit (but UX degrades after ~50) |
| 549 | - `recents`: Enforce `max_recents` limit |
| 550 | |
| 551 | --- |
| 552 | |
| 553 | ## Configuration Directory Detection |
| 554 | |
| 555 | ### XDG Base Directory Specification |
| 556 | |
| 557 | **Linux/macOS**: |
| 558 | 1. Check `$XDG_CONFIG_HOME` environment variable |
| 559 | 2. If set: use `$XDG_CONFIG_HOME/fac/` |
| 560 | 3. If not set: use `~/.config/fac/` |
| 561 | |
| 562 | **Fallback**: |
| 563 | - If `~/.config/` doesn't exist and can't be created: use `~/.fac/` |
| 564 | |
| 565 | **Windows (Future)**: |
| 566 | - Use `%APPDATA%\fac\` (e.g., `C:\Users\User\AppData\Roaming\fac\`) |
| 567 | |
| 568 | **Fortran Example**: |
| 569 | ```fortran |
| 570 | function get_config_directory() result(path) |
| 571 | character(len=:), allocatable :: path |
| 572 | character(len=1024) :: xdg_config_home, home |
| 573 | integer :: status |
| 574 | |
| 575 | ! Try XDG_CONFIG_HOME |
| 576 | call get_environment_variable('XDG_CONFIG_HOME', xdg_config_home, status=status) |
| 577 | if (status == 0 .and. len_trim(xdg_config_home) > 0) then |
| 578 | path = trim(xdg_config_home) // '/fac/' |
| 579 | return |
| 580 | end if |
| 581 | |
| 582 | ! Try ~/.config/fac/ |
| 583 | call get_environment_variable('HOME', home, status=status) |
| 584 | if (status == 0) then |
| 585 | path = trim(home) // '/.config/fac/' |
| 586 | return |
| 587 | end if |
| 588 | |
| 589 | ! Fallback to ~/.fac/ |
| 590 | path = trim(home) // '/.fac/' |
| 591 | end function |
| 592 | ``` |
| 593 | |
| 594 | --- |
| 595 | |
| 596 | ## Migration Strategy |
| 597 | |
| 598 | ### Version 1.0 → 2.0 (Hypothetical) |
| 599 | |
| 600 | If schema changes in future: |
| 601 | |
| 602 | ```fortran |
| 603 | subroutine load_favorites(favorites) |
| 604 | type(favorites_t), intent(out) :: favorites |
| 605 | character(len=64) :: version |
| 606 | integer :: iostat |
| 607 | |
| 608 | ! Parse JSON and get version |
| 609 | call parse_json_version(path, version, iostat) |
| 610 | |
| 611 | if (version == '1.0') then |
| 612 | call load_favorites_v1(favorites) |
| 613 | else if (version == '2.0') then |
| 614 | call load_favorites_v2(favorites) |
| 615 | else |
| 616 | ! Unknown version: try to load as latest, or fail gracefully |
| 617 | call load_favorites_v2(favorites) |
| 618 | end if |
| 619 | end subroutine |
| 620 | |
| 621 | subroutine migrate_favorites_v1_to_v2(favorites_v1, favorites_v2) |
| 622 | ! Convert old format to new format |
| 623 | end subroutine |
| 624 | ``` |
| 625 | |
| 626 | **Backward Compatibility**: |
| 627 | - Keep support for old versions for at least 2 major releases |
| 628 | - Automatically migrate on load, save in new format |
| 629 | |
| 630 | --- |
| 631 | |
| 632 | ## Security Considerations |
| 633 | |
| 634 | ### Path Validation |
| 635 | - **Verify paths are absolute**: Reject relative paths |
| 636 | - **Prevent path traversal**: No `..` components in stored paths |
| 637 | - **Resolve symlinks**: Store canonical paths |
| 638 | - **Don't follow untrusted symlinks**: Check ownership |
| 639 | |
| 640 | ### File Permissions |
| 641 | - **Config directory**: 0755 (rwxr-xr-x) |
| 642 | - **Config files**: 0644 (rw-r--r--) |
| 643 | - **Backup files**: 0644 (rw-r--r--) |
| 644 | - **Respect umask**: Use system default permissions |
| 645 | |
| 646 | ### Arbitrary Code Execution |
| 647 | - **No eval**: Don't execute any code from config files |
| 648 | - **JSON only**: Only parse JSON, no shell commands |
| 649 | - **Sanitize paths**: Validate before using in file operations |
| 650 | |
| 651 | --- |
| 652 | |
| 653 | ## Performance Considerations |
| 654 | |
| 655 | ### Lazy Loading |
| 656 | - Don't load favorites/recents until needed (Fortress welcome menu) |
| 657 | - Cache in memory during session |
| 658 | - Only write on changes |
| 659 | |
| 660 | ### Update Frequency |
| 661 | - **recents.json**: Update once per workspace open (not every save) |
| 662 | - **favorites.json**: Update only when user adds/removes favorite |
| 663 | - **Batch updates**: If multiple changes, write once |
| 664 | |
| 665 | ### File Size Limits |
| 666 | - **favorites.json**: Reasonable limit ~1000 entries (~100KB) |
| 667 | - **recents.json**: Hard limit `max_recents` (default 20) |
| 668 | - **backup-metadata.json**: Limit ~100 backups per workspace (~10KB) |
| 669 | |
| 670 | --- |
| 671 | |
| 672 | ## Testing Strategy |
| 673 | |
| 674 | ### Test Cases |
| 675 | |
| 676 | #### favorites.json |
| 677 | 1. Create empty favorites file on first run |
| 678 | 2. Add favorite, verify saved correctly |
| 679 | 3. Remove favorite, verify saved correctly |
| 680 | 4. Load favorites on startup |
| 681 | 5. Handle corrupted JSON (backup and recreate) |
| 682 | 6. Handle nonexistent paths (skip with warning) |
| 683 | 7. Handle duplicate paths (don't add duplicate) |
| 684 | |
| 685 | #### recents.json |
| 686 | 1. Create empty recents on first run |
| 687 | 2. Open workspace, verify added to recents |
| 688 | 3. Open same workspace again, verify `last_opened` updated |
| 689 | 4. Open `max_recents + 1` workspaces, verify oldest removed |
| 690 | 5. Load recents on startup |
| 691 | 6. Handle corrupted JSON (backup and recreate) |
| 692 | 7. Verify sorting (most recent first) |
| 693 | |
| 694 | #### backup-metadata.json |
| 695 | 1. Quit with dirty buffer, verify backup created |
| 696 | 2. Open workspace, detect backup, prompt user |
| 697 | 3. Restore backup, verify file restored |
| 698 | 4. Ignore backup, verify metadata cleaned up |
| 699 | 5. Show diff, verify diff displayed |
| 700 | 6. Handle missing backup file (skip entry) |
| 701 | 7. Cleanup empty metadata file |
| 702 | |
| 703 | ### Edge Cases |
| 704 | - Config directory doesn't exist (create it) |
| 705 | - Config directory not writable (read-only mode) |
| 706 | - Disk full during save (handle gracefully) |
| 707 | - Home directory not set (fallback to /tmp?) |
| 708 | - JSON with BOM (byte order mark) - handle or reject |
| 709 | - JSON with comments (non-standard) - reject |
| 710 | |
| 711 | --- |
| 712 | |
| 713 | ## Implementation Notes |
| 714 | |
| 715 | ### JSON Parsing |
| 716 | |
| 717 | **Option 1**: Use external library (e.g., json-fortran) |
| 718 | - **Pros**: Robust, well-tested, full JSON support |
| 719 | - **Cons**: External dependency |
| 720 | |
| 721 | **Option 2**: Write simple parser for our specific format |
| 722 | - **Pros**: No dependencies, full control |
| 723 | - **Cons**: More work, potential bugs |
| 724 | |
| 725 | **Recommendation**: Use json-fortran (fpm package) |
| 726 | |
| 727 | **Alternative**: If no dependencies allowed, write minimal parser for our specific schema (we only need basic key-value parsing, not full JSON) |
| 728 | |
| 729 | ### Timestamp Generation |
| 730 | |
| 731 | **Fortran 2003+**: |
| 732 | ```fortran |
| 733 | use iso_fortran_env, only: int64 |
| 734 | |
| 735 | function get_iso8601_timestamp() result(timestamp) |
| 736 | character(len=24) :: timestamp |
| 737 | integer :: values(8) |
| 738 | |
| 739 | call date_and_time(values=values) |
| 740 | |
| 741 | write(timestamp, '(I4.4,"-",I2.2,"-",I2.2,"T",I2.2,":",I2.2,":",I2.2,"Z")') & |
| 742 | values(1), values(2), values(3), values(5), values(6), values(7) |
| 743 | end function |
| 744 | ``` |
| 745 | |
| 746 | ### Path Operations |
| 747 | |
| 748 | Use existing fac modules: |
| 749 | - `src/fortress/filesystem/path_utils_module.f90` - Path manipulation |
| 750 | - `src/fortress/filesystem/directory_module.f90` - Directory operations |
| 751 | |
| 752 | Or POSIX C bindings: |
| 753 | ```fortran |
| 754 | interface |
| 755 | function c_realpath(path, resolved) bind(C, name="realpath") |
| 756 | use iso_c_binding |
| 757 | type(c_ptr), value :: path |
| 758 | type(c_ptr), value :: resolved |
| 759 | type(c_ptr) :: c_realpath |
| 760 | end function |
| 761 | end interface |
| 762 | ``` |
| 763 | |
| 764 | --- |
| 765 | |
| 766 | ## Related Documents |
| 767 | |
| 768 | - `WORKSPACE_VISION.md` - Overall design vision |
| 769 | - `workspace_spec.md` - workspace.json format |
| 770 | - `fortress_integration.md` - Fortress integration details |
| 771 | - `WORKSPACE_ROADMAP.md` - Implementation phases |
| 772 | |
| 773 | --- |
| 774 | |
| 775 | ## Appendix: JSON Examples |
| 776 | |
| 777 | ### Empty Configuration (First Run) |
| 778 | |
| 779 | **favorites.json**: |
| 780 | ```json |
| 781 | { |
| 782 | "version": "1.0", |
| 783 | "favorites": [] |
| 784 | } |
| 785 | ``` |
| 786 | |
| 787 | **recents.json**: |
| 788 | ```json |
| 789 | { |
| 790 | "version": "1.0", |
| 791 | "max_recents": 20, |
| 792 | "recents": [] |
| 793 | } |
| 794 | ``` |
| 795 | |
| 796 | ### Populated Configuration |
| 797 | |
| 798 | **favorites.json**: |
| 799 | ```json |
| 800 | { |
| 801 | "version": "1.0", |
| 802 | "favorites": [ |
| 803 | { |
| 804 | "path": "/home/user/projects/facsimile", |
| 805 | "label": "fac Editor", |
| 806 | "added": "2025-01-01T08:00:00Z", |
| 807 | "pinned": true |
| 808 | }, |
| 809 | { |
| 810 | "path": "/home/user/projects/fortress", |
| 811 | "label": "Fortress", |
| 812 | "added": "2025-01-02T09:15:00Z", |
| 813 | "pinned": false |
| 814 | } |
| 815 | ] |
| 816 | } |
| 817 | ``` |
| 818 | |
| 819 | **recents.json**: |
| 820 | ```json |
| 821 | { |
| 822 | "version": "1.0", |
| 823 | "max_recents": 20, |
| 824 | "recents": [ |
| 825 | { |
| 826 | "path": "/home/user/projects/facsimile", |
| 827 | "label": "facsimile", |
| 828 | "last_opened": "2025-01-05T15:30:00Z", |
| 829 | "open_count": 47 |
| 830 | } |
| 831 | ] |
| 832 | } |
| 833 | ``` |
| 834 | |
| 835 | --- |
| 836 | |
| 837 | **End of Specification** |