Fortran · 9591 bytes Raw Blame History
1 ! Favorites management module
2 ! Handles favorites.json read/write and favorite workspace management
3
4 module favorites_module
5 use iso_fortran_env, only: int32
6 use config_module, only: get_config_dir, ensure_config_dir
7 implicit none
8 private
9
10 public :: favorite_t, favorites_load, favorites_save, favorites_add, favorites_remove
11 public :: favorites_exists, favorites_get_path
12
13 integer, parameter :: MAX_PATH_LEN = 512
14 integer, parameter :: MAX_LABEL_LEN = 128
15 integer, parameter :: MAX_FAVORITES = 50
16
17 type :: favorite_t
18 character(len=MAX_PATH_LEN) :: path = ""
19 character(len=MAX_LABEL_LEN) :: label = ""
20 character(len=32) :: added = "" ! ISO 8601 timestamp
21 logical :: pinned = .false.
22 end type favorite_t
23
24 contains
25
26 !> Get path to favorites.json
27 subroutine favorites_get_path(favorites_file)
28 character(len=:), allocatable, intent(out) :: favorites_file
29 character(len=:), allocatable :: config_dir
30
31 call get_config_dir(config_dir)
32 favorites_file = trim(config_dir) // '/favorites.json'
33 end subroutine favorites_get_path
34
35 !> Check if favorites.json exists
36 function favorites_exists() result(exists)
37 logical :: exists
38 character(len=:), allocatable :: favorites_file
39 integer :: unit, ios
40
41 call favorites_get_path(favorites_file)
42 open(newunit=unit, file=favorites_file, status='old', iostat=ios)
43 exists = (ios == 0)
44 if (exists) close(unit)
45 end function favorites_exists
46
47 !> Load favorites from favorites.json
48 subroutine favorites_load(favorites, count, success)
49 type(favorite_t), allocatable, intent(out) :: favorites(:)
50 integer, intent(out) :: count
51 logical, intent(out) :: success
52 character(len=:), allocatable :: favorites_file
53 character(len=1024) :: line
54 integer :: unit, ios, i
55 logical :: in_favorites, reading_entry
56
57 success = .false.
58 count = 0
59 allocate(favorites(MAX_FAVORITES))
60
61 ! Ensure config directory exists
62 call ensure_config_dir(success)
63 if (.not. success) return
64
65 call favorites_get_path(favorites_file)
66
67 ! Open file
68 open(newunit=unit, file=favorites_file, status='old', iostat=ios)
69 if (ios /= 0) then
70 ! File doesn't exist - return empty list
71 success = .true.
72 count = 0
73 return
74 end if
75
76 ! Simple JSON parsing (look for "path", "label", "added", "pinned" fields)
77 in_favorites = .false.
78 reading_entry = .false.
79 i = 0
80
81 do
82 read(unit, '(A)', iostat=ios) line
83 if (ios /= 0) exit
84
85 ! Trim whitespace
86 line = adjustl(line)
87
88 ! Check if we're in the favorites array
89 if (index(line, '"favorites"') > 0) then
90 in_favorites = .true.
91 cycle
92 end if
93
94 if (.not. in_favorites) cycle
95
96 ! Look for start of entry
97 if (index(line, '{') > 0 .and. .not. reading_entry) then
98 reading_entry = .true.
99 i = i + 1
100 if (i > MAX_FAVORITES) exit
101 ! Initialize entry
102 favorites(i)%path = ""
103 favorites(i)%label = ""
104 favorites(i)%added = ""
105 favorites(i)%pinned = .false.
106 cycle
107 end if
108
109 if (reading_entry) then
110 ! Parse fields
111 if (index(line, '"path"') > 0) then
112 call extract_json_string(line, favorites(i)%path)
113 else if (index(line, '"label"') > 0) then
114 call extract_json_string(line, favorites(i)%label)
115 else if (index(line, '"added"') > 0) then
116 call extract_json_string(line, favorites(i)%added)
117 else if (index(line, '"pinned"') > 0) then
118 favorites(i)%pinned = (index(line, 'true') > 0)
119 end if
120
121 ! Check for end of entry
122 if (index(line, '}') > 0) then
123 reading_entry = .false.
124 end if
125 end if
126 end do
127
128 close(unit)
129 count = i
130 success = .true.
131 end subroutine favorites_load
132
133 !> Save favorites to favorites.json
134 subroutine favorites_save(favorites, count, success)
135 type(favorite_t), intent(in) :: favorites(:)
136 integer, intent(in) :: count
137 logical, intent(out) :: success
138 character(len=:), allocatable :: favorites_file
139 integer :: unit, ios, i
140
141 success = .false.
142
143 ! Ensure config directory exists
144 call ensure_config_dir(success)
145 if (.not. success) return
146
147 call favorites_get_path(favorites_file)
148
149 ! Write JSON file
150 open(newunit=unit, file=favorites_file, status='replace', iostat=ios)
151 if (ios /= 0) return
152
153 write(unit, '(A)') '{'
154 write(unit, '(A)') ' "version": "1.0",'
155 write(unit, '(A)') ' "favorites": ['
156
157 do i = 1, count
158 write(unit, '(A)') ' {'
159 write(unit, '(A)') ' "path": "' // trim(favorites(i)%path) // '",'
160 write(unit, '(A)') ' "label": "' // trim(favorites(i)%label) // '",'
161 write(unit, '(A)') ' "added": "' // trim(favorites(i)%added) // '",'
162 if (favorites(i)%pinned) then
163 write(unit, '(A)') ' "pinned": true'
164 else
165 write(unit, '(A)') ' "pinned": false'
166 end if
167
168 if (i < count) then
169 write(unit, '(A)') ' },'
170 else
171 write(unit, '(A)') ' }'
172 end if
173 end do
174
175 write(unit, '(A)') ' ]'
176 write(unit, '(A)') '}'
177
178 close(unit)
179 success = .true.
180 end subroutine favorites_save
181
182 !> Add a favorite workspace
183 subroutine favorites_add(path, label, success)
184 character(len=*), intent(in) :: path
185 character(len=*), intent(in) :: label
186 logical, intent(out) :: success
187 type(favorite_t), allocatable :: favorites(:)
188 integer :: count, i
189 character(len=32) :: timestamp
190
191 success = .false.
192
193 ! Load existing favorites
194 call favorites_load(favorites, count, success)
195 if (.not. success) return
196
197 ! Check if path already exists
198 do i = 1, count
199 if (trim(favorites(i)%path) == trim(path)) then
200 ! Already exists - don't add duplicate
201 success = .true.
202 return
203 end if
204 end do
205
206 ! Check if we've reached max favorites
207 if (count >= MAX_FAVORITES) then
208 success = .false.
209 return
210 end if
211
212 ! Add new favorite
213 count = count + 1
214 favorites(count)%path = trim(path)
215 favorites(count)%label = trim(label)
216 call get_current_timestamp(timestamp)
217 favorites(count)%added = trim(timestamp)
218 favorites(count)%pinned = .false.
219
220 ! Save
221 call favorites_save(favorites, count, success)
222 end subroutine favorites_add
223
224 !> Remove a favorite by index
225 subroutine favorites_remove(index, success)
226 integer, intent(in) :: index
227 logical, intent(out) :: success
228 type(favorite_t), allocatable :: favorites(:), temp(:)
229 integer :: count, i, j
230
231 success = .false.
232
233 ! Load existing favorites
234 call favorites_load(favorites, count, success)
235 if (.not. success) return
236
237 ! Check valid index
238 if (index < 1 .or. index > count) then
239 success = .false.
240 return
241 end if
242
243 ! Create new array without the removed entry
244 allocate(temp(MAX_FAVORITES))
245 j = 0
246 do i = 1, count
247 if (i /= index) then
248 j = j + 1
249 temp(j) = favorites(i)
250 end if
251 end do
252
253 ! Save
254 call favorites_save(temp, j, success)
255 deallocate(temp)
256 end subroutine favorites_remove
257
258 !> Extract string value from JSON line (simple parser)
259 subroutine extract_json_string(line, value)
260 character(len=*), intent(in) :: line
261 character(len=*), intent(out) :: value
262 integer :: start_quote, end_quote, colon_pos
263
264 value = ""
265
266 ! Find colon
267 colon_pos = index(line, ':')
268 if (colon_pos == 0) return
269
270 ! Find first quote after colon
271 start_quote = index(line(colon_pos:), '"')
272 if (start_quote == 0) return
273 start_quote = start_quote + colon_pos
274
275 ! Find second quote
276 end_quote = index(line(start_quote+1:), '"')
277 if (end_quote == 0) return
278 end_quote = end_quote + start_quote
279
280 ! Extract string
281 if (end_quote > start_quote) then
282 value = line(start_quote+1:end_quote-1)
283 end if
284 end subroutine extract_json_string
285
286 !> Get current timestamp in ISO 8601 format
287 subroutine get_current_timestamp(timestamp)
288 character(len=*), intent(out) :: timestamp
289 integer :: values(8)
290
291 call date_and_time(values=values)
292
293 ! Format: YYYY-MM-DDTHH:MM:SSZ
294 write(timestamp, '(I4.4,A,I2.2,A,I2.2,A,I2.2,A,I2.2,A,I2.2,A)') &
295 values(1), '-', values(2), '-', values(3), 'T', &
296 values(5), ':', values(6), ':', values(7), 'Z'
297 end subroutine get_current_timestamp
298
299 end module favorites_module
300