rea_rs/
simple_functions.rs

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    /// Show message in console.
21    ///
22    /// # Note
23    ///
24    /// `\n` will be added in the end.
25    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    /// Run action by it's command id.
39    ///
40    /// # Note
41    ///
42    /// It seems, that flag should be always 0.
43    /// If project is None — will perform on current project.
44    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                &current
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    /// Get project from the current tab.
69    pub fn current_project(&self) -> Project {
70        Project::new(ProjectContext::CurrentProject)
71    }
72
73    /// Open new project tab.
74    ///
75    /// To open project in new tab use [Reaper::open_project]
76    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    /// Open project from the filename.
93    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    /// Add reascript from file and put to the action list.
118    ///
119    /// commit must be used in the last call,
120    /// but it is faster to make it false in a bulk.
121    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    /// Remove reascript.
133    ///
134    /// commit must be used in the last call,
135    /// but it is faster to make it false in a bulk.
136    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    /// Ask user to select a file.
177    ///
178    /// extension — extension for file, e.g. "mp3", "txt". Or empty string.
179    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    /// Arm or disarm command.
202    ///
203    /// # Original doc
204    ///
205    /// arms a command (or disarms if 0 passed) in section
206    /// (empty string for main)
207    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    /// Get armed command.
221    ///
222    /// If string is empty (`len() = 0`), then it's main section.
223    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    /// Reset global peak cache.
238    pub fn clear_peak_cache(&self) {
239        self.low().ClearPeakCache()
240    }
241
242    // TODO: db to slider?
243
244    /// Get ID for action with the given name.
245    ///
246    /// # Note
247    ///
248    /// name is the ID string, that was made, when registered as action,
249    /// but not the description line.
250    ///
251    /// If action name doesn't start with underscore, it will be added.
252    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            // debug!("action name: {:?}", name);
262            let id = self.low().NamedCommandLookup(as_mut_i8(name.as_str()));
263            // debug!("got action id: {:?}", id);
264            match id {
265                x if x <= 0 => None,
266                _ => Some(CommandId::new(id as u32)),
267            }
268        }
269    }
270
271    /// Get action name (string ID) of an action with the given ID.
272    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        // debug!("received result: {:?}", result);
276        match result.is_null() {
277            true => None,
278            false => Some(as_string(result).unwrap()),
279        }
280    }
281
282    /// Return REAPER bin directory (e.g. "C:\\Program Files\\REAPER").
283    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    /// Get globally overrided automation mode.
289    ///
290    /// None if do not overrides.
291    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    /// Override global automation mode.
302    pub fn set_global_automation_mode(&self, mode: AutomationMode) {
303        self.low().SetGlobalAutomationOverride(mode.int_value());
304    }
305
306    /// Show text inputs to user and get values from them.
307    ///
308    /// # Note
309    ///
310    /// default buf size is 1024
311    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    /// Call function while freezing the UI.
344    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    /// Call function in undo block with given name.
351    ///
352    /// # Note
353    ///
354    /// Probably, it's better to use `UndoFlags.all()`
355    /// by default.
356    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 undo_name = ;
375            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    /// Show message box to user and get result.
391    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    /// Redraw the arrange view.
409    pub fn update_arrange(&self) {
410        self.low().UpdateArrange();
411    }
412
413    /// Redraw timeline.
414    pub fn update_timeline(&self) {
415        self.low().UpdateTimeline();
416    }
417
418    /// Open preferences window.
419    ///
420    /// page should be positive or None.
421    ///
422    /// if not page — then name will be used.
423    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    /// Iter through all opened projects.
436    ///
437    /// # Warning
438    ///
439    /// This operation, probably, of O(n²) complexity.
440    /// So, it's better not to use it in loop or too often.
441    pub fn iter_projects<'a>(&self) -> ProjectIterator {
442        ProjectIterator::new(*self.low())
443    }
444
445    /// Checks if the given pointer is still valid.
446    ///
447    /// Returns true if the pointer is a valid object
448    /// of the correct type in the current project.
449    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    /// Checks if the given pointer is still valid.
460    ///
461    /// # Example
462    ///
463    /// ```no_run
464    /// use rea_rs::{Reaper, ProjectContext, WithReaperPtr};
465    /// let rpr = Reaper::get();
466    /// let pr = rpr.current_project();
467    /// let track = pr.get_track(0).ok_or("No track")?;
468    /// let track_is_valid = rpr.validate_ptr_2(&pr, track.get_pointer());
469    /// assert!(track_is_valid);
470    /// # Ok::<_, Box<dyn std::error::Error>>(())
471    /// ```
472    ///
473    /// Returns `true` if the pointer is a valid object of the
474    /// correct type in the given project.
475    /// The project is ignored if the pointer itself is a project.
476    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
500/// Iterates through all opened projects.
501///
502/// Should be created by [`Reaper::iter_projects()`]
503pub 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}