1mod config;
2mod packs;
3
4use std::{
5 collections::BTreeSet,
6 fmt::Write,
7 fs,
8 path::{Path, PathBuf},
9};
10
11use chromasync_color::ColorError;
12use chromasync_extract::{ExtractError, ExtractedSeed};
13use chromasync_renderers::{RendererError, RendererRegistry, TargetRegistry};
14use chromasync_template::{ListedTemplate, TemplateError, resolve_tokens_with_strategy};
15use chromasync_types::{
16 ChromaStrategy, ContrastStrategy, GeneratedArtifact, GeneratedPalette, GenerationContext,
17 GenerationRequest, PaletteFamilyName, SemanticTokens, ThemeMode, ThemePack,
18};
19use thiserror::Error;
20
21pub use chromasync_renderers::{ListedTarget, OutputRegistry};
22pub use config::{
23 ChromasyncConfig, ConfigHook, ConfigTarget, HookEvents, InstallSummary, SyncMode, SyncProfile,
24 config_file_path, expand_tilde, install_target,
25};
26pub use packs::{PackError, PackRegistry, pack_search_roots};
27
28#[derive(Debug, Clone)]
29pub struct PackInfo {
30 pub pack: ThemePack,
31 pub templates: Vec<ListedTemplate>,
32 pub targets: Vec<ListedTarget>,
33}
34
35#[derive(Debug, Error)]
36pub enum CoreError {
37 #[error("{feature} is not implemented yet")]
38 NotYetImplemented { feature: &'static str },
39 #[error("{operation} requires a seed color via --seed")]
40 MissingSeed { operation: &'static str },
41 #[error("{operation} requires an image path via --image")]
42 MissingWallpaper { operation: &'static str },
43 #[error(
44 "{operation} requires a template via --template (target '{target}' does not specify a preferred_template)"
45 )]
46 MissingTemplate {
47 operation: &'static str,
48 target: String,
49 },
50 #[error(transparent)]
51 Pack(#[from] PackError),
52 #[error(transparent)]
53 Template(#[from] TemplateError),
54 #[error(transparent)]
55 Renderer(#[from] RendererError),
56 #[error(transparent)]
57 Extract(#[from] ExtractError),
58 #[error(transparent)]
59 Color(#[from] ColorError),
60 #[error("refusing to overwrite existing artifact '{path}'")]
61 ArtifactExists { path: PathBuf },
62 #[error("multiple artifacts would write to the same destination '{path}'")]
63 ArtifactCollision { path: PathBuf },
64 #[error("failed to create output directory '{path}': {source}")]
65 CreateOutputDir {
66 path: PathBuf,
67 source: std::io::Error,
68 },
69 #[error("failed to write artifact '{path}': {source}")]
70 WriteArtifact {
71 path: PathBuf,
72 source: std::io::Error,
73 },
74 #[error("could not resolve the chromasync user config directory")]
75 UserConfigDirUnavailable,
76 #[error("failed to read chromasync config '{path}': {source}")]
77 ConfigRead {
78 path: PathBuf,
79 source: std::io::Error,
80 },
81 #[error("failed to parse chromasync config '{path}': {error}")]
82 ConfigParse {
83 path: PathBuf,
84 #[source]
85 error: toml::de::Error,
86 },
87 #[error("failed to serialize chromasync config: {error}")]
88 ConfigSerialize { error: String },
89 #[error("failed to write chromasync config '{path}': {source}")]
90 ConfigWrite {
91 path: PathBuf,
92 source: std::io::Error,
93 },
94 #[error("target '{name}' is already installed at '{path}'; pass --overwrite to replace it")]
95 TargetAlreadyInstalled { name: String, path: PathBuf },
96 #[error("failed to create targets directory '{path}': {source}")]
97 CreateTargetsDir {
98 path: PathBuf,
99 source: std::io::Error,
100 },
101 #[error("failed to copy target file from '{from}' to '{to}': {source}")]
102 CopyTargetFile {
103 from: PathBuf,
104 to: PathBuf,
105 source: std::io::Error,
106 },
107}
108
109pub fn list_templates() -> Result<Vec<ListedTemplate>, CoreError> {
110 let packs = PackRegistry::discover()?;
111
112 chromasync_template::list_templates_with_packs(packs.packs()).map_err(CoreError::from)
113}
114
115pub fn list_targets() -> Result<Vec<ListedTarget>, CoreError> {
116 Ok(load_output_registry()?.list_targets())
117}
118
119pub fn load_output_registry() -> Result<OutputRegistry, CoreError> {
120 let packs = PackRegistry::discover()?;
121
122 OutputRegistry::discover_with_packs(packs.packs()).map_err(CoreError::from)
123}
124
125pub fn list_packs() -> Result<Vec<ThemePack>, CoreError> {
126 Ok(PackRegistry::discover()?.packs().to_vec())
127}
128
129pub fn pack_info(name: &str) -> Result<PackInfo, CoreError> {
130 let packs = PackRegistry::discover()?;
131 let pack = packs
132 .get(name)
133 .cloned()
134 .ok_or_else(|| PackError::PackNotFound {
135 pack: name.to_owned(),
136 })?;
137 let templates = chromasync_template::pack_templates(&pack)?;
138 let built_in = RendererRegistry::new();
139 let targets = TargetRegistry::from_theme_packs(
140 std::slice::from_ref(&pack),
141 &built_in.built_in_name_set(),
142 )?
143 .list_targets()
144 .to_vec();
145
146 Ok(PackInfo {
147 pack,
148 templates,
149 targets,
150 })
151}
152
153pub fn generate_palette(
154 seed: &str,
155 mode: ThemeMode,
156 chroma: ChromaStrategy,
157) -> Result<GeneratedPalette, CoreError> {
158 chromasync_color::generate_palette(seed, mode, chroma).map_err(CoreError::from)
159}
160
161pub fn generate(request: &GenerationRequest) -> Result<Vec<GeneratedArtifact>, CoreError> {
162 let output_registry = load_output_registry()?;
163
164 generate_with_output_registry(request, &output_registry)
165}
166
167pub fn preview(request: &GenerationRequest) -> Result<String, CoreError> {
168 let palette = palette_from_request(request, "preview")?;
169 let template_name = request.template.as_deref().ok_or(CoreError::MissingSeed {
170 operation: "preview requires a template via --template",
171 })?;
172 let template = load_template(template_name, request.mode)?;
173 let tokens = resolve_tokens_with_strategy(&palette, &template.definition, request.contrast)?;
174
175 Ok(render_preview(
176 &palette,
177 &template,
178 &tokens,
179 request.contrast,
180 ))
181}
182
183pub fn export_tokens(request: &GenerationRequest) -> Result<SemanticTokens, CoreError> {
184 let palette = palette_from_request(request, "token export")?;
185 let template_name = request.template.as_deref().ok_or(CoreError::MissingSeed {
186 operation: "token export requires a template via --template",
187 })?;
188 let template = load_template(template_name, request.mode)?;
189
190 resolve_tokens_with_strategy(&palette, &template.definition, request.contrast)
191 .map_err(CoreError::from)
192}
193
194pub fn generate_from_wallpaper(
195 request: &GenerationRequest,
196) -> Result<Vec<GeneratedArtifact>, CoreError> {
197 let output_registry = load_output_registry()?;
198
199 generate_from_wallpaper_with_output_registry(request, &output_registry)
200}
201
202pub fn generate_with_output_registry(
203 request: &GenerationRequest,
204 output_registry: &OutputRegistry,
205) -> Result<Vec<GeneratedArtifact>, CoreError> {
206 render_from_palette(request, output_registry)
207}
208
209pub fn generate_from_wallpaper_with_output_registry(
210 request: &GenerationRequest,
211 output_registry: &OutputRegistry,
212) -> Result<Vec<GeneratedArtifact>, CoreError> {
213 render_from_palette_with_wallpaper(request, output_registry)
214}
215
216pub fn write_artifacts(
228 output_dir: &Path,
229 artifacts: &[GeneratedArtifact],
230 force: bool,
231) -> Result<Vec<PathBuf>, CoreError> {
232 if artifacts.is_empty() {
233 return Ok(Vec::new());
234 }
235
236 let resolved: Vec<ResolvedArtifact> = artifacts
237 .iter()
238 .map(|artifact| ResolvedArtifact {
239 output_dir: output_dir.to_path_buf(),
240 file_name: artifact.file_name.clone(),
241 content: artifact.content.clone(),
242 force,
243 })
244 .collect();
245
246 write_resolved_artifacts(&resolved)
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct ResolvedArtifact {
258 pub output_dir: PathBuf,
259 pub file_name: String,
260 pub content: String,
261 pub force: bool,
262}
263
264pub fn write_resolved_artifacts(entries: &[ResolvedArtifact]) -> Result<Vec<PathBuf>, CoreError> {
271 if entries.is_empty() {
272 return Ok(Vec::new());
273 }
274
275 let mut seen_paths = BTreeSet::new();
276 let mut destinations = Vec::with_capacity(entries.len());
277
278 for entry in entries {
279 let path = entry.output_dir.join(&entry.file_name);
280
281 if !seen_paths.insert(path.clone()) {
282 return Err(CoreError::ArtifactCollision { path });
283 }
284
285 if !entry.force && path.exists() {
286 return Err(CoreError::ArtifactExists { path });
287 }
288
289 destinations.push(path);
290 }
291
292 for entry in entries {
293 fs::create_dir_all(&entry.output_dir).map_err(|source| CoreError::CreateOutputDir {
294 path: entry.output_dir.clone(),
295 source,
296 })?;
297 }
298
299 for (entry, path) in entries.iter().zip(&destinations) {
300 fs::write(path, &entry.content).map_err(|source| CoreError::WriteArtifact {
301 path: path.clone(),
302 source,
303 })?;
304 }
305
306 Ok(destinations)
307}
308
309fn palette_from_request(
310 request: &GenerationRequest,
311 operation: &'static str,
312) -> Result<GeneratedPalette, CoreError> {
313 let seed = request
314 .seed
315 .as_deref()
316 .ok_or(CoreError::MissingSeed { operation })?;
317
318 generate_palette(seed, request.mode, request.chroma)
319}
320
321fn palette_from_wallpaper_request(
322 request: &GenerationRequest,
323) -> Result<GeneratedPalette, CoreError> {
324 let wallpaper = request
325 .wallpaper
326 .as_deref()
327 .ok_or(CoreError::MissingWallpaper {
328 operation: "wallpaper theme generation",
329 })?;
330 let extraction = chromasync_extract::extract_seed_candidates(wallpaper)?;
331
332 palette_from_extracted_seeds(&extraction.seeds, request.mode, request.chroma)
333}
334
335fn palette_from_extracted_seeds(
336 seeds: &[ExtractedSeed],
337 mode: ThemeMode,
338 chroma: ChromaStrategy,
339) -> Result<GeneratedPalette, CoreError> {
340 let primary_seed = seeds.first().ok_or(CoreError::NotYetImplemented {
341 feature: "wallpaper extraction returned no seed candidates",
342 })?;
343 let mut palette = generate_palette(&primary_seed.hex, mode, chroma)?;
344
345 apply_seed_metadata(&mut palette, primary_seed, 0);
346
347 if let Some(seed) = seeds.get(1) {
348 replace_family_from_seed(
349 &mut palette,
350 PaletteFamilyName::Secondary,
351 seed,
352 1,
353 mode,
354 chroma,
355 )?;
356 }
357
358 if let Some(seed) = seeds.get(2) {
359 replace_family_from_seed(
360 &mut palette,
361 PaletteFamilyName::Tertiary,
362 seed,
363 2,
364 mode,
365 chroma,
366 )?;
367 }
368
369 Ok(palette)
370}
371
372fn replace_family_from_seed(
373 palette: &mut GeneratedPalette,
374 family_name: PaletteFamilyName,
375 seed: &ExtractedSeed,
376 seed_index: usize,
377 mode: ThemeMode,
378 chroma: ChromaStrategy,
379) -> Result<(), CoreError> {
380 let source_palette = generate_palette(&seed.hex, mode, chroma)?;
381 let mut family = source_palette
382 .families
383 .get(&PaletteFamilyName::Primary)
384 .cloned()
385 .ok_or(CoreError::NotYetImplemented {
386 feature: "primary palette family generation",
387 })?;
388
389 family.name = family_name;
390 family.dominance = Some(seed.dominance);
391 family.source_region = seed.source_region.clone();
392 family.seed_index = Some(seed_index);
393 palette.families.insert(family_name, family);
394
395 Ok(())
396}
397
398fn apply_seed_metadata(palette: &mut GeneratedPalette, seed: &ExtractedSeed, seed_index: usize) {
399 for family in palette.families.values_mut() {
400 family.dominance = Some(seed.dominance);
401 family.source_region = seed.source_region.clone();
402 family.seed_index = Some(seed_index);
403 }
404}
405
406fn render_from_palette(
407 request: &GenerationRequest,
408 output_registry: &OutputRegistry,
409) -> Result<Vec<GeneratedArtifact>, CoreError> {
410 use std::collections::BTreeMap;
411
412 let mut groups: BTreeMap<(String, ChromaStrategy), Vec<String>> = BTreeMap::new();
414
415 for target in &request.targets {
416 let template_name = request
417 .template
418 .clone()
419 .or_else(|| output_registry.resolve_preferred_template(target))
420 .ok_or_else(|| CoreError::MissingTemplate {
421 operation: "theme generation",
422 target: target.clone(),
423 })?;
424 let chroma = output_registry
425 .resolve_chroma_strategy(target)
426 .unwrap_or(request.chroma);
427
428 groups
429 .entry((template_name, chroma))
430 .or_default()
431 .push(target.clone());
432 }
433
434 let mut artifacts = Vec::new();
435
436 for ((template_name, chroma), targets) in &groups {
437 let template = load_template(template_name, request.mode)?;
438 let palette = if *chroma == request.chroma {
439 palette_from_request(request, "theme generation")?
440 } else {
441 let mut req = request.clone();
442 req.chroma = *chroma;
443 palette_from_request(&req, "theme generation (chroma override)")?
444 };
445
446 let tokens =
447 resolve_tokens_with_strategy(&palette, &template.definition, request.contrast)?;
448 let context = GenerationContext {
449 mode: request.mode,
450 template_name: template.definition.name.clone(),
451 chroma: *chroma,
452 output_dir: request.output_dir.clone(),
453 seed: request.seed.clone(),
454 };
455
456 artifacts.extend(
457 output_registry
458 .generate(targets, &tokens, &context)
459 .map_err(CoreError::from)?,
460 );
461 }
462
463 Ok(artifacts)
464}
465
466fn render_from_palette_with_wallpaper(
467 request: &GenerationRequest,
468 output_registry: &OutputRegistry,
469) -> Result<Vec<GeneratedArtifact>, CoreError> {
470 use std::collections::BTreeMap;
471
472 let mut groups: BTreeMap<(String, ChromaStrategy), Vec<String>> = BTreeMap::new();
474
475 for target in &request.targets {
476 let template_name = request
477 .template
478 .clone()
479 .or_else(|| output_registry.resolve_preferred_template(target))
480 .ok_or_else(|| CoreError::MissingTemplate {
481 operation: "theme generation",
482 target: target.clone(),
483 })?;
484 let chroma = output_registry
485 .resolve_chroma_strategy(target)
486 .unwrap_or(request.chroma);
487
488 groups
489 .entry((template_name, chroma))
490 .or_default()
491 .push(target.clone());
492 }
493
494 let mut artifacts = Vec::new();
495
496 for ((template_name, chroma), targets) in &groups {
497 let template = load_template(template_name, request.mode)?;
498 let palette = if *chroma == request.chroma {
499 palette_from_wallpaper_request(request)?
500 } else {
501 let mut req = request.clone();
502 req.chroma = *chroma;
503 palette_from_wallpaper_request(&req)?
504 };
505
506 let tokens =
507 resolve_tokens_with_strategy(&palette, &template.definition, request.contrast)?;
508 let context = GenerationContext {
509 mode: request.mode,
510 template_name: template.definition.name.clone(),
511 chroma: *chroma,
512 output_dir: request.output_dir.clone(),
513 seed: request.seed.clone(),
514 };
515
516 artifacts.extend(
517 output_registry
518 .generate(targets, &tokens, &context)
519 .map_err(CoreError::from)?,
520 );
521 }
522
523 Ok(artifacts)
524}
525
526fn load_template(requested: &str, mode: ThemeMode) -> Result<ListedTemplate, CoreError> {
527 let packs = PackRegistry::discover()?;
528
529 chromasync_template::load_template_with_packs(requested, mode, packs.packs())
530 .map_err(CoreError::from)
531}
532
533fn render_preview(
534 palette: &GeneratedPalette,
535 template: &ListedTemplate,
536 tokens: &SemanticTokens,
537 contrast: ContrastStrategy,
538) -> String {
539 let mut output = String::with_capacity(2048);
540
541 let _ = writeln!(output, "Seed: {}", palette.seed);
542 let _ = writeln!(output, "Mode: {}", palette.mode);
543 let _ = writeln!(output, "Template: {}", template.definition.name);
544 let _ = writeln!(output, "Contrast: {contrast}");
545 let _ = writeln!(
546 output,
547 "Template Source: {} ({})",
548 template.source.label(),
549 template.source.location()
550 );
551
552 if let Some(description) = &template.definition.description {
553 let _ = writeln!(output, "Description: {description}");
554 }
555
556 let _ = writeln!(output);
557 let _ = writeln!(output, "Palette Families");
558
559 for family_name in PaletteFamilyName::ALL {
560 if let Some(family) = palette.families.get(&family_name) {
561 let _ = write!(
562 output,
563 "{} hue={:.2} chroma={:.3}",
564 family_name, family.hue, family.base_chroma
565 );
566
567 for tone in &family.tones {
568 let _ = write!(output, " {}={}", tone.tone, tone.hex);
569 }
570
571 let _ = writeln!(output);
572 }
573 }
574
575 let _ = writeln!(output);
576 let _ = writeln!(output, "Semantic Tokens");
577
578 for (name, value) in semantic_token_rows(tokens) {
579 let _ = writeln!(output, "{name:<16} {value}");
580 }
581
582 output
583}
584
585fn semantic_token_rows(tokens: &SemanticTokens) -> [(&'static str, &str); 17] {
586 [
587 ("bg", tokens.bg.as_str()),
588 ("bg_secondary", tokens.bg_secondary.as_str()),
589 ("surface", tokens.surface.as_str()),
590 ("surface_elevated", tokens.surface_elevated.as_str()),
591 ("text", tokens.text.as_str()),
592 ("text_muted", tokens.text_muted.as_str()),
593 ("border", tokens.border.as_str()),
594 ("border_strong", tokens.border_strong.as_str()),
595 ("accent", tokens.accent.as_str()),
596 ("accent_hover", tokens.accent_hover.as_str()),
597 ("accent_active", tokens.accent_active.as_str()),
598 ("accent_fg", tokens.accent_fg.as_str()),
599 ("selection", tokens.selection.as_str()),
600 ("link", tokens.link.as_str()),
601 ("success", tokens.success.as_str()),
602 ("warning", tokens.warning.as_str()),
603 ("error", tokens.error.as_str()),
604 ]
605}
606
607#[cfg(test)]
608mod tests {
609 use std::path::PathBuf;
610
611 use chromasync_color::contrast_ratio;
612 use chromasync_types::{
613 ChromaStrategy, ContrastStrategy, GenerationRequest, PaletteFamilyName, ThemeMode,
614 };
615
616 use super::{export_tokens, generate_palette, palette_from_wallpaper_request, preview};
617
618 #[test]
619 fn palette_generation_is_available_without_renderer_code() {
620 let palette = generate_palette("#ff6b6b", ThemeMode::Dark, ChromaStrategy::Normal)
621 .expect("palette generation should work");
622
623 assert!(palette.families.contains_key(&PaletteFamilyName::Primary));
624 assert!(palette.families.contains_key(&PaletteFamilyName::Neutral));
625 }
626
627 #[test]
628 fn token_export_resolves_built_in_templates() {
629 let request = GenerationRequest {
630 seed: Some("#4ecdc4".to_owned()),
631 wallpaper: None,
632 template: Some("minimal".to_owned()),
633 mode: ThemeMode::Dark,
634 chroma: ChromaStrategy::Normal,
635 contrast: ContrastStrategy::RelativeLuminance,
636 targets: Vec::new(),
637 output_dir: "chromasync".into(),
638 };
639
640 let tokens = export_tokens(&request).expect("token export should work");
641
642 assert!(tokens.bg.starts_with('#'));
643 assert!(tokens.accent.starts_with('#'));
644 assert!(
645 contrast_ratio(&tokens.text, &tokens.bg).expect("contrast should compute")
646 >= chromasync_color::MIN_CONTRAST_RATIO
647 );
648 }
649
650 #[test]
651 fn preview_includes_palette_and_semantic_token_sections() {
652 let request = GenerationRequest {
653 seed: Some("#ff6b6b".to_owned()),
654 wallpaper: None,
655 template: Some("brutalist".to_owned()),
656 mode: ThemeMode::Dark,
657 chroma: ChromaStrategy::Normal,
658 contrast: ContrastStrategy::RelativeLuminance,
659 targets: Vec::new(),
660 output_dir: "chromasync".into(),
661 };
662
663 let rendered = preview(&request).expect("preview should render");
664
665 assert!(rendered.contains("Palette Families"));
666 assert!(rendered.contains("Semantic Tokens"));
667 assert!(rendered.contains("primary"));
668 assert!(rendered.contains("accent"));
669 }
670
671 #[test]
672 fn generate_returns_requested_artifacts() {
673 let request = GenerationRequest {
674 seed: Some("#4ecdc4".to_owned()),
675 wallpaper: None,
676 template: Some("terminal".to_owned()),
677 mode: ThemeMode::Dark,
678 chroma: ChromaStrategy::Normal,
679 contrast: ContrastStrategy::RelativeLuminance,
680 targets: vec![
681 example_target_path("gtk.toml"),
682 "hyprland".to_owned(),
683 "kitty".to_owned(),
684 example_target_path("css.toml"),
685 ],
686 output_dir: "chromasync".into(),
687 };
688
689 let artifacts = super::generate(&request).expect("generation should render artifacts");
690
691 assert_eq!(artifacts.len(), 4);
692 assert_eq!(
693 artifacts
694 .iter()
695 .map(|artifact| artifact.target.clone())
696 .collect::<Vec<_>>(),
697 vec![
698 "gtk".to_owned(),
699 "hyprland".to_owned(),
700 "kitty".to_owned(),
701 "css".to_owned(),
702 ]
703 );
704 assert_eq!(
705 artifacts
706 .iter()
707 .map(|artifact| artifact.file_name.as_str())
708 .collect::<Vec<_>>(),
709 vec!["gtk.css", "hyprland.conf", "kitty.conf", "theme.css"]
710 );
711 assert!(
712 artifacts
713 .iter()
714 .all(|artifact| !artifact.content.is_empty())
715 );
716 }
717
718 #[test]
719 fn wallpaper_generation_assigns_metadata_and_multi_seed_families() {
720 let request = GenerationRequest {
721 seed: None,
722 wallpaper: Some(wallpaper_fixture("wallpaper-blocks.png")),
723 template: Some("minimal".to_owned()),
724 mode: ThemeMode::Dark,
725 chroma: ChromaStrategy::Normal,
726 contrast: ContrastStrategy::RelativeLuminance,
727 targets: vec![example_target_path("css.toml")],
728 output_dir: "chromasync".into(),
729 };
730
731 let palette = palette_from_wallpaper_request(&request)
732 .expect("wallpaper palette generation should succeed");
733
734 let primary = palette
735 .families
736 .get(&PaletteFamilyName::Primary)
737 .expect("primary family should exist");
738 let secondary = palette
739 .families
740 .get(&PaletteFamilyName::Secondary)
741 .expect("secondary family should exist");
742 let tertiary = palette
743 .families
744 .get(&PaletteFamilyName::Tertiary)
745 .expect("tertiary family should exist");
746
747 assert_eq!(primary.seed_index, Some(0));
748 assert_eq!(secondary.seed_index, Some(1));
749 assert_eq!(tertiary.seed_index, Some(2));
750 assert_eq!(primary.source_region.as_deref(), Some("center-left"));
751 assert_eq!(secondary.source_region.as_deref(), Some("center"));
752 assert_eq!(tertiary.source_region.as_deref(), Some("center-right"));
753 assert!(primary.dominance.expect("primary dominance should exist") > 0.45);
754 assert!(
755 secondary
756 .dominance
757 .expect("secondary dominance should exist")
758 > 0.30
759 );
760 assert!(tertiary.dominance.expect("tertiary dominance should exist") > 0.15);
761 }
762
763 fn wallpaper_fixture(name: &str) -> PathBuf {
764 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
765 .join("../chromasync-extract/tests/fixtures")
766 .join(name)
767 }
768
769 fn example_target_path(name: &str) -> String {
770 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
771 .join("../../examples/targets")
772 .join(name)
773 .display()
774 .to_string()
775 }
776}