Skip to content

Commit 097ba02

Browse files
committed
feat(batch): add incremental progress reporting for downloads
Wire download progress callbacks through TaskLocal to update BatchProgressView in real-time during icon/image exports. Add log message queue to serialize warning/error output and prevent race conditions when multiple configs report issues simultaneously.
1 parent 278ae08 commit 097ba02

14 files changed

Lines changed: 219 additions & 42 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![CI](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml)
66
[![Release](https://github.com/alexey1312/ExFig/actions/workflows/release.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/release.yml)
77
[![Docs](https://github.com/alexey1312/ExFig/actions/workflows/deploy-docc.yml/badge.svg)](https://alexey1312.github.io/ExFig/documentation/exfig)
8-
![Coverage](https://img.shields.io/badge/coverage-45.93%25-yellow)
8+
![Coverage](https://img.shields.io/badge/coverage-45.61%25-yellow)
99
[![License](https://img.shields.io/github/license/alexey1312/ExFig.svg)](LICENSE)
1010

1111
Command-line utility to export colors, typography, icons, and images from Figma to Xcode, Android Studio, Flutter, and

Sources/ExFig/Batch/BatchConfigRunner.swift

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// swiftlint:disable file_length
12
import FigmaAPI
23
import Foundation
34

@@ -68,13 +69,16 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
6869
var allFileVersions: [String: FileVersionInfo] = [:]
6970

7071
// Only run exports for configured asset types
72+
// Each export is wrapped with asset type context for progress reporting
7173
if hasColorsConfig(params) {
7274
let colors = makeColors(
7375
options: options,
7476
cacheOptions: cacheOptions,
7577
faultToleranceOptions: faultToleranceOptions
7678
)
77-
colorsCount = try await colors.performExport(client: client, ui: ui)
79+
colorsCount = try await BatchProgressViewStorage.$currentAssetType.withValue(.colors) {
80+
try await colors.performExport(client: client, ui: ui)
81+
}
7882
}
7983

8084
if hasIconsConfig(params) {
@@ -83,7 +87,9 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
8387
cacheOptions: cacheOptions,
8488
faultToleranceOptions: heavyFaultToleranceOptions
8589
)
86-
let result = try await icons.performExportWithResult(client: client, ui: ui)
90+
let result = try await BatchProgressViewStorage.$currentAssetType.withValue(.icons) {
91+
try await icons.performExportWithResult(client: client, ui: ui)
92+
}
8793
iconsCount = result.count
8894
allComputedHashes = mergeHashes(allComputedHashes, result.computedHashes)
8995
allGranularStats = GranularCacheStats.merge(allGranularStats, result.granularCacheStats)
@@ -101,7 +107,9 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
101107
cacheOptions: cacheOptions,
102108
faultToleranceOptions: heavyFaultToleranceOptions
103109
)
104-
let result = try await images.performExportWithResult(client: client, ui: ui)
110+
let result = try await BatchProgressViewStorage.$currentAssetType.withValue(.images) {
111+
try await images.performExportWithResult(client: client, ui: ui)
112+
}
105113
imagesCount = result.count
106114
allComputedHashes = mergeHashes(allComputedHashes, result.computedHashes)
107115
allGranularStats = GranularCacheStats.merge(allGranularStats, result.granularCacheStats)
@@ -119,7 +127,9 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
119127
cacheOptions: cacheOptions,
120128
faultToleranceOptions: faultToleranceOptions
121129
)
122-
typographyCount = try await typography.performExport(client: client, ui: ui)
130+
typographyCount = try await BatchProgressViewStorage.$currentAssetType.withValue(.typography) {
131+
try await typography.performExport(client: client, ui: ui)
132+
}
123133
}
124134

125135
return ExportStats(
@@ -264,6 +274,7 @@ struct BatchConfigRunner: Sendable {
264274
)
265275
}
266276

277+
// swiftlint:disable:next function_body_length
267278
func process(
268279
configFile: ConfigFile,
269280
ui: TerminalUI,
@@ -303,12 +314,34 @@ struct BatchConfigRunner: Sendable {
303314
}
304315
)
305316

306-
let stats = try await exporter.export(
307-
configFile: configFile,
308-
options: options,
309-
client: client,
310-
ui: ui
311-
)
317+
// Create progress callback that updates BatchProgressView
318+
let configName = configFile.name
319+
let callback: BatchProgressViewStorage.DownloadProgressCallback = { current, total in
320+
guard let progressView else { return }
321+
// Route to correct asset type based on current context
322+
if let assetType = BatchProgressViewStorage.currentAssetType {
323+
switch assetType {
324+
case .icons:
325+
await progressView.updateProgress(name: configName, icons: (current, total))
326+
case .images:
327+
await progressView.updateProgress(name: configName, images: (current, total))
328+
case .colors:
329+
await progressView.updateProgress(name: configName, colors: (current, total))
330+
case .typography:
331+
await progressView.updateProgress(name: configName, typography: (current, total))
332+
}
333+
}
334+
}
335+
336+
// Inject progress callback so export files can report incremental progress
337+
let stats = try await BatchProgressViewStorage.$downloadProgressCallback.withValue(callback) {
338+
try await exporter.export(
339+
configFile: configFile,
340+
options: options,
341+
client: client,
342+
ui: ui
343+
)
344+
}
312345

313346
// Update progress view with final counts or log success
314347
if let progressView {

Sources/ExFig/Loaders/DownloadImageLoader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class DownloadImageLoader: @unchecked Sendable {
3333
!component.name.trimmingCharacters(in: .whitespaces).isEmpty
3434
}
3535

36-
logger.info("Fetching vector images...")
36+
logger.info("Fetching \(imagesDict.count) images from '\(frameName)'...")
3737
let imageIdToImagePath = try await loadImages(
3838
fileId: fileId,
3939
imagesDict: imagesDict,
@@ -79,7 +79,7 @@ final class DownloadImageLoader: @unchecked Sendable {
7979
throw ExFigError.componentsNotFound
8080
}
8181

82-
logger.info("Fetching raster images at \(scale)x...")
82+
logger.info("Fetching \(imagesDict.count) images from '\(frameName)' at \(scale)x...")
8383
let params = FormatParams(scale: scale, format: format)
8484
let imageIdToImagePath = try await loadImages(
8585
fileId: fileId,

Sources/ExFig/Loaders/ImageLoaderBase.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class ImageLoaderBase: @unchecked Sendable {
252252
return true
253253
}
254254

255-
logger.info("Fetching vector images...")
255+
logger.info("Fetching \(imagesDict.count) images from '\(frameName)'...")
256256
let imageIdToImagePath = try await loadImages(
257257
fileId: fileId,
258258
imagesDict: imagesDict,
@@ -360,7 +360,7 @@ class ImageLoaderBase: @unchecked Sendable {
360360
return true
361361
}
362362

363-
logger.info("Fetching vector images...")
363+
logger.info("Fetching \(imagesDict.count) images from '\(frameName)'...")
364364
let imageIdToImagePath = try await loadImages(
365365
fileId: fileId,
366366
imagesDict: imagesDict,
@@ -465,7 +465,7 @@ class ImageLoaderBase: @unchecked Sendable {
465465
return true
466466
}
467467

468-
logger.info("Fetching vector images...")
468+
logger.info("Fetching \(imagesDict.count) images from '\(frameName)'...")
469469
let imageIdToImagePath = try await loadImages(
470470
fileId: fileId,
471471
imagesDict: imagesDict,

Sources/ExFig/Shared/BatchProgressViewStorage.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,23 @@ enum BatchProgressViewStorage {
1515
/// When set, individual export commands suppress their spinners and progress bars
1616
/// to prevent corruption of the multi-line batch progress display.
1717
@TaskLocal static var progressView: BatchProgressView?
18+
19+
/// Callback type for reporting incremental download progress.
20+
typealias DownloadProgressCallback = @Sendable (Int, Int) async -> Void
21+
22+
/// TaskLocal callback for download progress updates.
23+
/// Export files report progress through this callback when in batch mode.
24+
@TaskLocal static var downloadProgressCallback: DownloadProgressCallback?
25+
26+
/// Current asset type being processed (icons, images, colors, typography).
27+
/// Used to route progress updates to the correct field in BatchProgressView.
28+
enum AssetType: String, Sendable {
29+
case colors
30+
case icons
31+
case images
32+
case typography
33+
}
34+
35+
/// TaskLocal to track which asset type is currently being processed.
36+
@TaskLocal static var currentAssetType: AssetType?
1837
}

Sources/ExFig/Subcommands/Export/AndroidIconsExport.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,12 @@ extension ExFigCommand.ExportIcons {
196196
try await PipelinedDownloader.download(
197197
files: remoteFiles,
198198
fileDownloader: fileDownloader
199-
) { current, _ in
199+
) { current, total in
200200
progress.update(current: current)
201+
// Report to batch progress if in batch mode
202+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
203+
Task { await callback(current, total) }
204+
}
201205
}
202206
}
203207
} else {
@@ -404,8 +408,12 @@ extension ExFigCommand.ExportIcons {
404408
try await PipelinedDownloader.download(
405409
files: remoteFiles,
406410
fileDownloader: fileDownloader
407-
) { current, _ in
411+
) { current, total in
408412
progress.update(current: current)
413+
// Report to batch progress if in batch mode
414+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
415+
Task { await callback(current, total) }
416+
}
409417
}
410418
}
411419
} else {

Sources/ExFig/Subcommands/Export/AndroidImagesExport.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,12 @@ extension ExFigCommand.ExportImages {
211211
try await PipelinedDownloader.download(
212212
files: remoteFiles,
213213
fileDownloader: fileDownloader
214-
) { current, _ in
214+
) { current, total in
215215
progress.update(current: current)
216+
// Report to batch progress if in batch mode
217+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
218+
Task { await callback(current, total) }
219+
}
216220
}
217221
}
218222
} else {
@@ -296,8 +300,12 @@ extension ExFigCommand.ExportImages {
296300
try await PipelinedDownloader.download(
297301
files: remoteFiles,
298302
fileDownloader: fileDownloader
299-
) { current, _ in
303+
) { current, total in
300304
progress.update(current: current)
305+
// Report to batch progress if in batch mode
306+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
307+
Task { await callback(current, total) }
308+
}
301309
}
302310
}
303311
} else {
@@ -381,8 +389,12 @@ extension ExFigCommand.ExportImages {
381389
try await PipelinedDownloader.download(
382390
files: remoteFiles,
383391
fileDownloader: fileDownloader
384-
) { current, _ in
392+
) { current, total in
385393
progress.update(current: current)
394+
// Report to batch progress if in batch mode
395+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
396+
Task { await callback(current, total) }
397+
}
386398
}
387399
}
388400
} else {

Sources/ExFig/Subcommands/Export/FlutterIconsExport.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,12 @@ extension ExFigCommand.ExportIcons {
163163
try await PipelinedDownloader.download(
164164
files: remoteFiles,
165165
fileDownloader: fileDownloader
166-
) { current, _ in
166+
) { current, total in
167167
progress.update(current: current)
168+
// Report to batch progress if in batch mode
169+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
170+
Task { await callback(current, total) }
171+
}
168172
}
169173
}
170174
} else {

Sources/ExFig/Subcommands/Export/FlutterImagesExport.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,12 @@ extension ExFigCommand.ExportImages {
168168
try await PipelinedDownloader.download(
169169
files: remoteFiles,
170170
fileDownloader: fileDownloader
171-
) { current, _ in
171+
) { current, total in
172172
progress.update(current: current)
173+
// Report to batch progress if in batch mode
174+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
175+
Task { await callback(current, total) }
176+
}
173177
}
174178
}
175179
} else {

Sources/ExFig/Subcommands/Export/iOSIconsExport.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,12 @@ extension ExFigCommand.ExportIcons {
181181
try await PipelinedDownloader.download(
182182
files: localAndRemoteFiles,
183183
fileDownloader: fileDownloader
184-
) { current, _ in
184+
) { current, total in
185185
progress.update(current: current)
186+
// Report to batch progress if in batch mode
187+
if let callback = BatchProgressViewStorage.downloadProgressCallback {
188+
Task { await callback(current, total) }
189+
}
186190
}
187191
}
188192
} else {

0 commit comments

Comments
 (0)