diff --git a/filesystem/keymap.go b/filesystem/keymap.go index 6d75e1d..4be11ec 100644 --- a/filesystem/keymap.go +++ b/filesystem/keymap.go @@ -12,6 +12,8 @@ type KeyMap struct { RemoveTrack string `json:"remove_track"` AddStep string `json:"add_step"` RemoveStep string `json:"remove_step"` + CopyStep string `json:"copy_step"` + PasteStep string `json:"paste_step"` PreviousStep string `json:"previous_step"` NextStep string `json:"next_step"` PageUp string `json:"page_up"` @@ -42,6 +44,8 @@ func NewDefaultAzertyKeyMap() KeyMap { RemoveTrack: ")", AddStep: "+", RemoveStep: "°", + CopyStep: "ctrl+c", + PasteStep: "ctrl+v", PreviousStep: ",", NextStep: ";", PageUp: "p", @@ -73,6 +77,8 @@ func NewDefaultAzertyMacKeyMap() KeyMap { RemoveTrack: ")", AddStep: "_", RemoveStep: "°", + CopyStep: "ctrl+c", + PasteStep: "ctrl+v", PreviousStep: ",", NextStep: ";", PageUp: "p", @@ -103,6 +109,8 @@ func NewDefaultQwertyKeyMap() KeyMap { RemoveTrack: "-", AddStep: "+", RemoveStep: "_", + CopyStep: "ctrl+c", + PasteStep: "ctrl+v", PreviousStep: ",", NextStep: ".", PageUp: "p", @@ -134,6 +142,8 @@ func NewDefaultQwertyMacKeyMap() KeyMap { RemoveTrack: "-", AddStep: "+", RemoveStep: "_", + CopyStep: "ctrl+c", + PasteStep: "ctrl+v", PreviousStep: ",", NextStep: ".", PageUp: "p", diff --git a/sequencer/helper.go b/sequencer/helper.go new file mode 100644 index 0000000..d598159 --- /dev/null +++ b/sequencer/helper.go @@ -0,0 +1,29 @@ +package sequencer + +// Helper functions for deep copying pointers +func copyIntPtr(ptr *int) *int { + if ptr == nil { + return nil + } + copy := *ptr + return © +} + +func copyUint8Ptr(ptr *uint8) *uint8 { + if ptr == nil { + return nil + } + copy := *ptr + return © +} + +func copyUint8SlicePtr(ptr *[]uint8) *[]uint8 { + if ptr == nil { + return nil + } + copy := make([]uint8, len(*ptr)) + for i, v := range *ptr { + copy[i] = v + } + return © +} diff --git a/sequencer/sequencer.go b/sequencer/sequencer.go index 6d9ea4e..c5c13cd 100644 --- a/sequencer/sequencer.go +++ b/sequencer/sequencer.go @@ -51,6 +51,8 @@ type Sequencer interface { AddStep(track int) RemoveStep(track int) ToggleStep(track, step int) + CopyStep(track, step int) + PasteStep(track, step int) Tempo() float64 SetTempo(tempo float64) Reset() @@ -72,6 +74,8 @@ type sequencer struct { isPlaying bool isFirstTick bool + + stepClipboard step } // New creates a new sequencer. It also creates new tracks and calls the @@ -245,6 +249,67 @@ func (s *sequencer) ToggleStep(track, step int) { s.tracks[track].steps[step].clearParameters() } +// CopyStep copies a step to the 'clipboard' step. +func (s *sequencer) CopyStep(track, srcStep int) { + if track < 0 || track >= len(s.tracks) || srcStep < 0 || srcStep >= len(s.tracks[track].steps) { + return // Out of bounds, do nothing + } + + originalStep := s.tracks[track].steps[srcStep] + + // Create a deep copy of the step + s.stepClipboard = step{ + midi: originalStep.midi, + track: nil, // We don't want to keep a reference to the original track + position: originalStep.position, + active: originalStep.active, + triggered: false, // Reset triggered state for the copy + controls: make(map[int]*midi.Control), + length: copyIntPtr(originalStep.length), + chord: copyUint8SlicePtr(originalStep.chord), + velocity: copyUint8Ptr(originalStep.velocity), + probability: copyIntPtr(originalStep.probability), + offset: originalStep.offset, + } + + // Deep copy the controls + for k, v := range originalStep.controls { + controlCopy := *v // Assuming midi.Control is safe to copy directly + s.stepClipboard.controls[k] = &controlCopy + } +} + +// PasteStep pastes a clipboard step into a destination step +func (s *sequencer) PasteStep(track, dstStep int) { + if track < 0 || track >= len(s.tracks) || dstStep < 0 || dstStep >= len(s.tracks[track].steps) { + return // Out of bounds, do nothing + } + + // Create a deep copy of the clipboard + newStep := step{ + midi: s.stepClipboard.midi, + track: s.tracks[track], + position: dstStep, + active: s.stepClipboard.active, + triggered: false, // Reset triggered state + controls: make(map[int]*midi.Control), + length: copyIntPtr(s.stepClipboard.length), + chord: copyUint8SlicePtr(s.stepClipboard.chord), + velocity: copyUint8Ptr(s.stepClipboard.velocity), + probability: copyIntPtr(s.stepClipboard.probability), + offset: s.stepClipboard.offset, + } + + // Deep copy the controls + for k, v := range s.stepClipboard.controls { + controlCopy := *v + newStep.controls[k] = &controlCopy + } + + // Replace the step in the track + s.tracks[track].steps[dstStep] = &newStep +} + func (s *sequencer) start() { // Each time the clock ticks, we call the sequencer tick method that // basically makes every track move forward in time. diff --git a/ui/keymap.go b/ui/keymap.go index cd1c399..f617953 100644 --- a/ui/keymap.go +++ b/ui/keymap.go @@ -19,6 +19,8 @@ type keyMap struct { AddStep key.Binding RemoveStep key.Binding + CopyStep key.Binding + PasteStep key.Binding NextStep key.Binding PreviousStep key.Binding @@ -101,6 +103,14 @@ func newKeyMap(keys filesystem.KeyMap) keyMap { key.WithKeys(keys.RemoveStep), key.WithHelp(keys.RemoveStep, "remove step"), ), + CopyStep: key.NewBinding( + key.WithKeys(keys.CopyStep), + key.WithHelp(keys.CopyStep, "copy active step parameters"), + ), + PasteStep: key.NewBinding( + key.WithKeys(keys.PasteStep), + key.WithHelp(keys.PasteStep, "paste copied parameters into active step"), + ), PreviousStep: key.NewBinding( key.WithKeys(keys.PreviousStep), key.WithHelp(keys.PreviousStep, "select previous step"), @@ -178,8 +188,8 @@ func newKeyMap(keys filesystem.KeyMap) keyMap { key.WithHelp(keys.Help, "toggle help"), ), Quit: key.NewBinding( - key.WithKeys("ctrl+c", "esc"), - key.WithHelp("ctrl+c/esc", "quit"), + key.WithKeys("ctrl+q", "esc"), + key.WithHelp("ctrl+q/esc", "quit"), ), } for i, k := range keys.Steps { diff --git a/ui/ui.go b/ui/ui.go index 9510668..bdfce22 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -218,6 +218,20 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.updateParams() return m, nil + case key.Matches(msg, m.keymap.CopyStep): + m.seq.CopyStep(m.activeTrack, m.activeStep) + m.mode = stepMode + m.stepModeTimer = 0 + m.updateParams() + return m, nil + + case key.Matches(msg, m.keymap.PasteStep): + m.seq.PasteStep(m.activeTrack, m.activeStep) + m.mode = stepMode + m.stepModeTimer = 0 + m.updateParams() + return m, nil + case key.Matches(msg, m.keymap.StepToggle): number := m.keymap.StepToggleIndex[msg.String()] if m.mode == patternMode {