@devaloop/devalang 0.0.1-alpha.3 → 0.0.1-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.devalang +1 -1
  2. package/Cargo.toml +9 -7
  3. package/README.md +49 -25
  4. package/docs/CHANGELOG.md +30 -0
  5. package/docs/COMMANDS.md +31 -0
  6. package/docs/CONFIG.md +6 -4
  7. package/docs/ROADMAP.md +4 -4
  8. package/docs/TODO.md +4 -4
  9. package/examples/index.deva +9 -2
  10. package/examples/samples/hat-808.wav +0 -0
  11. package/out-tsc/bin/devalang.exe +0 -0
  12. package/package.json +44 -42
  13. package/project-version.json +6 -6
  14. package/rust/audio/engine.rs +126 -0
  15. package/rust/audio/interpreter.rs +143 -0
  16. package/rust/audio/loader.rs +46 -0
  17. package/rust/audio/mod.rs +5 -0
  18. package/rust/audio/player.rs +54 -0
  19. package/rust/audio/render.rs +57 -0
  20. package/rust/cli/build.rs +17 -5
  21. package/rust/cli/check.rs +2 -1
  22. package/rust/cli/init.rs +4 -2
  23. package/rust/cli/mod.rs +29 -0
  24. package/rust/cli/play.rs +192 -0
  25. package/rust/cli/template.rs +2 -1
  26. package/rust/config/loader.rs +0 -1
  27. package/rust/config/mod.rs +3 -2
  28. package/rust/core/builder/mod.rs +54 -6
  29. package/rust/core/debugger/lexer.rs +20 -5
  30. package/rust/core/debugger/preprocessor.rs +9 -5
  31. package/rust/core/lexer/handler/mod.rs +2 -2
  32. package/rust/core/lexer/handler/newline.rs +5 -1
  33. package/rust/core/lexer/mod.rs +10 -5
  34. package/rust/core/parser/handler/loop_.rs +11 -0
  35. package/rust/core/parser/mod.rs +0 -1
  36. package/rust/core/preprocessor/loader.rs +89 -16
  37. package/rust/core/preprocessor/module.rs +2 -0
  38. package/rust/core/preprocessor/resolver/bank.rs +46 -0
  39. package/rust/core/preprocessor/resolver/loop_.rs +148 -0
  40. package/rust/core/preprocessor/resolver/mod.rs +151 -0
  41. package/rust/core/preprocessor/resolver/tempo.rs +49 -0
  42. package/rust/core/preprocessor/resolver/trigger.rs +114 -0
  43. package/rust/lib.rs +118 -0
  44. package/rust/main.rs +8 -0
  45. package/rust/utils/logger.rs +45 -6
  46. package/rust/utils/spinner.rs +2 -0
  47. package/rust/utils/watcher.rs +10 -2
  48. package/templates/minimal/.devalang +2 -1
  49. package/templates/minimal/README.md +202 -0
  50. package/templates/welcome/.devalang +2 -1
  51. package/templates/welcome/README.md +48 -31
  52. package/rust/core/preprocessor/resolver.rs +0 -372
