1use crate::{
2 errors::{ReaperError, ReaperResult},
3 misc_enums::ProjectContext,
4 ptr_wrappers::Hwnd,
5 reaper_pointer::ReaperPointer,
6 utils::{
7 as_c_char, as_c_str, as_mut_i8, as_string, make_string_buf, WithNull,
8 },
9 AutomationMode, CommandId, MIDIEditor, MessageBoxType, MessageBoxValue,
10 Project, Reaper, Section, UndoFlags,
11};
12use int_enum::IntEnum;
13use log::debug;
14use std::{
15 collections::HashMap, error::Error, ffi::CString, fs::canonicalize,
16 marker::PhantomData, path::Path, ptr::NonNull,
17};
18
19impl Reaper {
20 pub fn show_console_msg(&self, msg: impl Into<String>) {
26 let mut msg: String = msg.into();
27 msg.push_str("\n");
28 unsafe {
29 self.low()
30 .ShowConsoleMsg(as_c_str(msg.with_null()).as_ptr())
31 };
32 }
33
34 pub fn clear_console(&self) {
35 self.low().ClearConsole()
36 }
37
38 pub fn perform_action(
45 &self,
46 action_id: impl Into<CommandId>,
47 flag: i32,
48 project: Option<&Project>,
49 ) {
50 let action_id = action_id.into();
51 let current: Project;
52 let project = match project {
53 None => {
54 current = self.current_project();
55 ¤t
56 }
57 Some(pr) => pr,
58 };
59 unsafe {
60 self.low().Main_OnCommandEx(
61 action_id.get() as i32,
62 flag,
63 project.context().to_raw(),
64 )
65 }
66 }
67
68 pub fn current_project(&self) -> Project {
70 Project::new(ProjectContext::CurrentProject)
71 }
72
73 pub fn add_project_tab(&self, make_current_project: bool) -> Project {
77 match make_current_project {
78 false => {
79 let current_project =
80 Project::new(ProjectContext::CurrentProject);
81 let project = self.add_project_tab(true);
82 current_project.make_current_project();
83 project
84 }
85 true => {
86 self.perform_action(CommandId::new(40859), 0, None);
87 self.current_project()
88 }
89 }
90 }
91
92 pub fn open_project(
94 &self,
95 file: &Path,
96 in_new_tab: bool,
97 make_current_project: bool,
98 ) -> Result<Project, &str> {
99 let current_project = self.current_project();
100 if in_new_tab {
101 self.add_project_tab(true);
102 }
103 if !file.is_file() {
104 return Err("path is not file");
105 }
106 let path = file.to_str().ok_or("can not use this path")?;
107 unsafe {
108 self.low().Main_openProject(as_mut_i8(path));
109 }
110 let project = self.current_project();
111 if !make_current_project {
112 current_project.make_current_project();
113 }
114 Ok(project)
115 }
116
117 pub fn add_reascript(
122 &self,
123 file: &Path,
124 section: Section,
125 commit: bool,
126 ) -> Result<CommandId, Box<dyn Error>> {
127 Ok(self
128 .add_remove_reascript(file, section, commit, true)?
129 .expect("should hold CommandId"))
130 }
131
132 pub fn remove_reascript(
137 &self,
138 file: &Path,
139 section: Section,
140 commit: bool,
141 ) -> Result<(), Box<dyn Error>> {
142 self.add_remove_reascript(file, section, commit, false)?;
143 Ok(())
144 }
145
146 fn add_remove_reascript(
147 &self,
148 file: &Path,
149 section: Section,
150 commit: bool,
151 add: bool,
152 ) -> ReaperResult<Option<CommandId>> {
153 if !file.is_file() {
154 return Err("path is not file!".into());
155 }
156 let abs = canonicalize(file)?;
157 unsafe {
158 let id = self.low().AddRemoveReaScript(
159 add,
160 section.id() as i32,
161 as_mut_i8(abs.to_str().ok_or("can not resolve path")?),
162 commit,
163 );
164 if id <= 0 {
165 return Err(Box::new(ReaperError::Str(
166 "Failed to add or remove reascript.",
167 )));
168 }
169 match add {
170 true => Ok(Some(CommandId::new(id as u32))),
171 false => Ok(None),
172 }
173 }
174 }
175
176 pub fn browse_for_file(
180 &self,
181 window_title: impl Into<String>,
182 extension: impl Into<String>,
183 ) -> Result<Box<Path>, Box<dyn Error>> {
184 unsafe {
185 let buf = make_string_buf(4096);
186 let result = self.low().GetUserFileNameForRead(
187 buf,
188 as_mut_i8(window_title.into().as_str()),
189 as_mut_i8(extension.into().as_str()),
190 );
191 match result {
192 false => Err(Box::new(ReaperError::UserAborted)),
193 true => {
194 let filename = CString::from_raw(buf).into_string()?;
195 Ok(Path::new(&filename).into())
196 }
197 }
198 }
199 }
200
201 pub fn arm_command(&self, command: CommandId, section: impl Into<String>) {
208 unsafe {
209 self.low().ArmCommand(
210 command.get() as i32,
211 as_mut_i8(section.into().as_str()),
212 )
213 }
214 }
215
216 pub fn disarm_command(&self) {
217 self.arm_command(CommandId::new(0), "");
218 }
219
220 pub fn armed_command(&self) -> Option<(CommandId, String)> {
224 unsafe {
225 let buf = make_string_buf(200);
226 let id = self.low().GetArmedCommand(buf, 200);
227 let result = CString::from_raw(buf)
228 .into_string()
229 .unwrap_or(String::from(""));
230 match id {
231 0 => None,
232 _ => Some((CommandId::new(id as u32), String::from(result))),
233 }
234 }
235 }
236
237 pub fn clear_peak_cache(&self) {
239 self.low().ClearPeakCache()
240 }
241
242 pub fn get_action_id(
253 &self,
254 action_name: impl Into<String>,
255 ) -> Option<CommandId> {
256 unsafe {
257 let mut name: String = action_name.into();
258 if !name.starts_with("_") {
259 name = String::from("_") + &name;
260 }
261 let id = self.low().NamedCommandLookup(as_mut_i8(name.as_str()));
263 match id {
265 x if x <= 0 => None,
266 _ => Some(CommandId::new(id as u32)),
267 }
268 }
269 }
270
271 pub fn get_action_name(&self, id: CommandId) -> Option<String> {
273 debug!("get action name");
274 let result = self.low().ReverseNamedCommandLookup(id.get() as i32);
275 match result.is_null() {
277 true => None,
278 false => Some(as_string(result).unwrap()),
279 }
280 }
281
282 pub fn get_binary_directory(&self) -> String {
284 let result = self.low().GetExePath();
285 as_string(result).expect("Can not convert result to string.")
286 }
287
288 pub fn get_global_automation_mode(&self) -> Option<AutomationMode> {
292 let result = self.low().GetGlobalAutomationOverride();
293 let mode =
294 AutomationMode::from_int(result).expect("should convert to enum.");
295 match mode {
296 AutomationMode::None => None,
297 _ => Some(mode),
298 }
299 }
300
301 pub fn set_global_automation_mode(&self, mode: AutomationMode) {
303 self.low().SetGlobalAutomationOverride(mode.int_value());
304 }
305
306 pub fn get_user_inputs<'a>(
312 &self,
313 title: impl Into<String>,
314 captions: Vec<&'a str>,
315 buf_size: impl Into<Option<usize>>,
316 ) -> ReaperResult<HashMap<String, String>> {
317 unsafe {
318 let buf_size = match buf_size.into() {
319 None => 1024,
320 Some(sz) => sz,
321 };
322 let buf = make_string_buf(buf_size);
323 let result = self.low().GetUserInputs(
324 as_c_char(title.into().as_str()),
325 captions.len() as i32,
326 as_mut_i8(captions.join(",").as_str()),
327 buf,
328 buf_size as i32,
329 );
330 if result == false {
331 return Err(Box::new(ReaperError::UserAborted));
332 }
333 let mut map = HashMap::new();
334 let values =
335 as_string(buf).expect("can not retrieve user inputs.");
336 for (key, val) in captions.into_iter().zip(values.split(",")) {
337 map.insert(String::from(key), String::from(val));
338 }
339 Ok(map)
340 }
341 }
342
343 pub fn with_prevent_ui_refresh(&self, f: impl Fn()) {
345 self.low().PreventUIRefresh(1);
346 (f)();
347 self.low().PreventUIRefresh(-1);
348 }
349
350 pub fn with_undo_block(
357 &self,
358 undo_name: impl Into<String>,
359 flags: UndoFlags,
360 project: Option<&Project>,
361 mut f: impl FnMut() -> ReaperResult<()>,
362 ) -> ReaperResult<()> {
363 let low = self.low();
364 let undo_name: String = undo_name.into();
365 match project {
366 None => low.Undo_BeginBlock(),
367 Some(pr) => unsafe {
368 low.Undo_BeginBlock2(pr.context().to_raw());
369 },
370 }
371
372 (f)()?;
373 unsafe {
374 let flags = flags.bits() as i32;
376 match project {
377 None => {
378 low.Undo_EndBlock(as_c_char(undo_name.as_str()), flags);
379 }
380 Some(pr) => low.Undo_EndBlock2(
381 pr.context().to_raw(),
382 as_c_char(undo_name.as_str()),
383 flags,
384 ),
385 }
386 }
387 Ok(())
388 }
389
390 pub fn show_message_box(
392 &self,
393 title: impl Into<String>,
394 text: impl Into<String>,
395 box_type: MessageBoxType,
396 ) -> ReaperResult<MessageBoxValue> {
397 unsafe {
398 let low = self.low();
399 let status = low.ShowMessageBox(
400 as_mut_i8(text.into().as_str()),
401 as_mut_i8(title.into().as_str()),
402 box_type.int_value(),
403 );
404 Ok(MessageBoxValue::from_int(status)?)
405 }
406 }
407
408 pub fn update_arrange(&self) {
410 self.low().UpdateArrange();
411 }
412
413 pub fn update_timeline(&self) {
415 self.low().UpdateTimeline();
416 }
417
418 pub fn view_prefs(
424 &self,
425 page: impl Into<Option<u32>>,
426 name: impl Into<Option<String>>,
427 ) {
428 let name = name.into().unwrap_or(String::from(""));
429 let page = page.into().unwrap_or(0_u32);
430 unsafe {
431 self.low().ViewPrefs(page as i32, as_c_char(name.as_str()));
432 }
433 }
434
435 pub fn iter_projects<'a>(&self) -> ProjectIterator {
442 ProjectIterator::new(*self.low())
443 }
444
445 pub fn validate_ptr<'a>(&self, pointer: impl Into<ReaperPointer>) -> bool {
450 let pointer: ReaperPointer = pointer.into();
451 unsafe {
452 self.low().ValidatePtr(
453 pointer.ptr_as_void(),
454 pointer.key_into_raw().as_ptr(),
455 )
456 }
457 }
458
459 pub fn validate_ptr_2<'a>(
477 &self,
478 project: &Project,
479 pointer: impl Into<ReaperPointer>,
480 ) -> bool {
481 let pointer: ReaperPointer = pointer.into();
482 unsafe {
483 self.low().ValidatePtr2(
484 project.context().to_raw(),
485 pointer.ptr_as_void(),
486 pointer.key_into_raw().as_ptr(),
487 )
488 }
489 }
490
491 pub fn active_midi_editor(&self) -> Option<MIDIEditor> {
492 let hwnd = self.low().MIDIEditor_GetActive();
493 match Hwnd::new(hwnd) {
494 None => None,
495 Some(ptr) => Some(MIDIEditor::new(ptr)),
496 }
497 }
498}
499
500pub struct ProjectIterator {
504 low: rea_rs_low::Reaper,
505 index: i32,
506 phantom: PhantomData<Project>,
507}
508impl ProjectIterator {
509 fn new(low: rea_rs_low::Reaper) -> Self {
510 Self {
511 low,
512 index: 0,
513 phantom: PhantomData::default(),
514 }
515 }
516}
517impl Iterator for ProjectIterator {
518 type Item = Project;
519 fn next(&mut self) -> Option<Self::Item> {
520 unsafe {
521 let raw = self.low.EnumProjects(self.index, as_mut_i8(""), 0);
522 let raw = NonNull::new(raw);
523 self.index += 1;
524 match raw {
525 None => None,
526 Some(raw) => Some(Project::new(ProjectContext::Proj(raw))),
527 }
528 }
529 }
530}