Small corrections
10
CHANGELOG.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Inline sample finder in the editor: press `Ctrl+B` to open a fuzzy-search popup of all sample folder names. Type to filter, `Ctrl+N`/`Ctrl+P` to navigate, `Tab`/`Enter` to insert the folder name at cursor, `Esc` to dismiss. Mutually exclusive with word completion.
|
||||||
|
- Sample browser now displays the 0-based file index next to each sample name, making it easy to reference samples by index in Forth scripts (e.g., `"drums" bank 0 n`).
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Header bar stats block (CPU/voices/Link peers) is now centered like all other header sections.
|
||||||
|
- CPU percentage changes color when load is high: accent color at 50%+, error color at 80%+.
|
||||||
|
|
||||||
## [0.0.8] - 2026-02-07
|
## [0.0.8] - 2026-02-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ impl CompletionState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SampleFinderState {
|
||||||
|
query: String,
|
||||||
|
folders: Vec<String>,
|
||||||
|
matches: Vec<usize>,
|
||||||
|
cursor: usize,
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleFinderState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
query: String::new(),
|
||||||
|
folders: Vec::new(),
|
||||||
|
matches: Vec::new(),
|
||||||
|
cursor: 0,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SearchState {
|
struct SearchState {
|
||||||
query: String,
|
query: String,
|
||||||
active: bool,
|
active: bool,
|
||||||
@@ -61,6 +81,7 @@ impl SearchState {
|
|||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
text: TextArea<'static>,
|
text: TextArea<'static>,
|
||||||
completion: CompletionState,
|
completion: CompletionState,
|
||||||
|
sample_finder: SampleFinderState,
|
||||||
search: SearchState,
|
search: SearchState,
|
||||||
scroll_offset: Cell<u16>,
|
scroll_offset: Cell<u16>,
|
||||||
}
|
}
|
||||||
@@ -110,6 +131,7 @@ impl Editor {
|
|||||||
Self {
|
Self {
|
||||||
text: TextArea::default(),
|
text: TextArea::default(),
|
||||||
completion: CompletionState::new(),
|
completion: CompletionState::new(),
|
||||||
|
sample_finder: SampleFinderState::new(),
|
||||||
search: SearchState::new(),
|
search: SearchState::new(),
|
||||||
scroll_offset: Cell::new(0),
|
scroll_offset: Cell::new(0),
|
||||||
}
|
}
|
||||||
@@ -118,6 +140,7 @@ impl Editor {
|
|||||||
pub fn set_content(&mut self, lines: Vec<String>) {
|
pub fn set_content(&mut self, lines: Vec<String>) {
|
||||||
self.text = TextArea::new(lines);
|
self.text = TextArea::new(lines);
|
||||||
self.completion.active = false;
|
self.completion.active = false;
|
||||||
|
self.sample_finder.active = false;
|
||||||
self.search.query.clear();
|
self.search.query.clear();
|
||||||
self.search.active = false;
|
self.search.active = false;
|
||||||
self.scroll_offset.set(0);
|
self.scroll_offset.set(0);
|
||||||
@@ -226,6 +249,79 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_sample_folders(&mut self, folders: Vec<String>) {
|
||||||
|
self.sample_finder.folders = folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_sample_finder(&mut self) {
|
||||||
|
self.completion.active = false;
|
||||||
|
self.sample_finder.query.clear();
|
||||||
|
self.sample_finder.cursor = 0;
|
||||||
|
self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
|
||||||
|
self.sample_finder.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dismiss_sample_finder(&mut self) {
|
||||||
|
self.sample_finder.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_finder_active(&self) -> bool {
|
||||||
|
self.sample_finder.active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_finder_input(&mut self, c: char) {
|
||||||
|
self.sample_finder.query.push(c);
|
||||||
|
self.update_sample_finder_matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_finder_backspace(&mut self) {
|
||||||
|
self.sample_finder.query.pop();
|
||||||
|
self.update_sample_finder_matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_finder_next(&mut self) {
|
||||||
|
if self.sample_finder.cursor + 1 < self.sample_finder.matches.len() {
|
||||||
|
self.sample_finder.cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_finder_prev(&mut self) {
|
||||||
|
if self.sample_finder.cursor > 0 {
|
||||||
|
self.sample_finder.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept_sample_finder(&mut self) {
|
||||||
|
if self.sample_finder.matches.is_empty() {
|
||||||
|
self.sample_finder.active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let idx = self.sample_finder.matches[self.sample_finder.cursor];
|
||||||
|
let name = self.sample_finder.folders[idx].clone();
|
||||||
|
self.text.insert_str(&name);
|
||||||
|
self.sample_finder.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_sample_finder_matches(&mut self) {
|
||||||
|
if self.sample_finder.query.is_empty() {
|
||||||
|
self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
|
||||||
|
} else {
|
||||||
|
let mut scored: Vec<(usize, usize)> = self
|
||||||
|
.sample_finder
|
||||||
|
.folders
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, name)| fuzzy_match(&self.sample_finder.query, name).map(|s| (s, i)))
|
||||||
|
.collect();
|
||||||
|
scored.sort_by_key(|(score, _)| *score);
|
||||||
|
self.sample_finder.matches = scored.into_iter().map(|(_, i)| i).collect();
|
||||||
|
}
|
||||||
|
self.sample_finder.cursor = self
|
||||||
|
.sample_finder
|
||||||
|
.cursor
|
||||||
|
.min(self.sample_finder.matches.len().saturating_sub(1));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn input(&mut self, input: impl Into<tui_textarea::Input>) {
|
pub fn input(&mut self, input: impl Into<tui_textarea::Input>) {
|
||||||
let input: tui_textarea::Input = input.into();
|
let input: tui_textarea::Input = input.into();
|
||||||
let has_modifier = input.ctrl || input.alt;
|
let has_modifier = input.ctrl || input.alt;
|
||||||
@@ -261,7 +357,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_completion(&mut self) {
|
fn update_completion(&mut self) {
|
||||||
if !self.completion.enabled || self.completion.candidates.is_empty() {
|
if !self.completion.enabled || self.completion.candidates.is_empty() || self.sample_finder.active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +491,9 @@ impl Editor {
|
|||||||
|
|
||||||
frame.render_widget(Paragraph::new(lines).scroll((offset as u16, 0)), area);
|
frame.render_widget(Paragraph::new(lines).scroll((offset as u16, 0)), area);
|
||||||
|
|
||||||
if self.completion.active && !self.completion.matches.is_empty() {
|
if self.sample_finder.active && !self.sample_finder.matches.is_empty() {
|
||||||
|
self.render_sample_finder(frame, area, cursor_row - offset);
|
||||||
|
} else if self.completion.active && !self.completion.matches.is_empty() {
|
||||||
self.render_completion(frame, area, cursor_row - offset);
|
self.render_completion(frame, area, cursor_row - offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,6 +609,98 @@ impl Editor {
|
|||||||
|
|
||||||
frame.render_widget(Paragraph::new(doc_lines), doc_area);
|
frame.render_widget(Paragraph::new(doc_lines), doc_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_sample_finder(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
|
||||||
|
let t = theme::get();
|
||||||
|
let max_visible: usize = 8;
|
||||||
|
let width: u16 = 24;
|
||||||
|
|
||||||
|
let visible_count = self.sample_finder.matches.len().min(max_visible);
|
||||||
|
let total_height = visible_count as u16 + 1; // +1 for query line
|
||||||
|
|
||||||
|
let (_, cursor_col) = self.text.cursor();
|
||||||
|
let popup_x = (editor_area.x + cursor_col as u16)
|
||||||
|
.min(editor_area.x + editor_area.width.saturating_sub(width));
|
||||||
|
|
||||||
|
let below_y = editor_area.y + cursor_row as u16 + 1;
|
||||||
|
let popup_y = if below_y + total_height > editor_area.y + editor_area.height {
|
||||||
|
(editor_area.y + cursor_row as u16).saturating_sub(total_height)
|
||||||
|
} else {
|
||||||
|
below_y
|
||||||
|
};
|
||||||
|
|
||||||
|
let area = Rect::new(popup_x, popup_y, width, total_height);
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
let bg_style = Style::default().bg(t.editor_widget.completion_bg);
|
||||||
|
let highlight_style = Style::default()
|
||||||
|
.fg(t.editor_widget.completion_selected)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
let normal_style = Style::default().fg(t.editor_widget.completion_fg);
|
||||||
|
|
||||||
|
let w = width as usize;
|
||||||
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
|
||||||
|
let query_display = format!("/{}", self.sample_finder.query);
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
format!("{query_display:<w$}"),
|
||||||
|
highlight_style.bg(t.editor_widget.completion_bg),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let scroll_offset = if self.sample_finder.cursor >= max_visible {
|
||||||
|
self.sample_finder.cursor - max_visible + 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in scroll_offset..scroll_offset + visible_count {
|
||||||
|
let idx = self.sample_finder.matches[i];
|
||||||
|
let name = &self.sample_finder.folders[idx];
|
||||||
|
let style = if i == self.sample_finder.cursor {
|
||||||
|
highlight_style
|
||||||
|
} else {
|
||||||
|
normal_style
|
||||||
|
};
|
||||||
|
let prefix = if i == self.sample_finder.cursor { "> " } else { " " };
|
||||||
|
let display = format!("{prefix}{name:<width$}", width = w - 2);
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
format!("{display:<w$}"),
|
||||||
|
style.bg(t.editor_widget.completion_bg),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill rest with bg
|
||||||
|
for _ in lines.len() as u16..total_height {
|
||||||
|
lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.render_widget(Paragraph::new(lines), area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
||||||
|
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
||||||
|
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
||||||
|
let mut ti = 0;
|
||||||
|
let mut score = 0;
|
||||||
|
let mut prev_pos = 0;
|
||||||
|
for (qi, &qc) in query_lower.iter().enumerate() {
|
||||||
|
loop {
|
||||||
|
if ti >= target_lower.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if target_lower[ti] == qc {
|
||||||
|
if qi > 0 {
|
||||||
|
score += ti - prev_pos;
|
||||||
|
}
|
||||||
|
prev_pos = ti;
|
||||||
|
ti += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ti += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(score)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_word_char(c: char) -> bool {
|
fn is_word_char(c: char) -> bool {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ mod waveform;
|
|||||||
|
|
||||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
pub use active_patterns::{ActivePatterns, MuteStatus};
|
||||||
pub use confirm::ConfirmModal;
|
pub use confirm::ConfirmModal;
|
||||||
pub use editor::{CompletionCandidate, Editor};
|
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||||
pub use file_browser::FileBrowserModal;
|
pub use file_browser::FileBrowserModal;
|
||||||
pub use list_select::ListSelect;
|
pub use list_select::ListSelect;
|
||||||
pub use modal::ModalFrame;
|
pub use modal::ModalFrame;
|
||||||
|
|||||||
@@ -158,12 +158,17 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
Style::new().fg(icon_color)
|
Style::new().fg(icon_color)
|
||||||
};
|
};
|
||||||
|
|
||||||
let spans = vec![
|
let mut spans = vec![
|
||||||
Span::raw(indent),
|
Span::raw(indent),
|
||||||
Span::styled(icon, icon_style),
|
Span::styled(icon, icon_style),
|
||||||
Span::styled(&entry.label, label_style),
|
Span::styled(&entry.label, label_style),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if matches!(entry.kind, TreeLineKind::File) {
|
||||||
|
let idx_style = Style::new().fg(colors.browser.empty_text);
|
||||||
|
spans.push(Span::styled(format!(" {}", entry.index), idx_style));
|
||||||
|
}
|
||||||
|
|
||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use crate::settings::Settings;
|
|||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
||||||
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
PlaybackState, ProjectState, SampleTree, StagedChange, StagedPropChange, UiState,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STEPS_PER_PAGE: usize = 32;
|
const STEPS_PER_PAGE: usize = 32;
|
||||||
@@ -328,6 +328,8 @@ impl App {
|
|||||||
self.editor_ctx
|
self.editor_ctx
|
||||||
.editor
|
.editor
|
||||||
.set_completion_enabled(self.ui.show_completion);
|
.set_completion_enabled(self.ui.show_completion);
|
||||||
|
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
||||||
|
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
||||||
if self.editor_ctx.show_stack {
|
if self.editor_ctx.show_stack {
|
||||||
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||||
}
|
}
|
||||||
@@ -359,6 +361,8 @@ impl App {
|
|||||||
self.editor_ctx
|
self.editor_ctx
|
||||||
.editor
|
.editor
|
||||||
.set_completion_enabled(self.ui.show_completion);
|
.set_completion_enabled(self.ui.show_completion);
|
||||||
|
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
||||||
|
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
||||||
self.editor_ctx.target = EditorTarget::Prelude;
|
self.editor_ctx.target = EditorTarget::Prelude;
|
||||||
self.ui.modal = Modal::Editor;
|
self.ui.modal = Modal::Editor;
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/input.rs
@@ -494,6 +494,19 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if editor.sample_finder_active() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => editor.dismiss_sample_finder(),
|
||||||
|
KeyCode::Tab | KeyCode::Enter => editor.accept_sample_finder(),
|
||||||
|
KeyCode::Backspace => editor.sample_finder_backspace(),
|
||||||
|
KeyCode::Char('n') if ctrl => editor.sample_finder_next(),
|
||||||
|
KeyCode::Char('p') if ctrl => editor.sample_finder_prev(),
|
||||||
|
KeyCode::Char(c) if !ctrl => editor.sample_finder_input(c),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
if editor.is_selecting() {
|
if editor.is_selecting() {
|
||||||
@@ -525,12 +538,17 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
KeyCode::Char('b') if ctrl => {
|
||||||
|
editor.activate_sample_finder();
|
||||||
|
}
|
||||||
KeyCode::Char('f') if ctrl => {
|
KeyCode::Char('f') if ctrl => {
|
||||||
editor.activate_search();
|
editor.activate_search();
|
||||||
}
|
}
|
||||||
KeyCode::Char('n') if ctrl => {
|
KeyCode::Char('n') if ctrl => {
|
||||||
if editor.completion_active() {
|
if editor.completion_active() {
|
||||||
editor.completion_next();
|
editor.completion_next();
|
||||||
|
} else if editor.sample_finder_active() {
|
||||||
|
editor.sample_finder_next();
|
||||||
} else {
|
} else {
|
||||||
editor.search_next();
|
editor.search_next();
|
||||||
}
|
}
|
||||||
@@ -538,6 +556,8 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Char('p') if ctrl => {
|
KeyCode::Char('p') if ctrl => {
|
||||||
if editor.completion_active() {
|
if editor.completion_active() {
|
||||||
editor.completion_prev();
|
editor.completion_prev();
|
||||||
|
} else if editor.sample_finder_active() {
|
||||||
|
editor.sample_finder_prev();
|
||||||
} else {
|
} else {
|
||||||
editor.search_prev();
|
editor.search_prev();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,5 +43,5 @@ pub use patterns_nav::{PatternsColumn, PatternsNav};
|
|||||||
pub use mute::MuteState;
|
pub use mute::MuteState;
|
||||||
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
pub use playback::{PlaybackState, StagedChange, StagedMuteChange, StagedPropChange};
|
||||||
pub use project::ProjectState;
|
pub use project::ProjectState;
|
||||||
pub use sample_browser::SampleBrowserState;
|
pub use sample_browser::{SampleBrowserState, SampleTree};
|
||||||
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};
|
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use cagire_ratatui::{TreeLine, TreeLineKind};
|
use cagire_ratatui::{fuzzy_match, TreeLine, TreeLineKind};
|
||||||
|
|
||||||
const AUDIO_EXTENSIONS: &[&str] = &["wav", "flac", "ogg", "aiff", "aif", "mp3"];
|
const AUDIO_EXTENSIONS: &[&str] = &["wav", "flac", "ogg", "aiff", "aif", "mp3"];
|
||||||
|
|
||||||
@@ -208,6 +208,29 @@ impl SampleTree {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn all_folder_names(&self) -> Vec<String> {
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for root in &self.roots {
|
||||||
|
Self::collect_folder_names(root, &mut names);
|
||||||
|
}
|
||||||
|
names.sort_by_key(|n| n.to_lowercase());
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_folder_names(node: &SampleNode, out: &mut Vec<String>) {
|
||||||
|
match node {
|
||||||
|
SampleNode::Root { children, .. } => {
|
||||||
|
for child in children {
|
||||||
|
Self::collect_folder_names(child, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SampleNode::Folder { name, .. } => {
|
||||||
|
out.push(name.clone());
|
||||||
|
}
|
||||||
|
SampleNode::File { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn visible_entries(&self) -> Vec<TreeLine> {
|
pub fn visible_entries(&self) -> Vec<TreeLine> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for root in &self.roots {
|
for root in &self.roots {
|
||||||
@@ -558,31 +581,6 @@ impl SampleBrowserState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
|
||||||
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
|
||||||
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
|
||||||
let mut ti = 0;
|
|
||||||
let mut score = 0;
|
|
||||||
let mut prev_pos = 0;
|
|
||||||
for (qi, &qc) in query_lower.iter().enumerate() {
|
|
||||||
loop {
|
|
||||||
if ti >= target_lower.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if target_lower[ti] == qc {
|
|
||||||
if qi > 0 {
|
|
||||||
score += ti - prev_pos;
|
|
||||||
}
|
|
||||||
prev_pos = ti;
|
|
||||||
ti += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ti += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(score)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_audio_file(name: &str) -> bool {
|
fn is_audio_file(name: &str) -> bool {
|
||||||
let lower = name.to_lowercase();
|
let lower = name.to_lowercase();
|
||||||
AUDIO_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
|
AUDIO_EXTENSIONS.iter().any(|ext| lower.ends_with(ext))
|
||||||
|
|||||||
@@ -333,14 +333,23 @@ fn render_header(
|
|||||||
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||||
let peers = link.peers();
|
let peers = link.peers();
|
||||||
let voices = app.metrics.active_voices;
|
let voices = app.metrics.active_voices;
|
||||||
let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} ");
|
let cpu_color = if cpu_pct >= 80.0 {
|
||||||
let stats_style = Style::new()
|
theme.flash.error_fg
|
||||||
.bg(theme.header.stats_bg)
|
} else if cpu_pct >= 50.0 {
|
||||||
.fg(theme.header.stats_fg);
|
theme.ui.accent
|
||||||
|
} else {
|
||||||
|
theme.header.stats_fg
|
||||||
|
};
|
||||||
|
let dim = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
|
||||||
|
let stats_line = Line::from(vec![
|
||||||
|
Span::styled(format!(" CPU {cpu_pct:.0}%"), dim.fg(cpu_color)),
|
||||||
|
Span::styled(format!(" V:{voices} L:{peers} "), dim),
|
||||||
|
]);
|
||||||
|
let block_style = Style::new().bg(theme.header.stats_bg);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(stats_text)
|
Paragraph::new(stats_line)
|
||||||
.block(Block::default().padding(pad).style(stats_style))
|
.block(Block::default().padding(pad).style(block_style))
|
||||||
.alignment(Alignment::Right),
|
.alignment(Alignment::Center),
|
||||||
stats_area,
|
stats_area,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -809,6 +818,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
Span::styled(" eval ", dim),
|
Span::styled(" eval ", dim),
|
||||||
Span::styled("C-f", key),
|
Span::styled("C-f", key),
|
||||||
Span::styled(" find ", dim),
|
Span::styled(" find ", dim),
|
||||||
|
Span::styled("C-b", key),
|
||||||
|
Span::styled(" samples ", dim),
|
||||||
Span::styled("C-s", key),
|
Span::styled("C-s", key),
|
||||||
Span::styled(" stack ", dim),
|
Span::styled(" stack ", dim),
|
||||||
Span::styled("C-u", key),
|
Span::styled("C-u", key),
|
||||||
|
|||||||
BIN
website/public/eight_pic.png
Normal file
|
After Width: | Height: | Size: 926 KiB |
BIN
website/public/fifth_pic.png
Normal file
|
After Width: | Height: | Size: 941 KiB |
BIN
website/public/fourth_pic.png
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
website/public/ninth_pic.png
Normal file
|
After Width: | Height: | Size: 805 KiB |
BIN
website/public/one_pic.png
Normal file
|
After Width: | Height: | Size: 925 KiB |
BIN
website/public/second_pic.png
Normal file
|
After Width: | Height: | Size: 833 KiB |
BIN
website/public/seventh_pic.png
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
website/public/sixth_pic.png
Normal file
|
After Width: | Height: | Size: 870 KiB |
@@ -103,7 +103,8 @@ li { margin: 0.1rem 0; }
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-cell video {
|
.example-cell video,
|
||||||
|
.example-cell img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|||||||
BIN
website/public/third_pic.png
Normal file
|
After Width: | Height: | Size: 835 KiB |
@@ -32,15 +32,15 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="examples-grid">
|
<div class="examples-grid">
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/one_pic.png" alt="Cagire screenshot 1"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/second_pic.png" alt="Cagire screenshot 2"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/third_pic.png" alt="Cagire screenshot 3"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/fourth_pic.png" alt="Cagire screenshot 4"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/fifth_pic.png" alt="Cagire screenshot 5"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/sixth_pic.png" alt="Cagire screenshot 6"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/seventh_pic.png" alt="Cagire screenshot 7"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/eight_pic.png" alt="Cagire screenshot 8"></div>
|
||||||
<div class="example-cell"><video src="" muted loop playsinline></video></div>
|
<div class="example-cell"><img src="/ninth_pic.png" alt="Cagire screenshot 9"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Download</h2>
|
<h2>Download</h2>
|
||||||
|
|||||||