@@ -0,0 +1,143 @@
1
+ use crate::{
2
+ audio::{ engine::AudioEngine, loader::load_trigger },
3
+ core::{
4
+ parser::statement::{ Statement, StatementKind },
5
+ shared::value::Value,
6
+ store::variable::VariableTable,
7
+ },
8
+ };
9
+
10
+ pub fn interprete_statements(
11
+ statements: &Vec<Statement>,
12
+ audio_engine: AudioEngine,
13
+ entry: String,
14
+ output: String
15
+ ) -> (AudioEngine, f32, f32) {
16
+ let mut base_bpm = 120.0;
17
+ let mut base_duration = 60.0 / base_bpm;
18
+
19
+ let variable_table = audio_engine.variables.clone();
20
+
21
+ let (updated_audio_engine, base_bpm, max_end_time) = execute_audio_statements(
22
+ audio_engine.clone(),
23
+ variable_table.clone(),
24
+ statements.clone(),
25
+ base_bpm.clone(),
26
+ base_duration.clone(),
27
+ 0.0,
28
+ 0.0
29
+ );
30
+
31
+ (updated_audio_engine, base_bpm, max_end_time)
32
+ }
33
+
34
+ pub fn execute_audio_statements(
35
+ mut audio_engine: AudioEngine,
36
+ mut variable_table: VariableTable,
37
+ mut statements: Vec<Statement>,
38
+ mut base_bpm: f32,
39
+ mut base_duration: f32,
40
+ mut max_end_time: f32,
41
+ mut cursor_time: f32
42
+ ) -> (AudioEngine, f32, f32) {
43
+ for stmt in statements {
44
+ match &stmt.kind {
45
+ StatementKind::Load { source, alias } => {
46
+ variable_table.set(alias.to_string(), Value::String(source.clone()));
47
+ }
48
+
49
+ StatementKind::Let { name } => {
50
+ variable_table.set(name.to_string(), stmt.value.clone());
51
+ }
52
+
53
+ StatementKind::Tempo => {
54
+ if let Value::Number(bpm_) = &stmt.value {
55
+ base_bpm = *bpm_ as f32;
56
+ base_duration = 60.0 / base_bpm;
57
+ } else {
58
+ eprintln!("❌ Invalid tempo value: {:?}", stmt.value);
59
+ }
60
+ }
61
+
62
+ StatementKind::Trigger { entity, duration } => {
63
+ if let Some(trigger_val) = variable_table.get(entity) {
64
+ let (src, duration_secs) = load_trigger(
65
+ trigger_val,
66
+ duration,
67
+ base_duration,
68
+ variable_table.clone()
69
+ );
70
+
71
+ audio_engine.insert(&src, cursor_time, duration_secs, None);
72
+
73
+ cursor_time += duration_secs;
74
+
75
+ if cursor_time > max_end_time {
76
+ max_end_time = cursor_time;
77
+ }
78
+ } else {
79
+ eprintln!("❌ Unknown trigger entity: {}", entity);
80
+ }
81
+ }
82
+
83
+ StatementKind::Loop => {
84
+ if let Value::Map(loop_value) = &stmt.value {
85
+ let iterator = loop_value.get("iterator");
86
+ let body = loop_value.get("body");
87
+
88
+ let loop_count = if let Some(Value::Number(n)) = iterator {
89
+ *n as usize
90
+ } else {
91
+ eprintln!("❌ Loop iterator must be a number: {:?}", iterator);
92
+ continue;
93
+ };
94
+
95
+ let loop_body = if let Some(Value::Block(body)) = body {
96
+ body.clone()
97
+ } else {
98
+ eprintln!("❌ Loop body must be a block: {:?}", body);
99
+ continue;
100
+ };
101
+
102
+ for _ in 0..loop_count {
103
+ let (loop_engine, _, loop_end_time) = execute_audio_statements(
104
+ audio_engine.clone(),
105
+ variable_table.clone(),
106
+ loop_body.clone(),
107
+ base_bpm,
108
+ base_duration,
109
+ max_end_time,
110
+ cursor_time
111
+ );
112
+
113
+ audio_engine = loop_engine;
114
+
115
+ // Update time and max_end_time after each loop iteration
116
+ cursor_time = loop_end_time;
117
+ if loop_end_time > max_end_time {
118
+ max_end_time = loop_end_time;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ StatementKind::Bank => {}
125
+
126
+ StatementKind::Import { names, source } => {}
127
+
128
+ StatementKind::Export { names, source } => {}
129
+
130
+ StatementKind::Unknown => {
131
+ // Ignore unknown statements
132
+ }
133
+
134
+ _ => {
135
+ eprintln!("Unsupported statement kind: {:?}", stmt);
136
+ }
137
+ }
138
+ }
139
+
140
+ audio_engine.set_variables(variable_table);
141
+
142
+ (audio_engine, base_bpm, max_end_time)
143
+ }
@@ -0,0 +1,46 @@
1
+ use crate::core::{ shared::{ duration::Duration, value::Value }, store::variable::VariableTable };
2
+
3
+ pub fn load_trigger(
4
+ trigger: &Value,
5
+ duration: &Duration,
6
+ base_duration: f32,
7
+ variable_table: VariableTable
8
+ ) -> (String, f32) {
9
+ let mut trigger_path = String::new();
10
+ let mut duration_as_secs = 0.0;
11
+
12
+ match trigger {
13
+ Value::String(src) => {
14
+ trigger_path = src.to_string();
15
+ }
16
+ _ => {
17
+ eprintln!("❌ Invalid trigger type. Expected a text variable.");
18
+ }
19
+ }
20
+
21
+ match duration {
22
+ Duration::Identifier(duration_identifier) => {
23
+ if duration_identifier == "auto" {
24
+ duration_as_secs = base_duration;
25
+ } else if let Some(Value::Number(num)) = variable_table.get(duration_identifier) {
26
+ duration_as_secs = *num;
27
+ } else {
28
+ eprintln!("❌ Invalid duration identifier: {}", duration_identifier);
29
+ }
30
+ }
31
+
32
+ Duration::Number(num) => {
33
+ duration_as_secs = *num;
34
+ }
35
+
36
+ Duration::Auto => {
37
+ duration_as_secs = base_duration;
38
+ }
39
+
40
+ _ => {
41
+ eprintln!("❌ Invalid duration type. Expected an identifier.");
42
+ }
43
+ }
44
+
45
+ (trigger_path, duration_as_secs)
46
+ }
@@ -0,0 +1,5 @@
1
+ pub mod engine;
2
+ pub mod interpreter;
3
+ pub mod loader;
4
+ pub mod player;
5
+ pub mod render;
@@ -0,0 +1,54 @@
1
+ use rodio::{ Decoder, OutputStream, OutputStreamHandle, Sink, Source };
2
+ use std::{ fs::File, io::BufReader };
3
+
4
+ pub struct AudioPlayer {
5
+ _stream: OutputStream,
6
+ handle: OutputStreamHandle,
7
+ sink: Sink,
8
+ last_path: Option<String>,
9
+ }
10
+
11
+ impl AudioPlayer {
12
+ pub fn new() -> Self {
13
+ let (stream, handle) = OutputStream::try_default().unwrap();
14
+ let sink = Sink::try_new(&handle).unwrap();
15
+
16
+ Self {
17
+ _stream: stream,
18
+ handle,
19
+ sink,
20
+ last_path: None,
21
+ }
22
+ }
23
+
24
+ fn load_source(&self, path: &str) -> impl Source<Item = f32> + Send + 'static {
25
+ let file = File::open(path).unwrap();
26
+ let reader = BufReader::new(file);
27
+
28
+ Decoder::new(reader).unwrap().convert_samples()
29
+ }
30
+
31
+ pub fn play_file_once(&mut self, path: &str) {
32
+ self.sink.stop();
33
+ self.sink = Sink::try_new(&self.handle).unwrap();
34
+
35
+ self.sink.set_volume(1.0);
36
+
37
+ let source = self.load_source(path);
38
+
39
+ self.sink.append(source);
40
+ self.last_path = Some(path.to_string());
41
+ }
42
+
43
+ pub fn replay_last(&mut self) {
44
+ if let Some(path) = self.last_path.clone() {
45
+ self.play_file_once(&path);
46
+ } else {
47
+ eprintln!("⚠️ No previous audio to replay.");
48
+ }
49
+ }
50
+
51
+ pub fn wait_until_end(&self) {
52
+ self.sink.sleep_until_end();
53
+ }
54
+ }
@@ -0,0 +1,57 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::{
4
+ audio::{ engine::AudioEngine, interpreter::interprete_statements },
5
+ core::{
6
+ parser::statement::Statement,
7
+ store::global::GlobalStore,
8
+ },
9
+ utils::logger::{ LogLevel, Logger },
10
+ };
11
+
12
+ pub fn render_audio_with_modules(
13
+ modules: HashMap<String, Vec<Statement>>,
14
+ output_dir: &str,
15
+ global_store: &mut GlobalStore
16
+ ) -> HashMap<String, AudioEngine> {
17
+ let mut result = HashMap::new();
18
+
19
+ for (module_name, statements) in modules {
20
+ let mut global_max_end_time = 0.0;
21
+ let mut audio_engine = AudioEngine::new();
22
+
23
+ // Apply the module's variable table if it exists
24
+ if let Some(module) = global_store.get_module(&module_name) {
25
+ audio_engine.set_variables(module.variable_table.clone());
26
+ }
27
+
28
+ // Interpret the statements to fill the audio buffer
29
+ let (mut audio_engine, module_base_bpm, module_max_end_time) = interprete_statements(
30
+ &statements,
31
+ audio_engine,
32
+ module_name.clone(),
33
+ output_dir.to_string()
34
+ );
35
+
36
+ // Calculate the module's maximum duration
37
+ global_max_end_time = module_max_end_time.max(global_max_end_time);
38
+ audio_engine.set_duration(global_max_end_time);
39
+
40
+ // Check if the buffer contains at least one non-zero sample
41
+ if audio_engine.buffer.iter().all(|&s| s == 0) {
42
+ let logger = Logger::new();
43
+
44
+ logger.log_message(
45
+ LogLevel::Warning,
46
+ format!("Module '{}' ignored: silent buffer (no non-zero samples)", module_name).as_str()
47
+ );
48
+
49
+ continue;
50
+ }
51
+
52
+ // Insert only if the module produces sound
53
+ result.insert(module_name, audio_engine);
54
+ }
55
+
56
+ result
57
+ }
package/rust/cli/build.rs CHANGED
@@ -1,7 +1,9 @@
1
1
  use crate::{
2
+ audio::render::render_audio_with_modules,
2
3
  config::Config,
3
4
  core::{
4
5
  builder::Builder,
6
+ debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
5
7
  preprocessor::loader::ModuleLoader,
6
8
  store::global::GlobalStore,
7
9
  utils::path::{ find_entry_file, normalize_path },
@@ -10,6 +12,7 @@ use crate::{
10
12
  };
11
13
  use std::{ thread, time::Duration };
12
14
 
15
+ #[cfg(feature = "cli")]
13
16
  pub fn handle_build_command(
14
17
  config: Option<Config>,
15
18
  entry: Option<String>,
@@ -102,13 +105,23 @@ fn begin_build(entry: String, output: String) {
102
105
 
103
106
  // SECTION Load
104
107
  // NOTE: We use modules in the build command, so we need to load them
105
- let modules = module_loader.load_all(&mut global_store);
108
+ let (modules_tokens, modules_statements) = module_loader.load_all_modules(&mut global_store);
109
+
110
+ // SECTION Write logs
111
+ write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
112
+ write_preprocessor_log_file(
113
+ &normalized_output_dir,
114
+ "resolved_statements.log",
115
+ modules_statements.clone()
116
+ );
106
117
 
107
- // SECTION Build
118
+ // SECTION Building AST and Audio
108
119
  let builder = Builder::new();
109
- builder.build_ast(&modules);
120
+ builder.build_ast(&modules_statements, &normalized_output_dir);
121
+ builder.build_audio(&modules_statements, &normalized_output_dir, &mut global_store);
110
122
 
111
- // TODO: Implement debugging
123
+ // SECTION Logging
124
+ let logger = Logger::new();
112
125
 
113
126
  let success_message = format!(
114
127
  "Build completed successfully in {:.2?}. Output files written to: '{}'",
@@ -116,6 +129,5 @@ fn begin_build(entry: String, output: String) {
116
129
  normalized_output_dir
117
130
  );
118
131
 
119
- let logger = Logger::new();
120
132
  logger.log_message(LogLevel::Success, &success_message);
121
133
  }
package/rust/cli/check.rs CHANGED
@@ -9,6 +9,7 @@ use crate::{
9
9
  };
10
10
  use std::{ thread, time::Duration };
11
11
 
12
+ #[cfg(feature = "cli")]
12
13
  pub fn handle_check_command(
13
14
  config: Option<Config>,
14
15
  entry: Option<String>,
@@ -101,7 +102,7 @@ fn begin_check(entry: String, output: String) {
101
102
 
102
103
  // SECTION Load
103
104
  // NOTE: We don't use modules in the check command, but we still need to load them
104
- let modules = module_loader.load_all(&mut global_store);
105
+ let modules = module_loader.load_all_modules(&mut global_store);
105
106
 
106
107
  // TODO: Implement debugging
107
108
 
package/rust/cli/init.rs CHANGED
@@ -1,11 +1,13 @@
1
1
  use std::{ fs, path::Path };
2
2
  use include_dir::{ include_dir, Dir };
3
- use inquire::{ Select, Confirm };
4
-
5
3
  use crate::{ cli::template::get_available_templates, utils::file::copy_dir_recursive };
6
4
 
5
+ #[cfg(feature = "cli")]
6
+ use inquire::{ Select, Confirm };
7
+
7
8
  static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
8
9
 
10
+ #[cfg(feature = "cli")]
9
11
  pub fn handle_init_command(name: Option<String>, template: Option<String>) {
10
12
  let current_dir = std::env::current_dir().unwrap();
11
13
  let project_name = name
package/rust/cli/mod.rs CHANGED
@@ -2,6 +2,7 @@ pub mod check;
2
2
  pub mod build;
3
3
  pub mod init;
4
4
  pub mod template;
5
+ pub mod play;
5
6
 
6
7
  use clap::{ Parser, Subcommand };
7
8
  use crate::utils::version::get_version;
@@ -173,4 +174,32 @@ pub enum Commands {
173
174
  ///
174
175
  debug: bool,
175
176
  },
177
+
178
+ Play {
179
+ #[arg(short, long)]
180
+ /// The entry point of the program to play.
181
+ ///
182
+ entry: Option<String>,
183
+
184
+ #[arg(short, long)]
185
+ /// The directory where the output files will be generated.
186
+ ///
187
+ output: Option<String>,
188
+
189
+ #[arg(long, default_value_t = false)]
190
+ /// Whether to watch for changes and re-play.
191
+ ///
192
+ /// ### Default value
193
+ /// - `false`
194
+ ///
195
+ watch: bool,
196
+
197
+ #[arg(long, default_value_t = false)]
198
+ /// Whether to replay the program after it finishes.
199
+ ///
200
+ /// ### Default value
201
+ /// - `false`
202
+ ///
203
+ repeat: bool,
204
+ },
176
205
  }
@@ -0,0 +1,192 @@
1
+ use crate::{
2
+ audio::player::AudioPlayer,
3
+ config::Config,
4
+ core::{
5
+ builder::Builder,
6
+ debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
7
+ preprocessor::loader::ModuleLoader,
8
+ store::global::GlobalStore,
9
+ utils::path::{ find_entry_file, normalize_path },
10
+ },
11
+ utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
12
+ };
13
+
14
+ use std::{ path::Path, sync::mpsc::channel, thread, time::Duration };
15
+ use std::fs;
16
+ use std::collections::HashMap;
17
+
18
+ #[cfg(feature = "cli")]
19
+ pub fn handle_play_command(
20
+ config: Option<Config>,
21
+ entry: Option<String>,
22
+ output: Option<String>,
23
+ watch: bool,
24
+ repeat: bool
25
+ ) {
26
+ let logger = Logger::new();
27
+
28
+ let entry_path = entry
29
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.entry.clone()))
30
+ .unwrap_or_else(|| "".to_string());
31
+
32
+ let output_path = output
33
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.output.clone()))
34
+ .unwrap_or_else(|| "".to_string());
35
+
36
+ let fetched_repeat = if repeat {
37
+ true
38
+ } else {
39
+ config
40
+ .as_ref()
41
+ .and_then(|c| c.defaults.repeat)
42
+ .unwrap_or(false)
43
+ };
44
+
45
+ if entry_path.is_empty() || output_path.is_empty() {
46
+ logger.log_message(LogLevel::Error, "Entry or output path not specified.");
47
+ std::process::exit(1);
48
+ }
49
+
50
+ let entry_file = find_entry_file(&entry_path).unwrap_or_else(|| {
51
+ logger.log_message(LogLevel::Error, "index.deva not found");
52
+ std::process::exit(1);
53
+ });
54
+
55
+ let audio_file = format!("{}/audio/index.wav", normalize_path(&output_path));
56
+ let mut audio_player = AudioPlayer::new();
57
+
58
+ if watch && fetched_repeat {
59
+ logger.log_message(
60
+ LogLevel::Error,
61
+ "Watch and repeat cannot be used together. Use repeat instead."
62
+ );
63
+ std::process::exit(1);
64
+ }
65
+
66
+ if watch {
67
+ let (tx, rx) = channel::<()>();
68
+
69
+ // Thread 1 : Watcher sending changes
70
+ let entry_clone = entry_path.clone();
71
+ thread::spawn(move || {
72
+ let _ = watch_directory(entry_clone, move || {
73
+ let _ = tx.send(()); // signal a change
74
+ });
75
+ });
76
+
77
+ // Main thread: build + play in a loop
78
+ begin_play(&config, &entry_file, &output_path);
79
+ audio_player.play_file_once(&audio_file);
80
+
81
+ logger.log_message(LogLevel::Watcher, "Watching for changes... Press Ctrl+C to exit.");
82
+
83
+ while let Ok(_) = rx.recv() {
84
+ logger.log_message(LogLevel::Watcher, "Change detected, rebuilding...");
85
+
86
+ begin_play(&config, &entry_file, &output_path);
87
+
88
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
89
+
90
+ audio_player.play_file_once(&audio_file);
91
+ }
92
+ } else if fetched_repeat {
93
+ // Initial build to start from a clean slate
94
+ begin_play(&config, &entry_file, &output_path);
95
+
96
+ logger.log_message(LogLevel::Info, "🎵 Playback started (repeat mode)...");
97
+
98
+ let mut last_snapshot = snapshot_files(&entry_path);
99
+ let mut audio_player = AudioPlayer::new();
100
+ audio_player.play_file_once(&audio_file);
101
+
102
+ loop {
103
+ let current_snapshot = snapshot_files(&entry_path);
104
+ let has_changed = files_changed(&last_snapshot, &current_snapshot);
105
+
106
+ if has_changed {
107
+ logger.log_message(LogLevel::Info, "Change detected, rebuilding in background...");
108
+ let entry_file = entry_file.clone();
109
+ let output_path = output_path.clone();
110
+ let config_clone = config.clone();
111
+
112
+ // Rebuild in a separate thread
113
+ std::thread::spawn(move || {
114
+ begin_play(&config_clone, &entry_file, &output_path);
115
+ });
116
+
117
+ last_snapshot = current_snapshot;
118
+ }
119
+
120
+ // Wait for the audio to finish without blocking the current playback
121
+ audio_player.wait_until_end();
122
+
123
+ // Then replay the audio (rebuilt or not)
124
+ audio_player.play_file_once(&audio_file);
125
+ }
126
+ } else {
127
+ // Single execution
128
+ begin_play(&config, &entry_file, &output_path);
129
+
130
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
131
+
132
+ audio_player.play_file_once(&audio_file);
133
+ audio_player.wait_until_end();
134
+ }
135
+ }
136
+
137
+ fn begin_play(config: &Option<Config>, entry_file: &str, output: &str) {
138
+ let spinner = with_spinner("Building...", || {
139
+ thread::sleep(Duration::from_millis(800));
140
+ });
141
+
142
+ let normalized_entry = normalize_path(entry_file);
143
+ let normalized_output_dir = normalize_path(&output);
144
+
145
+ let duration = std::time::Instant::now();
146
+ let mut global_store = GlobalStore::new();
147
+ let loader = ModuleLoader::new(&normalized_entry, &normalized_output_dir);
148
+ let (modules_tokens, modules_statements) = loader.load_all_modules(&mut global_store);
149
+
150
+ // SECTION Write logs
151
+ write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
152
+ write_preprocessor_log_file(
153
+ &normalized_output_dir,
154
+ "resolved_statements.log",
155
+ modules_statements.clone()
156
+ );
157
+
158
+ // SECTION Building AST and Audio
159
+ let builder = Builder::new();
160
+ builder.build_ast(&modules_statements, &output);
161
+ builder.build_audio(&modules_statements, &output, &mut global_store);
162
+
163
+ // SECTION Logging
164
+ let logger = Logger::new();
165
+ let success_message = format!(
166
+ "Build completed successfully in {:.2?}. Output files written to: '{}'",
167
+ duration.elapsed(),
168
+ normalized_output_dir
169
+ );
170
+
171
+ logger.log_message(LogLevel::Success, &success_message);
172
+ }
173
+
174
+ fn snapshot_files<P: AsRef<Path>>(dir: P) -> HashMap<String, u64> {
175
+ let mut map = HashMap::new();
176
+ if let Ok(entries) = fs::read_dir(dir) {
177
+ for entry in entries.flatten() {
178
+ if let Ok(meta) = entry.metadata() {
179
+ if let Ok(mtime) = meta.modified() {
180
+ if let Ok(duration) = mtime.duration_since(std::time::UNIX_EPOCH) {
181
+ map.insert(entry.path().display().to_string(), duration.as_secs());
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
187
+ map
188
+ }
189
+
190
+ fn files_changed(old: &HashMap<String, u64>, new: &HashMap<String, u64>) -> bool {
191
+ old != new
192
+ }
@@ -1,9 +1,9 @@
1
1
  use include_dir::{ include_dir, Dir, DirEntry };
2
-
3
2
  use crate::utils::file::format_file_size;
4
3
 
5
4
  static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
6
5
 
6
+ #[cfg(feature = "cli")]
7
7
  pub fn handle_template_list_command() {
8
8
  let available_templates = get_available_templates();
9
9
 
@@ -16,6 +16,7 @@ pub fn handle_template_list_command() {
16
16
  println!("\nUsage : devalang init --name <project-name> --template <template-name>");
17
17
  }
18
18
 
19
+ #[cfg(feature = "cli")]
19
20
  pub fn handle_template_info_command(name: String) {
20
21
  let template_dir = TEMPLATES_DIR.get_dir(name.clone()).unwrap_or_else(|| {
21
22
  println!("❌ The template '{}' is not found.", name);
@@ -1,5 +1,4 @@
1
1
  use std::{ fs, path::Path };
2
-
3
2
  use crate::config::Config;
4
3
 
5
4
  pub fn load_config(path: Option<&Path>) -> Option<Config> {
@@ -2,14 +2,15 @@ pub mod loader;
2
2
 
3
3
  use serde::Deserialize;
4
4
 
5
- #[derive(Debug, Deserialize)]
5
+ #[derive(Debug, Deserialize, Clone)]
6
6
  pub struct Config {
7
7
  pub defaults: ConfigDefaults,
8
8
  }
9
9
 
10
- #[derive(Debug, Deserialize)]
10
+ #[derive(Debug, Deserialize, Clone)]
11
11
  pub struct ConfigDefaults {
12
12
  pub entry: Option<String>,
13
13
  pub output: Option<String>,
14
14
  pub watch: Option<bool>,
15
+ pub repeat: Option<bool>,
15
16
  }