Skip to main content

maolan_engine/
history.rs

1use crate::{
2    audio::io::AudioIO,
3    kind::Kind,
4    message::{Action, ClipMoveFrom, ClipMoveTo},
5    midi::io::MIDIIO,
6    state::State,
7};
8use std::collections::VecDeque;
9use std::sync::Arc;
10
11#[derive(Clone, Debug)]
12pub struct UndoEntry {
13    pub forward_actions: Vec<Action>,
14    pub inverse_actions: Vec<Action>,
15}
16
17pub struct History {
18    undo_stack: VecDeque<UndoEntry>,
19    redo_stack: VecDeque<UndoEntry>,
20    max_history: usize,
21}
22
23impl History {
24    pub fn new(max_history: usize) -> Self {
25        Self {
26            undo_stack: VecDeque::new(),
27            redo_stack: VecDeque::new(),
28            max_history,
29        }
30    }
31
32    pub fn record(&mut self, entry: UndoEntry) {
33        self.undo_stack.push_back(entry);
34        self.redo_stack.clear(); // Clear redo stack on new action
35
36        // Limit history size
37        if self.undo_stack.len() > self.max_history {
38            self.undo_stack.pop_front();
39        }
40    }
41
42    pub fn undo(&mut self) -> Option<Vec<Action>> {
43        self.undo_stack.pop_back().map(|entry| {
44            let inverse = entry.inverse_actions.clone();
45            self.redo_stack.push_back(entry);
46            inverse
47        })
48    }
49
50    pub fn redo(&mut self) -> Option<Vec<Action>> {
51        self.redo_stack.pop_back().map(|entry| {
52            let forward = entry.forward_actions.clone();
53            self.undo_stack.push_back(entry);
54            forward
55        })
56    }
57
58    pub fn clear(&mut self) {
59        self.undo_stack.clear();
60        self.redo_stack.clear();
61    }
62}
63
64impl Default for History {
65    fn default() -> Self {
66        Self::new(100)
67    }
68}
69
70/// Check if an action should be recorded in history
71pub fn should_record(action: &Action) -> bool {
72    matches!(
73        action,
74        Action::SetTempo(_)
75            | Action::SetLoopEnabled(_)
76            | Action::SetLoopRange(_)
77            | Action::SetPunchEnabled(_)
78            | Action::SetPunchRange(_)
79            | Action::SetMetronomeEnabled(_)
80            | Action::SetTimeSignature { .. }
81            | Action::AddTrack { .. }
82            | Action::RemoveTrack(_)
83            | Action::RenameTrack { .. }
84            | Action::TrackLevel(_, _)
85            | Action::TrackBalance(_, _)
86            | Action::TrackToggleArm(_)
87            | Action::TrackToggleMute(_)
88            | Action::TrackToggleSolo(_)
89            | Action::TrackToggleInputMonitor(_)
90            | Action::TrackToggleDiskMonitor(_)
91            | Action::TrackSetMidiLearnBinding { .. }
92            | Action::SetGlobalMidiLearnBinding { .. }
93            | Action::TrackSetVcaMaster { .. }
94            | Action::TrackSetFrozen { .. }
95            | Action::TrackAddAudioInput(_)
96            | Action::TrackAddAudioOutput(_)
97            | Action::TrackRemoveAudioInput(_)
98            | Action::TrackRemoveAudioOutput(_)
99            | Action::AddClip { .. }
100            | Action::RemoveClip { .. }
101            | Action::RenameClip { .. }
102            | Action::ClipMove { .. }
103            | Action::SetClipFade { .. }
104            | Action::SetClipBounds { .. }
105            | Action::SetClipMuted { .. }
106            | Action::SetAudioClipWarpMarkers { .. }
107            | Action::ClearAllMidiLearnBindings
108            | Action::Connect { .. }
109            | Action::Disconnect { .. }
110            | Action::TrackConnectVst3Audio { .. }
111            | Action::TrackDisconnectVst3Audio { .. }
112            | Action::TrackConnectPluginAudio { .. }
113            | Action::TrackDisconnectPluginAudio { .. }
114            | Action::TrackConnectPluginMidi { .. }
115            | Action::TrackDisconnectPluginMidi { .. }
116            | Action::TrackLoadClapPlugin { .. }
117            | Action::TrackUnloadClapPlugin { .. }
118            | Action::TrackLoadLv2Plugin { .. }
119            | Action::TrackUnloadLv2PluginInstance { .. }
120            | Action::TrackLoadVst3Plugin { .. }
121            | Action::TrackUnloadVst3PluginInstance { .. }
122            | Action::TrackSetLv2ControlValue { .. }
123            | Action::TrackSetClapParameter { .. }
124            | Action::TrackSetVst3Parameter { .. }
125            | Action::ModifyMidiNotes { .. }
126            | Action::ModifyMidiControllers { .. }
127            | Action::DeleteMidiControllers { .. }
128            | Action::InsertMidiControllers { .. }
129            | Action::DeleteMidiNotes { .. }
130            | Action::InsertMidiNotes { .. }
131            | Action::SetMidiSysExEvents { .. }
132    )
133}
134
135/// Create an inverse action that will undo the given action
136/// Returns None if the action cannot be inverted
137pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
138    match action {
139        Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
140
141        Action::RemoveTrack(name) => {
142            // Find the track to capture its data
143            let track = state.tracks.get(name)?;
144            let track_lock = track.lock();
145            Some(Action::AddTrack {
146                name: track_lock.name.clone(),
147                audio_ins: track_lock.primary_audio_ins(),
148                midi_ins: track_lock.midi.ins.len(),
149                audio_outs: track_lock.primary_audio_outs(),
150                midi_outs: track_lock.midi.outs.len(),
151            })
152        }
153
154        Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
155            old_name: new_name.clone(),
156            new_name: old_name.clone(),
157        }),
158
159        Action::TrackLevel(name, _new_level) => {
160            // Find current level
161            let track = state.tracks.get(name)?;
162            let track_lock = track.lock();
163            Some(Action::TrackLevel(name.clone(), track_lock.level))
164        }
165
166        Action::TrackBalance(name, _new_balance) => {
167            // Find current balance
168            let track = state.tracks.get(name)?;
169            let track_lock = track.lock();
170            Some(Action::TrackBalance(name.clone(), track_lock.balance))
171        }
172
173        Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
174        Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
175        Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
176        Action::TrackToggleInputMonitor(name) => {
177            Some(Action::TrackToggleInputMonitor(name.clone()))
178        }
179        Action::TrackToggleDiskMonitor(name) => Some(Action::TrackToggleDiskMonitor(name.clone())),
180        Action::TrackSetMidiLearnBinding {
181            track_name, target, ..
182        } => {
183            let track = state.tracks.get(track_name)?;
184            let track_lock = track.lock();
185            let binding = match target {
186                crate::message::TrackMidiLearnTarget::Volume => {
187                    track_lock.midi_learn_volume.clone()
188                }
189                crate::message::TrackMidiLearnTarget::Balance => {
190                    track_lock.midi_learn_balance.clone()
191                }
192                crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
193                crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
194                crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
195                crate::message::TrackMidiLearnTarget::InputMonitor => {
196                    track_lock.midi_learn_input_monitor.clone()
197                }
198                crate::message::TrackMidiLearnTarget::DiskMonitor => {
199                    track_lock.midi_learn_disk_monitor.clone()
200                }
201            };
202            Some(Action::TrackSetMidiLearnBinding {
203                track_name: track_name.clone(),
204                target: *target,
205                binding,
206            })
207        }
208        Action::TrackSetVcaMaster { track_name, .. } => {
209            let track = state.tracks.get(track_name)?;
210            let track_lock = track.lock();
211            Some(Action::TrackSetVcaMaster {
212                track_name: track_name.clone(),
213                master_track: track_lock.vca_master(),
214            })
215        }
216        Action::TrackSetFrozen { track_name, .. } => {
217            let track = state.tracks.get(track_name)?;
218            let track_lock = track.lock();
219            Some(Action::TrackSetFrozen {
220                track_name: track_name.clone(),
221                frozen: track_lock.frozen(),
222            })
223        }
224        Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
225        Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
226        Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
227        Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
228
229        Action::AddClip {
230            track_name, kind, ..
231        } => {
232            // To undo adding a clip, we need to know which index it will have
233            let track = state.tracks.get(track_name)?;
234            let track_lock = track.lock();
235            let clip_index = match kind {
236                Kind::Audio => track_lock.audio.clips.len(),
237                Kind::MIDI => track_lock.midi.clips.len(),
238            };
239            Some(Action::RemoveClip {
240                track_name: track_name.clone(),
241                kind: *kind,
242                clip_indices: vec![clip_index],
243            })
244        }
245
246        Action::RemoveClip {
247            track_name,
248            kind,
249            clip_indices,
250        } => {
251            // To undo removing clips, we need to capture their data
252            let track = state.tracks.get(track_name)?;
253            let track_lock = track.lock();
254
255            // For now, we only support undoing single clip removal
256            if clip_indices.len() != 1 {
257                return None;
258            }
259
260            let clip_idx = clip_indices[0];
261            match kind {
262                Kind::Audio => {
263                    let clip = track_lock.audio.clips.get(clip_idx)?;
264                    let length = clip.end.saturating_sub(clip.start);
265                    Some(Action::AddClip {
266                        name: clip.name.clone(),
267                        track_name: track_name.clone(),
268                        start: clip.start,
269                        length,
270                        offset: clip.offset,
271                        input_channel: clip.input_channel,
272                        muted: clip.muted,
273                        kind: Kind::Audio,
274                        fade_enabled: clip.fade_enabled,
275                        fade_in_samples: clip.fade_in_samples,
276                        fade_out_samples: clip.fade_out_samples,
277                        warp_markers: clip.warp_markers.clone(),
278                    })
279                }
280                Kind::MIDI => {
281                    let clip = track_lock.midi.clips.get(clip_idx)?;
282                    let length = clip.end.saturating_sub(clip.start);
283                    Some(Action::AddClip {
284                        name: clip.name.clone(),
285                        track_name: track_name.clone(),
286                        start: clip.start,
287                        length,
288                        offset: clip.offset,
289                        input_channel: clip.input_channel,
290                        muted: clip.muted,
291                        kind: Kind::MIDI,
292                        fade_enabled: true,    // Default value for MIDI clips
293                        fade_in_samples: 240,  // Default value
294                        fade_out_samples: 240, // Default value
295                        warp_markers: vec![],
296                    })
297                }
298            }
299        }
300
301        Action::RenameClip {
302            track_name,
303            kind,
304            clip_index,
305            new_name: _,
306        } => {
307            // Find current name
308            let track = state.tracks.get(track_name)?;
309            let track_lock = track.lock();
310            let old_name = match kind {
311                Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
312                Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
313            };
314            Some(Action::RenameClip {
315                track_name: track_name.clone(),
316                kind: *kind,
317                clip_index: *clip_index,
318                new_name: old_name,
319            })
320        }
321
322        Action::ClipMove {
323            kind,
324            from,
325            to,
326            copy,
327        } => {
328            let (original_start, original_input_channel) = {
329                let source_track = state.tracks.get(&from.track_name)?;
330                let source_lock = source_track.lock();
331                match kind {
332                    Kind::Audio => {
333                        let clip = source_lock.audio.clips.get(from.clip_index)?;
334                        (clip.start, clip.input_channel)
335                    }
336                    Kind::MIDI => {
337                        let clip = source_lock.midi.clips.get(from.clip_index)?;
338                        (clip.start, clip.input_channel)
339                    }
340                }
341            };
342
343            if *copy {
344                // If it was a copy, we need to remove the newly created clip
345                let dest_track = state.tracks.get(&to.track_name)?;
346                let dest_lock = dest_track.lock();
347                let clip_idx = match kind {
348                    Kind::Audio => dest_lock.audio.clips.len(),
349                    Kind::MIDI => dest_lock.midi.clips.len(),
350                };
351                Some(Action::RemoveClip {
352                    track_name: to.track_name.clone(),
353                    kind: *kind,
354                    clip_indices: vec![clip_idx],
355                })
356            } else {
357                // If it was a move, reverse the move from the destination track.
358                let dest_track = state.tracks.get(&to.track_name)?;
359                let dest_lock = dest_track.lock();
360                let dest_len = match kind {
361                    Kind::Audio => {
362                        if dest_lock.audio.clips.is_empty() {
363                            return None;
364                        }
365                        dest_lock.audio.clips.len()
366                    }
367                    Kind::MIDI => {
368                        if dest_lock.midi.clips.is_empty() {
369                            return None;
370                        }
371                        dest_lock.midi.clips.len()
372                    }
373                };
374                let moved_clip_index = if from.track_name == to.track_name {
375                    dest_len.saturating_sub(1)
376                } else {
377                    dest_len
378                };
379                Some(Action::ClipMove {
380                    kind: *kind,
381                    from: ClipMoveFrom {
382                        track_name: to.track_name.clone(),
383                        clip_index: moved_clip_index,
384                    },
385                    to: ClipMoveTo {
386                        track_name: from.track_name.clone(),
387                        sample_offset: original_start,
388                        input_channel: original_input_channel,
389                    },
390                    copy: false,
391                })
392            }
393        }
394
395        Action::SetClipFade {
396            track_name,
397            clip_index,
398            kind,
399            ..
400        } => {
401            // Capture current fade settings
402            let track = state.tracks.get(track_name)?;
403            let track_lock = track.lock();
404            match kind {
405                Kind::Audio => {
406                    let clip = track_lock.audio.clips.get(*clip_index)?;
407                    Some(Action::SetClipFade {
408                        track_name: track_name.clone(),
409                        clip_index: *clip_index,
410                        kind: *kind,
411                        fade_enabled: clip.fade_enabled,
412                        fade_in_samples: clip.fade_in_samples,
413                        fade_out_samples: clip.fade_out_samples,
414                    })
415                }
416                Kind::MIDI => {
417                    // MIDI clips don't have fade fields in engine, use defaults
418                    Some(Action::SetClipFade {
419                        track_name: track_name.clone(),
420                        clip_index: *clip_index,
421                        kind: *kind,
422                        fade_enabled: true,
423                        fade_in_samples: 240,
424                        fade_out_samples: 240,
425                    })
426                }
427            }
428        }
429        Action::SetClipBounds {
430            track_name,
431            clip_index,
432            kind,
433            ..
434        } => {
435            let track = state.tracks.get(track_name)?;
436            let track_lock = track.lock();
437            match kind {
438                Kind::Audio => {
439                    let clip = track_lock.audio.clips.get(*clip_index)?;
440                    Some(Action::SetClipBounds {
441                        track_name: track_name.clone(),
442                        clip_index: *clip_index,
443                        kind: *kind,
444                        start: clip.start,
445                        length: clip.end.max(1),
446                        offset: clip.offset,
447                    })
448                }
449                Kind::MIDI => {
450                    let clip = track_lock.midi.clips.get(*clip_index)?;
451                    Some(Action::SetClipBounds {
452                        track_name: track_name.clone(),
453                        clip_index: *clip_index,
454                        kind: *kind,
455                        start: clip.start,
456                        length: clip.end.max(1),
457                        offset: clip.offset,
458                    })
459                }
460            }
461        }
462        Action::SetClipMuted {
463            track_name,
464            clip_index,
465            kind,
466            ..
467        } => {
468            let track = state.tracks.get(track_name)?;
469            let track_lock = track.lock();
470            let muted = match kind {
471                Kind::Audio => track_lock.audio.clips.get(*clip_index)?.muted,
472                Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.muted,
473            };
474            Some(Action::SetClipMuted {
475                track_name: track_name.clone(),
476                clip_index: *clip_index,
477                kind: *kind,
478                muted,
479            })
480        }
481        Action::SetAudioClipWarpMarkers {
482            track_name,
483            clip_index,
484            ..
485        } => {
486            let track = state.tracks.get(track_name)?;
487            let track_lock = track.lock();
488            let clip = track_lock.audio.clips.get(*clip_index)?;
489            Some(Action::SetAudioClipWarpMarkers {
490                track_name: track_name.clone(),
491                clip_index: *clip_index,
492                warp_markers: clip.warp_markers.clone(),
493            })
494        }
495
496        Action::Connect {
497            from_track,
498            from_port,
499            to_track,
500            to_port,
501            kind,
502        } => Some(Action::Disconnect {
503            from_track: from_track.clone(),
504            from_port: *from_port,
505            to_track: to_track.clone(),
506            to_port: *to_port,
507            kind: *kind,
508        }),
509
510        Action::Disconnect {
511            from_track,
512            from_port,
513            to_track,
514            to_port,
515            kind,
516        } => Some(Action::Connect {
517            from_track: from_track.clone(),
518            from_port: *from_port,
519            to_track: to_track.clone(),
520            to_port: *to_port,
521            kind: *kind,
522        }),
523        Action::TrackConnectVst3Audio {
524            track_name,
525            from_node,
526            from_port,
527            to_node,
528            to_port,
529        } => Some(Action::TrackDisconnectVst3Audio {
530            track_name: track_name.clone(),
531            from_node: from_node.clone(),
532            from_port: *from_port,
533            to_node: to_node.clone(),
534            to_port: *to_port,
535        }),
536        Action::TrackDisconnectVst3Audio {
537            track_name,
538            from_node,
539            from_port,
540            to_node,
541            to_port,
542        } => Some(Action::TrackConnectVst3Audio {
543            track_name: track_name.clone(),
544            from_node: from_node.clone(),
545            from_port: *from_port,
546            to_node: to_node.clone(),
547            to_port: *to_port,
548        }),
549        Action::TrackConnectPluginAudio {
550            track_name,
551            from_node,
552            from_port,
553            to_node,
554            to_port,
555        } => Some(Action::TrackDisconnectPluginAudio {
556            track_name: track_name.clone(),
557            from_node: from_node.clone(),
558            from_port: *from_port,
559            to_node: to_node.clone(),
560            to_port: *to_port,
561        }),
562        Action::TrackDisconnectPluginAudio {
563            track_name,
564            from_node,
565            from_port,
566            to_node,
567            to_port,
568        } => Some(Action::TrackConnectPluginAudio {
569            track_name: track_name.clone(),
570            from_node: from_node.clone(),
571            from_port: *from_port,
572            to_node: to_node.clone(),
573            to_port: *to_port,
574        }),
575        Action::TrackConnectPluginMidi {
576            track_name,
577            from_node,
578            from_port,
579            to_node,
580            to_port,
581        } => Some(Action::TrackDisconnectPluginMidi {
582            track_name: track_name.clone(),
583            from_node: from_node.clone(),
584            from_port: *from_port,
585            to_node: to_node.clone(),
586            to_port: *to_port,
587        }),
588        Action::TrackDisconnectPluginMidi {
589            track_name,
590            from_node,
591            from_port,
592            to_node,
593            to_port,
594        } => Some(Action::TrackConnectPluginMidi {
595            track_name: track_name.clone(),
596            from_node: from_node.clone(),
597            from_port: *from_port,
598            to_node: to_node.clone(),
599            to_port: *to_port,
600        }),
601
602        Action::TrackLoadClapPlugin {
603            track_name,
604            plugin_path,
605        } => Some(Action::TrackUnloadClapPlugin {
606            track_name: track_name.clone(),
607            plugin_path: plugin_path.clone(),
608        }),
609
610        Action::TrackUnloadClapPlugin {
611            track_name,
612            plugin_path,
613        } => Some(Action::TrackLoadClapPlugin {
614            track_name: track_name.clone(),
615            plugin_path: plugin_path.clone(),
616        }),
617        Action::TrackLoadLv2Plugin {
618            track_name,
619            plugin_uri: _,
620        } => {
621            let track = state.tracks.get(track_name)?;
622            let track = track.lock();
623            Some(Action::TrackUnloadLv2PluginInstance {
624                track_name: track_name.clone(),
625                instance_id: track.next_lv2_instance_id,
626            })
627        }
628        Action::TrackUnloadLv2PluginInstance {
629            track_name,
630            instance_id,
631        } => {
632            let track = state.tracks.get(track_name)?;
633            let track = track.lock();
634            let plugin_uri = track
635                .loaded_lv2_instances()
636                .into_iter()
637                .find(|(id, _)| *id == *instance_id)
638                .map(|(_, uri)| uri)?;
639            Some(Action::TrackLoadLv2Plugin {
640                track_name: track_name.clone(),
641                plugin_uri,
642            })
643        }
644        Action::TrackLoadVst3Plugin {
645            track_name,
646            plugin_path: _,
647        } => {
648            let track = state.tracks.get(track_name)?;
649            let track = track.lock();
650            Some(Action::TrackUnloadVst3PluginInstance {
651                track_name: track_name.clone(),
652                instance_id: track.next_plugin_instance_id,
653            })
654        }
655        Action::TrackUnloadVst3PluginInstance {
656            track_name,
657            instance_id,
658        } => {
659            let track = state.tracks.get(track_name)?;
660            let track = track.lock();
661            let plugin_path = track
662                .loaded_vst3_instances()
663                .into_iter()
664                .find(|(id, _, _)| *id == *instance_id)
665                .map(|(_, path, _)| path)?;
666            Some(Action::TrackLoadVst3Plugin {
667                track_name: track_name.clone(),
668                plugin_path,
669            })
670        }
671        Action::TrackSetClapParameter {
672            track_name,
673            instance_id,
674            ..
675        } => {
676            let track = state.tracks.get(track_name)?;
677            let track = track.lock();
678            let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
679            Some(Action::TrackClapRestoreState {
680                track_name: track_name.clone(),
681                instance_id: *instance_id,
682                state: snapshot,
683            })
684        }
685        Action::TrackSetVst3Parameter {
686            track_name,
687            instance_id,
688            ..
689        } => {
690            let track = state.tracks.get(track_name)?;
691            let track = track.lock();
692            let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
693            Some(Action::TrackVst3RestoreState {
694                track_name: track_name.clone(),
695                instance_id: *instance_id,
696                state: snapshot,
697            })
698        }
699        Action::TrackSetLv2ControlValue {
700            track_name,
701            instance_id,
702            ..
703        } => {
704            let track = state.tracks.get(track_name)?;
705            let track = track.lock();
706            let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
707            Some(Action::TrackSetLv2PluginState {
708                track_name: track_name.clone(),
709                instance_id: *instance_id,
710                state: snapshot,
711            })
712        }
713        Action::ModifyMidiNotes {
714            track_name,
715            clip_index,
716            note_indices,
717            new_notes,
718            old_notes,
719        } => Some(Action::ModifyMidiNotes {
720            track_name: track_name.clone(),
721            clip_index: *clip_index,
722            note_indices: note_indices.clone(),
723            new_notes: old_notes.clone(),
724            old_notes: new_notes.clone(),
725        }),
726        Action::ModifyMidiControllers {
727            track_name,
728            clip_index,
729            controller_indices,
730            new_controllers,
731            old_controllers,
732        } => Some(Action::ModifyMidiControllers {
733            track_name: track_name.clone(),
734            clip_index: *clip_index,
735            controller_indices: controller_indices.clone(),
736            new_controllers: old_controllers.clone(),
737            old_controllers: new_controllers.clone(),
738        }),
739        Action::DeleteMidiControllers {
740            track_name,
741            clip_index,
742            deleted_controllers,
743            ..
744        } => Some(Action::InsertMidiControllers {
745            track_name: track_name.clone(),
746            clip_index: *clip_index,
747            controllers: deleted_controllers.clone(),
748        }),
749        Action::InsertMidiControllers {
750            track_name,
751            clip_index,
752            controllers,
753        } => {
754            let mut controller_indices: Vec<usize> =
755                controllers.iter().map(|(idx, _)| *idx).collect();
756            controller_indices.sort_unstable_by(|a, b| b.cmp(a));
757            Some(Action::DeleteMidiControllers {
758                track_name: track_name.clone(),
759                clip_index: *clip_index,
760                controller_indices,
761                deleted_controllers: controllers.clone(),
762            })
763        }
764
765        Action::DeleteMidiNotes {
766            track_name,
767            clip_index,
768            deleted_notes,
769            ..
770        } => Some(Action::InsertMidiNotes {
771            track_name: track_name.clone(),
772            clip_index: *clip_index,
773            notes: deleted_notes.clone(),
774        }),
775
776        Action::InsertMidiNotes {
777            track_name,
778            clip_index,
779            notes,
780        } => {
781            let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
782            note_indices.sort_unstable_by(|a, b| b.cmp(a));
783            Some(Action::DeleteMidiNotes {
784                track_name: track_name.clone(),
785                clip_index: *clip_index,
786                note_indices,
787                deleted_notes: notes.clone(),
788            })
789        }
790        Action::SetMidiSysExEvents {
791            track_name,
792            clip_index,
793            new_sysex_events,
794            old_sysex_events,
795        } => Some(Action::SetMidiSysExEvents {
796            track_name: track_name.clone(),
797            clip_index: *clip_index,
798            new_sysex_events: old_sysex_events.clone(),
799            old_sysex_events: new_sysex_events.clone(),
800        }),
801
802        // These are more complex and would need additional state tracking
803        _ => None,
804    }
805}
806
807pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
808    if let Action::ClearAllMidiLearnBindings = action {
809        let mut actions = Vec::<Action>::new();
810        for (track_name, track) in &state.tracks {
811            let t = track.lock();
812            let mut push_if_some =
813                |target: crate::message::TrackMidiLearnTarget,
814                 binding: Option<crate::message::MidiLearnBinding>| {
815                    if binding.is_some() {
816                        actions.push(Action::TrackSetMidiLearnBinding {
817                            track_name: track_name.clone(),
818                            target,
819                            binding,
820                        });
821                    }
822                };
823            push_if_some(
824                crate::message::TrackMidiLearnTarget::Volume,
825                t.midi_learn_volume.clone(),
826            );
827            push_if_some(
828                crate::message::TrackMidiLearnTarget::Balance,
829                t.midi_learn_balance.clone(),
830            );
831            push_if_some(
832                crate::message::TrackMidiLearnTarget::Mute,
833                t.midi_learn_mute.clone(),
834            );
835            push_if_some(
836                crate::message::TrackMidiLearnTarget::Solo,
837                t.midi_learn_solo.clone(),
838            );
839            push_if_some(
840                crate::message::TrackMidiLearnTarget::Arm,
841                t.midi_learn_arm.clone(),
842            );
843            push_if_some(
844                crate::message::TrackMidiLearnTarget::InputMonitor,
845                t.midi_learn_input_monitor.clone(),
846            );
847            push_if_some(
848                crate::message::TrackMidiLearnTarget::DiskMonitor,
849                t.midi_learn_disk_monitor.clone(),
850            );
851        }
852        return Some(actions);
853    }
854
855    if let Action::RemoveTrack(track_name) = action {
856        let mut actions = Vec::new();
857        {
858            let track = state.tracks.get(track_name)?;
859            let track = track.lock();
860            actions.push(Action::AddTrack {
861                name: track.name.clone(),
862                audio_ins: track.primary_audio_ins(),
863                midi_ins: track.midi.ins.len(),
864                audio_outs: track.primary_audio_outs(),
865                midi_outs: track.midi.outs.len(),
866            });
867            for _ in track.primary_audio_ins()..track.audio.ins.len() {
868                actions.push(Action::TrackAddAudioInput(track.name.clone()));
869            }
870            for _ in track.primary_audio_outs()..track.audio.outs.len() {
871                actions.push(Action::TrackAddAudioOutput(track.name.clone()));
872            }
873
874            if track.level != 0.0 {
875                actions.push(Action::TrackLevel(track.name.clone(), track.level));
876            }
877            if track.balance != 0.0 {
878                actions.push(Action::TrackBalance(track.name.clone(), track.balance));
879            }
880            if track.armed {
881                actions.push(Action::TrackToggleArm(track.name.clone()));
882            }
883            if track.muted {
884                actions.push(Action::TrackToggleMute(track.name.clone()));
885            }
886            if track.soloed {
887                actions.push(Action::TrackToggleSolo(track.name.clone()));
888            }
889            if track.input_monitor {
890                actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
891            }
892            if !track.disk_monitor {
893                actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
894            }
895            if track.midi_learn_volume.is_some() {
896                actions.push(Action::TrackSetMidiLearnBinding {
897                    track_name: track.name.clone(),
898                    target: crate::message::TrackMidiLearnTarget::Volume,
899                    binding: track.midi_learn_volume.clone(),
900                });
901            }
902            if track.midi_learn_balance.is_some() {
903                actions.push(Action::TrackSetMidiLearnBinding {
904                    track_name: track.name.clone(),
905                    target: crate::message::TrackMidiLearnTarget::Balance,
906                    binding: track.midi_learn_balance.clone(),
907                });
908            }
909            if track.midi_learn_mute.is_some() {
910                actions.push(Action::TrackSetMidiLearnBinding {
911                    track_name: track.name.clone(),
912                    target: crate::message::TrackMidiLearnTarget::Mute,
913                    binding: track.midi_learn_mute.clone(),
914                });
915            }
916            if track.midi_learn_solo.is_some() {
917                actions.push(Action::TrackSetMidiLearnBinding {
918                    track_name: track.name.clone(),
919                    target: crate::message::TrackMidiLearnTarget::Solo,
920                    binding: track.midi_learn_solo.clone(),
921                });
922            }
923            if track.midi_learn_arm.is_some() {
924                actions.push(Action::TrackSetMidiLearnBinding {
925                    track_name: track.name.clone(),
926                    target: crate::message::TrackMidiLearnTarget::Arm,
927                    binding: track.midi_learn_arm.clone(),
928                });
929            }
930            if track.midi_learn_input_monitor.is_some() {
931                actions.push(Action::TrackSetMidiLearnBinding {
932                    track_name: track.name.clone(),
933                    target: crate::message::TrackMidiLearnTarget::InputMonitor,
934                    binding: track.midi_learn_input_monitor.clone(),
935                });
936            }
937            if track.midi_learn_disk_monitor.is_some() {
938                actions.push(Action::TrackSetMidiLearnBinding {
939                    track_name: track.name.clone(),
940                    target: crate::message::TrackMidiLearnTarget::DiskMonitor,
941                    binding: track.midi_learn_disk_monitor.clone(),
942                });
943            }
944            if track.vca_master.is_some() {
945                actions.push(Action::TrackSetVcaMaster {
946                    track_name: track.name.clone(),
947                    master_track: track.vca_master(),
948                });
949            }
950            for (other_name, other_track_handle) in &state.tracks {
951                if other_name == track_name {
952                    continue;
953                }
954                let other_track = other_track_handle.lock();
955                if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
956                    actions.push(Action::TrackSetVcaMaster {
957                        track_name: other_name.clone(),
958                        master_track: Some(track_name.clone()),
959                    });
960                }
961            }
962
963            for clip in &track.audio.clips {
964                let length = clip.end.saturating_sub(clip.start).max(1);
965                actions.push(Action::AddClip {
966                    name: clip.name.clone(),
967                    track_name: track.name.clone(),
968                    start: clip.start,
969                    length,
970                    offset: clip.offset,
971                    input_channel: clip.input_channel,
972                    muted: clip.muted,
973                    kind: Kind::Audio,
974                    fade_enabled: clip.fade_enabled,
975                    fade_in_samples: clip.fade_in_samples,
976                    fade_out_samples: clip.fade_out_samples,
977                    warp_markers: clip.warp_markers.clone(),
978                });
979            }
980            for clip in &track.midi.clips {
981                let length = clip.end.saturating_sub(clip.start).max(1);
982                actions.push(Action::AddClip {
983                    name: clip.name.clone(),
984                    track_name: track.name.clone(),
985                    start: clip.start,
986                    length,
987                    offset: clip.offset,
988                    input_channel: clip.input_channel,
989                    muted: clip.muted,
990                    kind: Kind::MIDI,
991                    fade_enabled: true,
992                    fade_in_samples: 240,
993                    fade_out_samples: 240,
994                    warp_markers: vec![],
995                });
996            }
997        }
998
999        let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1000        let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1001
1002        for (from_name, from_track_handle) in &state.tracks {
1003            let from_track = from_track_handle.lock();
1004            for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1005                let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1006                for conn in conns {
1007                    for (to_name, to_track_handle) in &state.tracks {
1008                        let to_track = to_track_handle.lock();
1009                        for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1010                            if Arc::ptr_eq(&conn, to_in)
1011                                && (from_name == track_name || to_name == track_name)
1012                                && seen_audio.insert((
1013                                    from_name.clone(),
1014                                    from_port,
1015                                    to_name.clone(),
1016                                    to_port,
1017                                ))
1018                            {
1019                                actions.push(Action::Connect {
1020                                    from_track: from_name.clone(),
1021                                    from_port,
1022                                    to_track: to_name.clone(),
1023                                    to_port,
1024                                    kind: Kind::Audio,
1025                                });
1026                            }
1027                        }
1028                    }
1029                }
1030            }
1031
1032            for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1033                let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1034                    out.lock().connections.to_vec();
1035                for conn in conns {
1036                    for (to_name, to_track_handle) in &state.tracks {
1037                        let to_track = to_track_handle.lock();
1038                        for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1039                            if Arc::ptr_eq(&conn, to_in)
1040                                && (from_name == track_name || to_name == track_name)
1041                                && seen_midi.insert((
1042                                    from_name.clone(),
1043                                    from_port,
1044                                    to_name.clone(),
1045                                    to_port,
1046                                ))
1047                            {
1048                                actions.push(Action::Connect {
1049                                    from_track: from_name.clone(),
1050                                    from_port,
1051                                    to_track: to_name.clone(),
1052                                    to_port,
1053                                    kind: Kind::MIDI,
1054                                });
1055                            }
1056                        }
1057                    }
1058                }
1059            }
1060        }
1061
1062        for (to_name, to_track_handle) in &state.tracks {
1063            if to_name != track_name {
1064                continue;
1065            }
1066            let to_track = to_track_handle.lock();
1067            for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1068                for (from_name, from_track_handle) in &state.tracks {
1069                    let from_track = from_track_handle.lock();
1070                    for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1071                        let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1072                        if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1073                            && seen_audio.insert((
1074                                from_name.clone(),
1075                                from_port,
1076                                to_name.clone(),
1077                                to_port,
1078                            ))
1079                        {
1080                            actions.push(Action::Connect {
1081                                from_track: from_name.clone(),
1082                                from_port,
1083                                to_track: to_name.clone(),
1084                                to_port,
1085                                kind: Kind::Audio,
1086                            });
1087                        }
1088                    }
1089                }
1090            }
1091            for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1092                for (from_name, from_track_handle) in &state.tracks {
1093                    let from_track = from_track_handle.lock();
1094                    for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1095                        let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1096                            out.lock().connections.to_vec();
1097                        if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1098                            && seen_midi.insert((
1099                                from_name.clone(),
1100                                from_port,
1101                                to_name.clone(),
1102                                to_port,
1103                            ))
1104                        {
1105                            actions.push(Action::Connect {
1106                                from_track: from_name.clone(),
1107                                from_port,
1108                                to_track: to_name.clone(),
1109                                to_port,
1110                                kind: Kind::MIDI,
1111                            });
1112                        }
1113                    }
1114                }
1115            }
1116        }
1117
1118        return Some(actions);
1119    }
1120
1121    create_inverse_action(action, state).map(|a| vec![a])
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126    use super::*;
1127    use crate::audio::clip::AudioClip;
1128    use crate::kind::Kind;
1129    #[cfg(all(unix, not(target_os = "macos")))]
1130    use crate::message::Lv2PluginState;
1131    use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1132    use crate::mutex::UnsafeMutex;
1133    use crate::track::Track;
1134    use crate::vst3::Vst3PluginState;
1135    use std::sync::Arc;
1136
1137    fn make_state_with_track(track: Track) -> State {
1138        let mut state = State::default();
1139        state.tracks.insert(
1140            track.name.clone(),
1141            Arc::new(UnsafeMutex::new(Box::new(track))),
1142        );
1143        state
1144    }
1145
1146    fn binding(cc: u8) -> MidiLearnBinding {
1147        MidiLearnBinding {
1148            device: Some("midi".to_string()),
1149            channel: 1,
1150            cc,
1151        }
1152    }
1153
1154    #[test]
1155    fn history_record_limits_size_and_clears_redo_on_new_entry() {
1156        let mut history = History::new(2);
1157        let a = UndoEntry {
1158            forward_actions: vec![Action::SetTempo(120.0)],
1159            inverse_actions: vec![Action::SetTempo(110.0)],
1160        };
1161        let b = UndoEntry {
1162            forward_actions: vec![Action::SetLoopEnabled(true)],
1163            inverse_actions: vec![Action::SetLoopEnabled(false)],
1164        };
1165        let c = UndoEntry {
1166            forward_actions: vec![Action::SetMetronomeEnabled(true)],
1167            inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1168        };
1169
1170        history.record(a);
1171        history.record(b.clone());
1172        history.record(c.clone());
1173
1174        let undo = history.undo().unwrap();
1175        assert!(matches!(
1176            undo.as_slice(),
1177            [Action::SetMetronomeEnabled(false)]
1178        ));
1179
1180        let redo = history.redo().unwrap();
1181        assert!(matches!(
1182            redo.as_slice(),
1183            [Action::SetMetronomeEnabled(true)]
1184        ));
1185
1186        history.undo();
1187        history.record(UndoEntry {
1188            forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1189            inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1190        });
1191
1192        assert!(history.redo().is_none());
1193        let undo = history.undo().unwrap();
1194        assert!(matches!(
1195            undo.as_slice(),
1196            [Action::SetClipPlaybackEnabled(false)]
1197        ));
1198        let undo = history.undo().unwrap();
1199        assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1200        assert!(history.undo().is_none());
1201    }
1202
1203    #[test]
1204    fn should_record_covers_recent_transport_and_lv2_actions() {
1205        assert!(should_record(&Action::SetLoopEnabled(true)));
1206        assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1207        assert!(should_record(&Action::SetPunchEnabled(true)));
1208        assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1209        assert!(should_record(&Action::SetMetronomeEnabled(true)));
1210        assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1211        assert!(!should_record(&Action::SetRecordEnabled(true)));
1212        assert!(should_record(&Action::SetClipBounds {
1213            track_name: "t".to_string(),
1214            clip_index: 0,
1215            kind: Kind::Audio,
1216            start: 64,
1217            length: 32,
1218            offset: 16,
1219        }));
1220        assert!(should_record(&Action::TrackLoadVst3Plugin {
1221            track_name: "t".to_string(),
1222            plugin_path: "/tmp/test.vst3".to_string(),
1223        }));
1224        #[cfg(all(unix, not(target_os = "macos")))]
1225        {
1226            assert!(should_record(&Action::TrackLoadLv2Plugin {
1227                track_name: "t".to_string(),
1228                plugin_uri: "urn:test".to_string(),
1229            }));
1230            assert!(should_record(&Action::TrackSetLv2ControlValue {
1231                track_name: "t".to_string(),
1232                instance_id: 0,
1233                index: 1,
1234                value: 0.5,
1235            }));
1236            assert!(!should_record(&Action::TrackSetLv2PluginState {
1237                track_name: "t".to_string(),
1238                instance_id: 0,
1239                state: Lv2PluginState {
1240                    port_values: vec![],
1241                    properties: vec![],
1242                },
1243            }));
1244        }
1245        assert!(!should_record(&Action::TrackVst3RestoreState {
1246            track_name: "t".to_string(),
1247            instance_id: 0,
1248            state: Vst3PluginState {
1249                plugin_id: "id".to_string(),
1250                component_state: vec![],
1251                controller_state: vec![],
1252            },
1253        }));
1254    }
1255
1256    #[test]
1257    fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1258        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1259        track
1260            .audio
1261            .clips
1262            .push(AudioClip::new("existing".to_string(), 0, 16));
1263        let state = make_state_with_track(track);
1264
1265        let inverse = create_inverse_action(
1266            &Action::AddClip {
1267                name: "new".to_string(),
1268                track_name: "t".to_string(),
1269                start: 32,
1270                length: 16,
1271                offset: 0,
1272                input_channel: 0,
1273                muted: false,
1274                kind: Kind::Audio,
1275                fade_enabled: false,
1276                fade_in_samples: 0,
1277                fade_out_samples: 0,
1278                warp_markers: vec![],
1279            },
1280            &state,
1281        )
1282        .unwrap();
1283
1284        match inverse {
1285            Action::RemoveClip {
1286                track_name,
1287                kind,
1288                clip_indices,
1289            } => {
1290                assert_eq!(track_name, "t");
1291                assert_eq!(kind, Kind::Audio);
1292                assert_eq!(clip_indices, vec![1]);
1293            }
1294            other => panic!("unexpected inverse action: {other:?}"),
1295        }
1296    }
1297
1298    #[test]
1299    fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1300        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1301        let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1302        clip.offset = 7;
1303        track.audio.clips.push(clip);
1304        let state = make_state_with_track(track);
1305
1306        let inverse = create_inverse_action(
1307            &Action::SetClipBounds {
1308                track_name: "t".to_string(),
1309                clip_index: 0,
1310                kind: Kind::Audio,
1311                start: 14,
1312                length: 22,
1313                offset: 11,
1314            },
1315            &state,
1316        )
1317        .expect("inverse action");
1318
1319        match inverse {
1320            Action::SetClipBounds {
1321                track_name,
1322                clip_index,
1323                kind,
1324                start,
1325                length,
1326                offset,
1327            } => {
1328                assert_eq!(track_name, "t");
1329                assert_eq!(clip_index, 0);
1330                assert_eq!(kind, Kind::Audio);
1331                assert_eq!(start, 10);
1332                assert_eq!(length, 30);
1333                assert_eq!(offset, 7);
1334            }
1335            other => panic!("unexpected inverse action: {other:?}"),
1336        }
1337    }
1338
1339    #[test]
1340    fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
1341        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1342        track.midi_learn_volume = Some(binding(7));
1343        let state = make_state_with_track(track);
1344
1345        let inverse = create_inverse_action(
1346            &Action::TrackSetMidiLearnBinding {
1347                track_name: "t".to_string(),
1348                target: TrackMidiLearnTarget::Volume,
1349                binding: Some(binding(9)),
1350            },
1351            &state,
1352        )
1353        .unwrap();
1354
1355        match inverse {
1356            Action::TrackSetMidiLearnBinding {
1357                track_name,
1358                target,
1359                binding,
1360            } => {
1361                assert_eq!(track_name, "t");
1362                assert_eq!(target, TrackMidiLearnTarget::Volume);
1363                assert_eq!(binding.unwrap().cc, 7);
1364            }
1365            other => panic!("unexpected inverse action: {other:?}"),
1366        }
1367    }
1368
1369    #[test]
1370    fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
1371        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1372        track.next_plugin_instance_id = 42;
1373        let state = make_state_with_track(track);
1374
1375        let inverse = create_inverse_action(
1376            &Action::TrackLoadVst3Plugin {
1377                track_name: "t".to_string(),
1378                plugin_path: "/tmp/test.vst3".to_string(),
1379            },
1380            &state,
1381        )
1382        .unwrap();
1383
1384        match inverse {
1385            Action::TrackUnloadVst3PluginInstance {
1386                track_name,
1387                instance_id,
1388            } => {
1389                assert_eq!(track_name, "t");
1390                assert_eq!(instance_id, 42);
1391            }
1392            other => panic!("unexpected inverse action: {other:?}"),
1393        }
1394    }
1395
1396    #[test]
1397    #[cfg(all(unix, not(target_os = "macos")))]
1398    fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
1399        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1400        track.next_lv2_instance_id = 5;
1401        let state = make_state_with_track(track);
1402
1403        let inverse = create_inverse_action(
1404            &Action::TrackLoadLv2Plugin {
1405                track_name: "t".to_string(),
1406                plugin_uri: "urn:test".to_string(),
1407            },
1408            &state,
1409        )
1410        .unwrap();
1411
1412        match inverse {
1413            Action::TrackUnloadLv2PluginInstance {
1414                track_name,
1415                instance_id,
1416            } => {
1417                assert_eq!(track_name, "t");
1418                assert_eq!(instance_id, 5);
1419            }
1420            other => panic!("unexpected inverse action: {other:?}"),
1421        }
1422    }
1423
1424    #[test]
1425    fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
1426        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1427        track.midi_learn_volume = Some(binding(7));
1428        track.midi_learn_disk_monitor = Some(binding(64));
1429        let state = make_state_with_track(track);
1430
1431        let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
1432
1433        assert_eq!(inverses.len(), 2);
1434        assert!(inverses.iter().any(|action| {
1435            matches!(
1436                action,
1437                Action::TrackSetMidiLearnBinding {
1438                    target: TrackMidiLearnTarget::Volume,
1439                    binding: Some(MidiLearnBinding { cc: 7, .. }),
1440                    ..
1441                }
1442            )
1443        }));
1444        assert!(inverses.iter().any(|action| {
1445            matches!(
1446                action,
1447                Action::TrackSetMidiLearnBinding {
1448                    target: TrackMidiLearnTarget::DiskMonitor,
1449                    binding: Some(MidiLearnBinding { cc: 64, .. }),
1450                    ..
1451                }
1452            )
1453        }));
1454    }
1455
1456    #[test]
1457    fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
1458        let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1459        track.level = -3.0;
1460        track.balance = 0.25;
1461        track.armed = true;
1462        track.muted = true;
1463        track.soloed = true;
1464        track.input_monitor = true;
1465        track.disk_monitor = false;
1466        track.midi_learn_volume = Some(binding(10));
1467        track.vca_master = Some("bus".to_string());
1468        track.audio.ins.push(Arc::new(AudioIO::new(64)));
1469        track.audio.outs.push(Arc::new(AudioIO::new(64)));
1470        let state = make_state_with_track(track);
1471
1472        let inverses =
1473            create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
1474
1475        assert!(matches!(
1476            inverses.first(),
1477            Some(Action::AddTrack {
1478                name,
1479                audio_ins: 1,
1480                audio_outs: 1,
1481                midi_ins: 1,
1482                midi_outs: 1,
1483            }) if name == "t"
1484        ));
1485        assert!(
1486            inverses
1487                .iter()
1488                .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
1489        );
1490        assert!(
1491            inverses
1492                .iter()
1493                .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
1494        );
1495        assert!(
1496            inverses.iter().any(
1497                |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
1498            )
1499        );
1500        assert!(
1501            inverses.iter().any(
1502                |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
1503            )
1504        );
1505        assert!(inverses.iter().any(|action| {
1506            matches!(
1507                action,
1508                Action::TrackSetMidiLearnBinding {
1509                    target: TrackMidiLearnTarget::Volume,
1510                    binding: Some(MidiLearnBinding { cc: 10, .. }),
1511                    ..
1512                }
1513            )
1514        }));
1515        assert!(inverses.iter().any(|action| {
1516            matches!(
1517                action,
1518                Action::TrackSetVcaMaster {
1519                    track_name,
1520                    master_track: Some(master),
1521                } if track_name == "t" && master == "bus"
1522            )
1523        }));
1524    }
1525}