@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.
- package/.devalang +1 -1
- package/Cargo.toml +9 -7
- package/README.md +49 -25
- package/docs/CHANGELOG.md +30 -0
- package/docs/COMMANDS.md +31 -0
- package/docs/CONFIG.md +6 -4
- package/docs/ROADMAP.md +4 -4
- package/docs/TODO.md +4 -4
- package/examples/index.deva +9 -2
- package/examples/samples/hat-808.wav +0 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +44 -42
- package/project-version.json +6 -6
- package/rust/audio/engine.rs +126 -0
- package/rust/audio/interpreter.rs +143 -0
- package/rust/audio/loader.rs +46 -0
- package/rust/audio/mod.rs +5 -0
- package/rust/audio/player.rs +54 -0
- package/rust/audio/render.rs +57 -0
- package/rust/cli/build.rs +17 -5
- package/rust/cli/check.rs +2 -1
- package/rust/cli/init.rs +4 -2
- package/rust/cli/mod.rs +29 -0
- package/rust/cli/play.rs +192 -0
- package/rust/cli/template.rs +2 -1
- package/rust/config/loader.rs +0 -1
- package/rust/config/mod.rs +3 -2
- package/rust/core/builder/mod.rs +54 -6
- package/rust/core/debugger/lexer.rs +20 -5
- package/rust/core/debugger/preprocessor.rs +9 -5
- package/rust/core/lexer/handler/mod.rs +2 -2
- package/rust/core/lexer/handler/newline.rs +5 -1
- package/rust/core/lexer/mod.rs +10 -5
- package/rust/core/parser/handler/loop_.rs +11 -0
- package/rust/core/parser/mod.rs +0 -1
- package/rust/core/preprocessor/loader.rs +89 -16
- package/rust/core/preprocessor/module.rs +2 -0
- package/rust/core/preprocessor/resolver/bank.rs +46 -0
- package/rust/core/preprocessor/resolver/loop_.rs +148 -0
- package/rust/core/preprocessor/resolver/mod.rs +151 -0
- package/rust/core/preprocessor/resolver/tempo.rs +49 -0
- package/rust/core/preprocessor/resolver/trigger.rs +114 -0
- package/rust/lib.rs +118 -0
- package/rust/main.rs +8 -0
- package/rust/utils/logger.rs +45 -6
- package/rust/utils/spinner.rs +2 -0
- package/rust/utils/watcher.rs +10 -2
- package/templates/minimal/.devalang +2 -1
- package/templates/minimal/README.md +202 -0
- package/templates/welcome/.devalang +2 -1
- package/templates/welcome/README.md +48 -31
- 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,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
|
|
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
|
|
118
|
+
// SECTION Building AST and Audio
|
|
108
119
|
let builder = Builder::new();
|
|
109
|
-
builder.build_ast(&
|
|
120
|
+
builder.build_ast(&modules_statements, &normalized_output_dir);
|
|
121
|
+
builder.build_audio(&modules_statements, &normalized_output_dir, &mut global_store);
|
|
110
122
|
|
|
111
|
-
//
|
|
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.
|
|
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
|
}
|
package/rust/cli/play.rs
ADDED
|
@@ -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, ¤t_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
|
+
}
|
package/rust/cli/template.rs
CHANGED
|
@@ -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);
|
package/rust/config/loader.rs
CHANGED
package/rust/config/mod.rs
CHANGED
|
@@ -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
|
}
|