Compare commits
3 Commits
0f0f13f2b8
...
e956346ae9
| Author | SHA1 | Date | |
|---|---|---|---|
| e956346ae9 | |||
| 6892575a53 | |||
| 27b826ebaf |
91
BUILDING.md
Normal file
91
BUILDING.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Building Cagire
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Rust** (stable toolchain): https://rustup.rs
|
||||
|
||||
## System Dependencies
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
cmake is required by `rusty_link` (Ableton Link C++ bindings). Xcode Command Line Tools provide the C++ compiler. CoreAudio and CoreMIDI are built-in. The desktop build needs no additional dependencies on macOS (Cocoa/Metal are provided by the system).
|
||||
|
||||
### Linux (Debian/Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo apt install cmake g++ pkg-config libasound2-dev libjack-jackd2-dev
|
||||
```
|
||||
|
||||
For the desktop build (egui/eframe), also install:
|
||||
|
||||
```bash
|
||||
sudo apt install libgl-dev libxkbcommon-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
|
||||
```
|
||||
|
||||
### Linux (Arch)
|
||||
|
||||
```bash
|
||||
sudo pacman -S cmake gcc pkgconf alsa-lib jack2
|
||||
```
|
||||
|
||||
For the desktop build:
|
||||
|
||||
```bash
|
||||
sudo pacman -S libxkbcommon libx11 libxcursor libxrandr libxi wayland mesa
|
||||
```
|
||||
|
||||
### Linux (Fedora)
|
||||
|
||||
```bash
|
||||
sudo dnf install cmake gcc-c++ pkgconf-pkg-config alsa-lib-devel jack-audio-connection-kit-devel
|
||||
```
|
||||
|
||||
For the desktop build:
|
||||
|
||||
```bash
|
||||
sudo dnf install libxkbcommon-devel libX11-devel libXcursor-devel libXrandr-devel libXi-devel wayland-devel mesa-libGL-devel
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Install Visual Studio Build Tools (MSVC) and CMake. Everything else is provided by the Windows SDK.
|
||||
|
||||
## Build
|
||||
|
||||
Terminal (default):
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Desktop (egui window):
|
||||
|
||||
```bash
|
||||
cargo build --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Terminal (default):
|
||||
|
||||
```bash
|
||||
cargo run --release -- [OPTIONS]
|
||||
```
|
||||
|
||||
Desktop (egui window):
|
||||
|
||||
```bash
|
||||
cargo run --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-s, --samples <path>` | Sample directory (repeatable) |
|
||||
| `-o, --output <device>` | Output audio device |
|
||||
| `-i, --input <device>` | Input audio device |
|
||||
| `-c, --channels <n>` | Output channel count |
|
||||
| `-b, --buffer <size>` | Audio buffer size |
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
### Forth Language
|
||||
|
||||
**Bracket syntax `[ ... ]`**
|
||||
- `[ v1 v2 v3 ]` pushes all items plus their count. Sugar for `v1 v2 v3 3`.
|
||||
|
||||
**New words:**
|
||||
- `index` — select item at explicit index (wraps with modulo).
|
||||
- `pbounce` — ping-pong cycle keyed by pattern iteration (vs `bounce` which is step-keyed).
|
||||
- `except` — inverse of `every`: run quotation on all iterations except every nth.
|
||||
- `every+` / `except+` — `every`/`except` with a phase offset.
|
||||
- `all` / `noall` — apply current params globally to all emitted sounds; clear global params.
|
||||
- `linmap` / `expmap` — linear and exponential range mapping.
|
||||
- `rec` / `overdub` (`dub`) — toggle recording/overdubbing master audio to a named sample.
|
||||
- `orec` / `odub` — toggle recording/overdubbing a single orbit to a named sample.
|
||||
|
||||
**Harmony and voicing words:**
|
||||
- `key!` — set tonal center for scale operations.
|
||||
- `triad` / `seventh` — diatonic triad/seventh from scale degree (follows a scale word).
|
||||
- `inv` / `dinv` — chord inversion / down inversion.
|
||||
- `drop2` / `drop3` — drop-2 / drop-3 voicings.
|
||||
- `tp` — transpose all ints on stack by N semitones.
|
||||
|
||||
**New chord types:**
|
||||
- `pwr`, `augmaj7`, `7sus4`, `9sus4`, `maj69`, `min69`, `maj11`, `maj13`, `min13`, `dom7s11`.
|
||||
|
||||
**Ducking compressor params:**
|
||||
- `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
|
||||
|
||||
### Engine
|
||||
- SF2 soundfont support: auto-scans sample directories for `.sf2` files and loads them.
|
||||
- Audio stream errors surfaced as flash messages instead of printing to stderr.
|
||||
|
||||
### UI / Visualization
|
||||
- Lissajous XY scope: stereo phase display using Braille characters, togglable via Options.
|
||||
- Gain boost (1x–16x) and normalize toggle for scope/lissajous/spectrum.
|
||||
- Pattern description field: editable via `d` on Patterns page, shown in pattern row and properties.
|
||||
- Mute/solo on main page now apply immediately (no staging).
|
||||
- 10 bundled demo projects loaded on fresh startup (togglable in Options).
|
||||
|
||||
### Themes
|
||||
- 5 new themes: Iceberg, Everforest, Fauve, Tropicalia, Jaipur.
|
||||
|
||||
### Desktop (egui)
|
||||
- Fixed Alt/Option key on macOS (dead-key composition now works).
|
||||
- Fixed multi-character text paste.
|
||||
- Extended function key support (F13–F20).
|
||||
|
||||
### Fixed
|
||||
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
|
||||
- Scope widget not drawing completely in some terminal sizes.
|
||||
|
||||
### Documentation
|
||||
- New tutorials: Recording (`docs/tutorials/recording.md`), Soundfonts (`docs/tutorials/soundfont.md`).
|
||||
|
||||
### UI / UX (breaking cosmetic changes)
|
||||
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
|
||||
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
|
||||
|
||||
5
Cross.toml
Normal file
5
Cross.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[build]
|
||||
volumes = ["/Users/bubo/doux:/Users/bubo/doux"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
dockerfile = "./cross/aarch64-linux.Dockerfile"
|
||||
@@ -99,6 +99,14 @@ impl Editor {
|
||||
self.text.is_selecting()
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, row: u16, col: u16) {
|
||||
self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
|
||||
}
|
||||
|
||||
pub fn scroll_offset(&self) -> u16 {
|
||||
self.scroll_offset.get()
|
||||
}
|
||||
|
||||
pub fn copy(&mut self) {
|
||||
self.text.copy();
|
||||
}
|
||||
@@ -111,6 +119,14 @@ impl Editor {
|
||||
self.text.paste()
|
||||
}
|
||||
|
||||
pub fn yank_text(&self) -> String {
|
||||
self.text.yank_text()
|
||||
}
|
||||
|
||||
pub fn set_yank_text(&mut self, text: impl Into<String>) {
|
||||
self.text.set_yank_text(text);
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
self.text.select_all();
|
||||
}
|
||||
@@ -138,7 +154,11 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, lines: Vec<String>) {
|
||||
let yank = self.text.yank_text();
|
||||
self.text = TextArea::new(lines);
|
||||
if !yank.is_empty() {
|
||||
self.text.set_yank_text(yank);
|
||||
}
|
||||
self.completion.active = false;
|
||||
self.sample_finder.active = false;
|
||||
self.search.query.clear();
|
||||
|
||||
15
cross/aarch64-linux.Dockerfile
Normal file
15
cross/aarch64-linux.Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main
|
||||
|
||||
RUN dpkg --add-architecture arm64 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake \
|
||||
libclang-dev \
|
||||
libasound2-dev:arm64 \
|
||||
libjack-dev:arm64 \
|
||||
libxcb-render0-dev:arm64 \
|
||||
libxcb-shape0-dev:arm64 \
|
||||
libxcb-xfixes0-dev:arm64 \
|
||||
libxkbcommon-dev:arm64 \
|
||||
libgl1-mesa-dev:arm64 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -22,7 +22,7 @@ use cagire::engine::{
|
||||
};
|
||||
use cagire::init::{init, InitArgs};
|
||||
use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
|
||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse};
|
||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
||||
use cagire::settings::Settings;
|
||||
use cagire::views;
|
||||
use crossbeam_channel::Receiver;
|
||||
@@ -168,6 +168,7 @@ struct CagireDesktop {
|
||||
mouse_y: Arc<AtomicU32>,
|
||||
mouse_down: Arc<AtomicU32>,
|
||||
last_frame: std::time::Instant,
|
||||
egui_mouse: EguiMouseState,
|
||||
}
|
||||
|
||||
impl CagireDesktop {
|
||||
@@ -212,6 +213,7 @@ impl CagireDesktop {
|
||||
mouse_y: b.mouse_y,
|
||||
mouse_down: b.mouse_down,
|
||||
last_frame: std::time::Instant::now(),
|
||||
egui_mouse: EguiMouseState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +324,7 @@ impl CagireDesktop {
|
||||
|
||||
let term = self.terminal.get_frame().area();
|
||||
let widget_rect = ctx.content_rect();
|
||||
for mouse in convert_egui_mouse(ctx, widget_rect, term) {
|
||||
for mouse in convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut self.app,
|
||||
link: &self.link,
|
||||
|
||||
@@ -343,13 +343,26 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
editor.select_all();
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
editor.copy();
|
||||
ctx.app.editor_ctx.editor.copy();
|
||||
let text = ctx.app.editor_ctx.editor.yank_text();
|
||||
if let Some(clip) = &mut ctx.app.clipboard {
|
||||
let _ = clip.set_text(text);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('x') if ctrl => {
|
||||
editor.cut();
|
||||
ctx.app.editor_ctx.editor.cut();
|
||||
let text = ctx.app.editor_ctx.editor.yank_text();
|
||||
if let Some(clip) = &mut ctx.app.clipboard {
|
||||
let _ = clip.set_text(text);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
editor.paste();
|
||||
if let Some(clip) = &mut ctx.app.clipboard {
|
||||
if let Ok(text) = clip.get_text() {
|
||||
ctx.app.editor_ctx.editor.set_yank_text(text);
|
||||
}
|
||||
}
|
||||
ctx.app.editor_ctx.editor.paste();
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down if shift => {
|
||||
if !editor.is_selecting() {
|
||||
|
||||
@@ -26,6 +26,12 @@ pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) {
|
||||
|
||||
match kind {
|
||||
MouseEventKind::Down(MouseButton::Left) => handle_click(ctx, col, row, term),
|
||||
MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => {
|
||||
handle_editor_drag(ctx, col, row, term);
|
||||
}
|
||||
MouseEventKind::Up(MouseButton::Left) => {
|
||||
ctx.app.editor_ctx.mouse_selecting = false;
|
||||
}
|
||||
MouseEventKind::ScrollUp => handle_scroll(ctx, col, row, term, true),
|
||||
MouseEventKind::ScrollDown => handle_scroll(ctx, col, row, term, false),
|
||||
_ => {}
|
||||
@@ -59,6 +65,55 @@ fn contains(area: Rect, col: u16, row: u16) -> bool {
|
||||
col >= area.x && col < area.x + area.width && row >= area.y && row < area.y + area.height
|
||||
}
|
||||
|
||||
fn handle_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
if ctx.app.editor_ctx.mouse_selecting {
|
||||
handle_editor_mouse(ctx, col, row, term, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, dragging: bool) {
|
||||
// Reconstruct editor area (mirrors render_modal_editor / ModalFrame::render_centered)
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 60 / 100).max(10);
|
||||
let modal_w = width.min(term.width.saturating_sub(4));
|
||||
let modal_h = height.min(term.height.saturating_sub(4));
|
||||
let mx = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
||||
let my = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
||||
// inner = area inside 1-cell border
|
||||
let inner_x = mx + 1;
|
||||
let inner_y = my + 1;
|
||||
let inner_w = modal_w.saturating_sub(2);
|
||||
let inner_h = modal_h.saturating_sub(2);
|
||||
|
||||
let show_search = ctx.app.editor_ctx.editor.search_active()
|
||||
|| !ctx.app.editor_ctx.editor.search_query().is_empty();
|
||||
let reserved = 1 + if show_search { 1 } else { 0 };
|
||||
let editor_y = inner_y + if show_search { 1 } else { 0 };
|
||||
let editor_h = inner_h.saturating_sub(reserved);
|
||||
|
||||
if col < inner_x || col >= inner_x + inner_w || row < editor_y || row >= editor_y + editor_h {
|
||||
return;
|
||||
}
|
||||
|
||||
let scroll = ctx.app.editor_ctx.editor.scroll_offset();
|
||||
let text_row = (row - editor_y) + scroll;
|
||||
let text_col = col - inner_x;
|
||||
|
||||
if dragging {
|
||||
if !ctx.app.editor_ctx.editor.is_selecting() {
|
||||
ctx.app.editor_ctx.editor.start_selection();
|
||||
}
|
||||
} else {
|
||||
ctx.app.editor_ctx.mouse_selecting = true;
|
||||
ctx.app.editor_ctx.editor.cancel_selection();
|
||||
}
|
||||
|
||||
ctx.app
|
||||
.editor_ctx
|
||||
.editor
|
||||
.move_cursor_to(text_row, text_col);
|
||||
}
|
||||
|
||||
fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
// Sticky minimap intercepts all clicks
|
||||
if matches!(ctx.app.ui.minimap, MinimapMode::Sticky) {
|
||||
@@ -745,8 +800,11 @@ fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
|
||||
fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
match &ctx.app.ui.modal {
|
||||
Modal::Editor | Modal::Preview => {
|
||||
// Don't dismiss editor/preview on click
|
||||
Modal::Editor => {
|
||||
handle_editor_mouse(ctx, col, row, term, false);
|
||||
}
|
||||
Modal::Preview => {
|
||||
// Don't dismiss preview on click
|
||||
}
|
||||
Modal::Confirm { .. } => {
|
||||
handle_confirm_click(ctx, col, row, term);
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EguiMouseState {
|
||||
prev_down: bool,
|
||||
prev_col: u16,
|
||||
prev_row: u16,
|
||||
}
|
||||
|
||||
pub fn convert_egui_mouse(
|
||||
ctx: &egui::Context,
|
||||
widget_rect: egui::Rect,
|
||||
term: Rect,
|
||||
state: &mut EguiMouseState,
|
||||
) -> Vec<MouseEvent> {
|
||||
let mut events = Vec::new();
|
||||
if widget_rect.width() < 1.0 || widget_rect.height() < 1.0 || term.width == 0 || term.height == 0 {
|
||||
if widget_rect.width() < 1.0
|
||||
|| widget_rect.height() < 1.0
|
||||
|| term.width == 0
|
||||
|| term.height == 0
|
||||
{
|
||||
return events;
|
||||
}
|
||||
|
||||
@@ -16,6 +28,15 @@ pub fn convert_egui_mouse(
|
||||
return;
|
||||
};
|
||||
if !widget_rect.contains(pos) {
|
||||
if state.prev_down && !i.pointer.primary_down() {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
column: state.prev_col,
|
||||
row: state.prev_row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
state.prev_down = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,15 +47,43 @@ pub fn convert_egui_mouse(
|
||||
let col = col.min(term.width.saturating_sub(1));
|
||||
let row = row.min(term.height.saturating_sub(1));
|
||||
|
||||
if i.pointer.button_clicked(egui::PointerButton::Primary) {
|
||||
let is_down = i.pointer.primary_down();
|
||||
let moved = col != state.prev_col || row != state.prev_row;
|
||||
|
||||
if !state.prev_down && is_down {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
} else if state.prev_down && is_down && moved {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Drag(MouseButton::Left),
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
} else if state.prev_down && !is_down {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
} else if !is_down && moved {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Moved,
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
}
|
||||
|
||||
state.prev_down = is_down;
|
||||
state.prev_col = col;
|
||||
state.prev_row = row;
|
||||
|
||||
let scroll = i.raw_scroll_delta.y;
|
||||
if scroll > 1.0 {
|
||||
events.push(MouseEvent {
|
||||
|
||||
@@ -99,6 +99,7 @@ pub struct EditorContext {
|
||||
pub stack_cache: RefCell<Option<StackCache>>,
|
||||
pub target: EditorTarget,
|
||||
pub steps_per_page: Cell<usize>,
|
||||
pub mouse_selecting: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -150,6 +151,7 @@ impl Default for EditorContext {
|
||||
stack_cache: RefCell::new(None),
|
||||
target: EditorTarget::default(),
|
||||
steps_per_page: Cell::new(32),
|
||||
mouse_selecting: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user