use std::collections::HashSet;
use std::path::PathBuf;
use crate::config::{Config, SortMode};
use crate::history::History;
use crate::package::{Runner, Script, Scripts, Workspace};
const MIN_COLUMN_WIDTH: u16 = 28;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum AppMode {
#[default]
Normal,
Filter { query: String },
MultiSelect { selected: HashSet<usize> },
Help,
Error { message: String },
Args { script_index: usize, input: String },
WorkspaceSelect,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WorkspaceContext {
Root,
Workspace(usize),
}
#[derive(Debug, Clone)]
pub struct ScriptRun {
pub script: Script,
pub args: Option<String>,
pub workspace: Option<String>,
pub workspace_path: Option<PathBuf>,
}
impl std::fmt::Display for ScriptRun {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let prefix = if let Some(ws) = &self.workspace {
format!("{} > ", ws)
} else {
String::new()
};
if let Some(args) = &self.args {
write!(f, "{}{} {}", prefix, self.script.name(), args)
} else {
write!(f, "{}{}", prefix, self.script.name())
}
}
}
pub struct App {
scripts: Scripts,
config: Config,
history: History,
runner: Runner,
project_name: String,
project_path: PathBuf,
is_monorepo: bool,
workspaces: Vec<Workspace>,
workspace_context: WorkspaceContext,
workspace_selected: usize,
mode: AppMode,
selected: usize,
scroll_offset: usize,
filter_text: String,
sort_mode: SortMode,
visible_indices: Vec<usize>,
columns: usize,
should_quit: bool,
script_to_run: Option<ScriptRun>,
}
impl App {
pub fn new(
scripts: Scripts,
config: Config,
history: History,
project_name: String,
project_path: PathBuf,
runner: Runner,
) -> Self {
Self::with_workspaces(
scripts,
config,
history,
project_name,
project_path,
runner,
Vec::new(),
)
}
pub fn with_workspaces(
scripts: Scripts,
config: Config,
history: History,
project_name: String,
project_path: PathBuf,
runner: Runner,
workspaces: Vec<Workspace>,
) -> Self {
let sort_mode = config.general.default_sort;
let visible_indices: Vec<usize> = (0..scripts.len()).collect();
let is_monorepo = !workspaces.is_empty();
let initial_mode = if is_monorepo {
AppMode::WorkspaceSelect
} else {
AppMode::Normal
};
let mut app = Self {
scripts,
config,
history,
runner,
project_name,
project_path,
is_monorepo,
workspaces,
workspace_context: WorkspaceContext::Root,
workspace_selected: 0,
mode: initial_mode,
selected: 0,
scroll_offset: 0,
filter_text: String::new(),
sort_mode,
visible_indices,
columns: 1,
should_quit: false,
script_to_run: None,
};
app.update_visible_scripts();
app
}
pub fn mode(&self) -> &AppMode {
&self.mode
}
pub fn should_quit(&self) -> bool {
self.should_quit
}
pub fn script_to_run(&self) -> Option<&ScriptRun> {
self.script_to_run.as_ref()
}
pub fn project_name(&self) -> &str {
&self.project_name
}
pub fn project_path(&self) -> &PathBuf {
&self.project_path
}
pub fn runner(&self) -> Runner {
self.runner
}
pub fn scripts(&self) -> &Scripts {
&self.scripts
}
pub fn filter_text(&self) -> &str {
&self.filter_text
}
pub fn sort_mode(&self) -> SortMode {
self.sort_mode
}
pub fn columns(&self) -> usize {
self.columns
}
pub fn selected_index(&self) -> usize {
self.selected
}
pub fn scroll_offset(&self) -> usize {
self.scroll_offset
}
pub fn visible_count(&self) -> usize {
self.visible_indices.len()
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn is_monorepo(&self) -> bool {
self.is_monorepo
}
pub fn workspaces(&self) -> &[Workspace] {
&self.workspaces
}
pub fn workspace_context(&self) -> &WorkspaceContext {
&self.workspace_context
}
pub fn workspace_selected(&self) -> usize {
self.workspace_selected
}
pub fn current_workspace(&self) -> Option<&Workspace> {
match &self.workspace_context {
WorkspaceContext::Root => None,
WorkspaceContext::Workspace(idx) => self.workspaces.get(*idx),
}
}
pub fn breadcrumb(&self) -> String {
match &self.workspace_context {
WorkspaceContext::Root => self.project_name.clone(),
WorkspaceContext::Workspace(idx) => {
if let Some(ws) = self.workspaces.get(*idx) {
format!("{} > {}", self.project_name, ws.name())
} else {
self.project_name.clone()
}
}
}
}
pub fn visible_scripts(&self) -> Vec<&Script> {
self.visible_indices
.iter()
.filter_map(|&i| self.scripts.iter().nth(i))
.collect()
}
pub fn selected_script(&self) -> Option<&Script> {
self.visible_indices
.get(self.selected)
.and_then(|&i| self.scripts.iter().nth(i))
}
pub fn get_visible_script(&self, index: usize) -> Option<&Script> {
self.visible_indices
.get(index)
.and_then(|&i| self.scripts.iter().nth(i))
}
pub fn set_mode(&mut self, mode: AppMode) {
self.mode = mode;
}
pub fn toggle_filter_mode(&mut self) {
match &self.mode {
AppMode::Filter { .. } => {
self.mode = AppMode::Normal;
self.filter_text.clear();
self.update_visible_scripts();
}
AppMode::Normal => {
self.mode = AppMode::Filter {
query: String::new(),
};
}
_ => {}
}
}
pub fn enter_args_mode(&mut self) {
if self.selected < self.visible_indices.len() {
self.mode = AppMode::Args {
script_index: self.selected,
input: String::new(),
};
}
}
pub fn toggle_multi_select(&mut self) {
match &self.mode {
AppMode::MultiSelect { .. } => {
self.mode = AppMode::Normal;
}
AppMode::Normal => {
self.mode = AppMode::MultiSelect {
selected: HashSet::new(),
};
}
_ => {}
}
}
pub fn toggle_help(&mut self) {
match self.mode {
AppMode::Help => {
self.mode = AppMode::Normal;
}
_ => {
self.mode = AppMode::Help;
}
}
}
pub fn enter_workspace_select(&mut self) {
if self.is_monorepo {
self.mode = AppMode::WorkspaceSelect;
self.workspace_selected = 0;
}
}
pub fn exit_workspace_select(&mut self) {
self.mode = AppMode::Normal;
self.selected = 0;
self.update_visible_scripts();
}
pub fn select_workspace(&mut self, index: usize) {
if index == 0 {
self.workspace_context = WorkspaceContext::Root;
} else if let Some(workspace) = self.workspaces.get(index - 1) {
self.workspace_context = WorkspaceContext::Workspace(index - 1);
self.scripts = Scripts::from_vec(workspace.scripts().to_vec());
}
self.selected = 0;
self.mode = AppMode::Normal;
self.update_visible_scripts();
}
pub fn select_current_workspace(&mut self) {
self.select_workspace(self.workspace_selected);
}
pub fn back_to_workspace_select(&mut self) {
if self.is_monorepo {
self.mode = AppMode::WorkspaceSelect;
}
}
pub fn workspace_move_up(&mut self) {
if self.workspace_selected > 0 {
self.workspace_selected -= 1;
}
}
pub fn workspace_move_down(&mut self) {
let max_index = self.workspaces.len();
if self.workspace_selected < max_index {
self.workspace_selected += 1;
}
}
pub fn workspace_move_left(&mut self) {
if self.workspace_selected > 0 {
self.workspace_selected -= 1;
}
}
pub fn workspace_move_right(&mut self) {
let max_index = self.workspaces.len();
if self.workspace_selected < max_index {
self.workspace_selected += 1;
}
}
pub fn select_workspace_by_number(&mut self, num: usize) {
if num > 0 && num <= self.workspaces.len() + 1 {
self.select_workspace(num - 1);
}
}
pub fn workspace_count(&self) -> usize {
self.workspaces.len() + 1 }
pub fn set_filter(&mut self, text: String) {
self.filter_text = text.clone();
self.mode = AppMode::Filter { query: text };
self.update_visible_scripts();
}
pub fn push_filter_char(&mut self, c: char) {
self.filter_text.push(c);
self.update_visible_scripts();
}
pub fn pop_filter_char(&mut self) {
self.filter_text.pop();
self.update_visible_scripts();
}
pub fn clear_filter(&mut self) {
self.filter_text.clear();
self.update_visible_scripts();
}
pub fn cycle_sort_mode(&mut self) {
self.sort_mode = match self.sort_mode {
SortMode::Recent => SortMode::Alpha,
SortMode::Alpha => SortMode::Category,
SortMode::Category => SortMode::Recent,
};
self.update_visible_scripts();
}
pub fn set_sort_mode(&mut self, mode: SortMode) {
self.sort_mode = mode;
self.update_visible_scripts();
}
pub fn update_visible_scripts(&mut self) {
let filtered_indices: Vec<usize> = if self.filter_text.is_empty() {
(0..self.scripts.len()).collect()
} else {
let matches = crate::filter::filter_scripts(
&self.filter_text,
self.scripts.as_slice(),
self.config.filter.search_descriptions,
);
matches.into_iter().map(|(idx, _score)| idx).collect()
};
self.visible_indices = self.sort_indices(filtered_indices);
if self.selected >= self.visible_indices.len() {
self.selected = self.visible_indices.len().saturating_sub(1);
}
}
fn sort_indices(&self, mut indices: Vec<usize>) -> Vec<usize> {
match self.sort_mode {
SortMode::Recent => {
let scripts_owned: Vec<Script> = indices
.iter()
.filter_map(|&i| self.scripts.iter().nth(i).cloned())
.collect();
let sorted = self
.history
.get_sorted_by_recent(&self.project_path, &scripts_owned);
sorted
.iter()
.filter_map(|s| {
self.scripts
.iter()
.position(|script| script.name() == s.name())
})
.filter(|i| indices.contains(i))
.collect()
}
SortMode::Alpha => {
indices.sort_by(|&a, &b| {
let name_a = self.scripts.iter().nth(a).map(|s| s.name()).unwrap_or("");
let name_b = self.scripts.iter().nth(b).map(|s| s.name()).unwrap_or("");
name_a.cmp(name_b)
});
indices
}
SortMode::Category => {
indices.sort_by(|&a, &b| {
let name_a = self.scripts.iter().nth(a).map(|s| s.name()).unwrap_or("");
let name_b = self.scripts.iter().nth(b).map(|s| s.name()).unwrap_or("");
let category_a = name_a.split(':').next().unwrap_or(name_a);
let category_b = name_b.split(':').next().unwrap_or(name_b);
category_a.cmp(category_b).then_with(|| name_a.cmp(name_b))
});
indices
}
}
}
pub fn update_columns(&mut self, width: u16) {
self.columns = calculate_columns(width);
}
fn row_count(&self) -> usize {
let count = self.visible_indices.len();
if count == 0 || self.columns == 0 {
return 0;
}
(count + self.columns - 1) / self.columns
}
fn current_position(&self) -> (usize, usize) {
let row = self.selected / self.columns;
let col = self.selected % self.columns;
(row, col)
}
pub fn move_up(&mut self) {
if self.visible_indices.is_empty() || self.columns == 0 {
return;
}
let (row, col) = self.current_position();
if row > 0 {
let new_index = (row - 1) * self.columns + col;
if new_index < self.visible_indices.len() {
self.selected = new_index;
}
}
}
pub fn move_down(&mut self) {
if self.visible_indices.is_empty() || self.columns == 0 {
return;
}
let (row, col) = self.current_position();
let new_index = (row + 1) * self.columns + col;
if new_index < self.visible_indices.len() {
self.selected = new_index;
} else {
let last_row = self.row_count().saturating_sub(1);
if row < last_row {
self.selected = self.visible_indices.len().saturating_sub(1);
}
}
}
pub fn move_left(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
pub fn move_right(&mut self) {
if self.selected < self.visible_indices.len().saturating_sub(1) {
self.selected += 1;
}
}
pub fn move_to_first(&mut self) {
self.selected = 0;
}
pub fn move_to_last(&mut self) {
self.selected = self.visible_indices.len().saturating_sub(1);
}
pub fn select_by_number(&mut self, num: usize) {
if num > 0 && num <= self.visible_indices.len() {
self.selected = num - 1;
}
}
pub fn select_prev(&mut self) {
self.move_up();
}
pub fn select_next(&mut self) {
self.move_down();
}
pub fn select_first(&mut self) {
self.move_to_first();
}
pub fn select_last(&mut self) {
self.move_to_last();
}
pub fn quit(&mut self) {
self.should_quit = true;
}
pub fn run_selected(&mut self) -> Option<ScriptRun> {
if let Some(script) = self.selected_script() {
let (workspace, workspace_path) = self.get_workspace_info();
let run = ScriptRun {
script: script.clone(),
args: None,
workspace,
workspace_path,
};
self.script_to_run = Some(run.clone());
self.should_quit = true;
Some(run)
} else {
None
}
}
fn get_workspace_info(&self) -> (Option<String>, Option<PathBuf>) {
match &self.workspace_context {
WorkspaceContext::Root => (None, None),
WorkspaceContext::Workspace(idx) => {
if let Some(ws) = self.workspaces.get(*idx) {
(Some(ws.name().to_string()), Some(ws.path().to_path_buf()))
} else {
(None, None)
}
}
}
}
pub fn run_numbered(&mut self, num: usize) -> Option<ScriptRun> {
if num > 0 && num <= self.visible_indices.len() {
self.selected = num - 1;
self.run_selected()
} else {
None
}
}
pub fn run_by_number(&mut self, num: usize) {
self.run_numbered(num);
}
pub fn run_with_args(&mut self, args: String) -> Option<ScriptRun> {
if let Some(script) = self.selected_script() {
let (workspace, workspace_path) = self.get_workspace_info();
let run = ScriptRun {
script: script.clone(),
args: if args.is_empty() { None } else { Some(args) },
workspace,
workspace_path,
};
self.script_to_run = Some(run.clone());
self.should_quit = true;
Some(run)
} else {
None
}
}
pub fn toggle_current_selection(&mut self) {
if let AppMode::MultiSelect { ref mut selected } = self.mode {
if selected.contains(&self.selected) {
selected.remove(&self.selected);
} else {
selected.insert(self.selected);
}
}
}
pub fn multi_selected_indices(&self) -> Option<&HashSet<usize>> {
if let AppMode::MultiSelect { ref selected } = self.mode {
Some(selected)
} else {
None
}
}
pub fn run_multi_selected(&mut self) -> Vec<ScriptRun> {
let (workspace, workspace_path) = self.get_workspace_info();
let runs: Vec<ScriptRun> = if let AppMode::MultiSelect { ref selected } = self.mode {
selected
.iter()
.filter_map(|&idx| {
self.get_visible_script(idx).map(|script| ScriptRun {
script: script.clone(),
args: None,
workspace: workspace.clone(),
workspace_path: workspace_path.clone(),
})
})
.collect()
} else {
vec![]
};
if !runs.is_empty() {
self.script_to_run = runs.first().cloned();
self.should_quit = true;
}
runs
}
}
pub fn calculate_columns(width: u16) -> usize {
if width < 60 {
1
} else if width < 90 {
2
} else if width < 120 {
3
} else if width < 160 {
4
} else {
5
}
}
pub fn calculate_column_width(total_width: u16, columns: usize) -> u16 {
if columns == 0 {
return total_width;
}
let padding = 2; let available = total_width.saturating_sub(padding * 2);
(available / columns as u16).max(MIN_COLUMN_WIDTH)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_scripts() -> Scripts {
let mut scripts = Scripts::new();
scripts.add(Script::new("dev", "vite"));
scripts.add(Script::new("build", "vite build"));
scripts.add(Script::new("test", "vitest"));
scripts.add(Script::new("lint", "eslint ."));
scripts.add(Script::new("format", "prettier --write ."));
scripts.add(Script::new("typecheck", "tsc --noEmit"));
scripts.add(Script::new("build:prod", "vite build --mode production"));
scripts.add(Script::new("build:dev", "vite build --mode development"));
scripts.add(Script::new("test:unit", "vitest unit"));
scripts
}
fn create_test_app() -> App {
let scripts = create_test_scripts();
let config = Config::default();
let history = History::new();
App::new(
scripts,
config,
history,
"test-project".to_string(),
PathBuf::from("/test/project"),
Runner::Npm,
)
}
#[test]
fn test_app_new() {
let app = create_test_app();
assert_eq!(app.project_name(), "test-project");
assert_eq!(app.runner(), Runner::Npm);
assert!(!app.should_quit());
assert!(app.script_to_run().is_none());
assert_eq!(app.mode(), &AppMode::Normal);
}
#[test]
fn test_visible_scripts() {
let app = create_test_app();
let visible = app.visible_scripts();
assert_eq!(visible.len(), 9);
}
#[test]
fn test_selected_script() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha); let script = app.selected_script().unwrap();
assert_eq!(script.name(), "build");
}
#[test]
fn test_move_left_right() {
let mut app = create_test_app();
app.update_columns(100); app.set_sort_mode(SortMode::Alpha);
assert_eq!(app.selected_index(), 0);
app.move_right();
assert_eq!(app.selected_index(), 1);
app.move_right();
assert_eq!(app.selected_index(), 2);
app.move_left();
assert_eq!(app.selected_index(), 1);
app.move_left();
assert_eq!(app.selected_index(), 0);
app.move_left();
assert_eq!(app.selected_index(), 0);
}
#[test]
fn test_move_up_down_single_column() {
let mut app = create_test_app();
app.update_columns(50); app.set_sort_mode(SortMode::Alpha);
assert_eq!(app.selected_index(), 0);
app.move_down();
assert_eq!(app.selected_index(), 1);
app.move_down();
assert_eq!(app.selected_index(), 2);
app.move_up();
assert_eq!(app.selected_index(), 1);
app.move_up();
assert_eq!(app.selected_index(), 0);
app.move_up();
assert_eq!(app.selected_index(), 0);
}
#[test]
fn test_move_up_down_multi_column() {
let mut app = create_test_app();
app.update_columns(100); app.set_sort_mode(SortMode::Alpha);
assert_eq!(app.selected_index(), 0);
app.move_down(); assert_eq!(app.selected_index(), 3);
app.move_down(); assert_eq!(app.selected_index(), 6);
app.move_right(); assert_eq!(app.selected_index(), 7);
app.move_up(); assert_eq!(app.selected_index(), 4);
app.move_up(); assert_eq!(app.selected_index(), 1);
}
#[test]
fn test_move_to_first_last() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
app.move_to_last();
assert_eq!(app.selected_index(), 8);
app.move_to_first();
assert_eq!(app.selected_index(), 0);
}
#[test]
fn test_select_by_number() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
app.select_by_number(5);
assert_eq!(app.selected_index(), 4);
app.select_by_number(1);
assert_eq!(app.selected_index(), 0);
app.select_by_number(9);
assert_eq!(app.selected_index(), 8);
app.select_by_number(0);
assert_eq!(app.selected_index(), 8);
app.select_by_number(100);
assert_eq!(app.selected_index(), 8);
}
#[test]
fn test_filter_updates_visible() {
let mut app = create_test_app();
app.set_filter("build".to_string());
let visible = app.visible_scripts();
assert_eq!(visible.len(), 3);
assert!(visible.iter().all(|s| s.name().contains("build")));
}
#[test]
fn test_filter_adjusts_selection() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
app.move_to_last();
assert_eq!(app.selected_index(), 8);
app.set_filter("test".to_string());
assert!(app.selected_index() < app.visible_count());
}
#[test]
fn test_filter_clear() {
let mut app = create_test_app();
app.set_filter("dev".to_string());
assert!(app.visible_count() < 9);
app.clear_filter();
assert_eq!(app.visible_count(), 9);
}
#[test]
fn test_filter_char_operations() {
let mut app = create_test_app();
app.push_filter_char('t');
assert_eq!(app.filter_text(), "t");
app.push_filter_char('e');
assert_eq!(app.filter_text(), "te");
app.pop_filter_char();
assert_eq!(app.filter_text(), "t");
app.pop_filter_char();
assert_eq!(app.filter_text(), "");
}
#[test]
fn test_cycle_sort_mode() {
let mut app = create_test_app();
assert_eq!(app.sort_mode(), SortMode::Recent);
app.cycle_sort_mode();
assert_eq!(app.sort_mode(), SortMode::Alpha);
app.cycle_sort_mode();
assert_eq!(app.sort_mode(), SortMode::Category);
app.cycle_sort_mode();
assert_eq!(app.sort_mode(), SortMode::Recent);
}
#[test]
fn test_sort_mode_alpha() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
let visible = app.visible_scripts();
let names: Vec<&str> = visible.iter().map(|s| s.name()).collect();
let mut sorted_names = names.clone();
sorted_names.sort();
assert_eq!(names, sorted_names);
}
#[test]
fn test_sort_mode_category() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Category);
let visible = app.visible_scripts();
let names: Vec<&str> = visible.iter().map(|s| s.name()).collect();
let build_indices: Vec<usize> = names
.iter()
.enumerate()
.filter(|(_, n)| n.starts_with("build"))
.map(|(i, _)| i)
.collect();
if build_indices.len() > 1 {
for i in 1..build_indices.len() {
assert!(build_indices[i] - build_indices[i - 1] <= 1);
}
}
}
#[test]
fn test_run_selected() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
let run = app.run_selected();
assert!(run.is_some());
let run = run.unwrap();
assert_eq!(run.script.name(), "build"); assert!(run.args.is_none());
assert!(app.should_quit());
}
#[test]
fn test_run_numbered() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
let run = app.run_numbered(3);
assert!(run.is_some());
assert!(run.unwrap().script.name().len() > 0);
assert_eq!(app.selected_index(), 2);
assert!(app.should_quit());
}
#[test]
fn test_run_with_args() {
let mut app = create_test_app();
app.set_sort_mode(SortMode::Alpha);
let run = app.run_with_args("--watch".to_string());
assert!(run.is_some());
let run = run.unwrap();
assert_eq!(run.args, Some("--watch".to_string()));
assert!(app.should_quit());
}
#[test]
fn test_quit() {
let mut app = create_test_app();
assert!(!app.should_quit());
app.quit();
assert!(app.should_quit());
}
#[test]
fn test_toggle_filter_mode() {
let mut app = create_test_app();
assert_eq!(app.mode(), &AppMode::Normal);
app.toggle_filter_mode();
assert!(matches!(app.mode(), &AppMode::Filter { .. }));
app.toggle_filter_mode();
assert_eq!(app.mode(), &AppMode::Normal);
}
#[test]
fn test_toggle_multi_select() {
let mut app = create_test_app();
assert_eq!(app.mode(), &AppMode::Normal);
app.toggle_multi_select();
assert!(matches!(app.mode(), &AppMode::MultiSelect { .. }));
app.toggle_multi_select();
assert_eq!(app.mode(), &AppMode::Normal);
}
#[test]
fn test_toggle_help() {
let mut app = create_test_app();
assert_eq!(app.mode(), &AppMode::Normal);
app.toggle_help();
assert_eq!(app.mode(), &AppMode::Help);
app.toggle_help();
assert_eq!(app.mode(), &AppMode::Normal);
}
#[test]
fn test_enter_args_mode() {
let mut app = create_test_app();
app.enter_args_mode();
assert!(matches!(
app.mode(),
&AppMode::Args {
script_index: 0,
..
}
));
}
#[test]
fn test_multi_select_toggle_selection() {
let mut app = create_test_app();
app.toggle_multi_select();
app.toggle_current_selection();
let selected = app.multi_selected_indices().unwrap();
assert!(selected.contains(&0));
app.move_right();
app.toggle_current_selection();
let selected = app.multi_selected_indices().unwrap();
assert!(selected.contains(&0));
assert!(selected.contains(&1));
app.toggle_current_selection();
let selected = app.multi_selected_indices().unwrap();
assert!(!selected.contains(&1));
}
#[test]
fn test_calculate_columns() {
assert_eq!(calculate_columns(50), 1);
assert_eq!(calculate_columns(59), 1);
assert_eq!(calculate_columns(60), 2);
assert_eq!(calculate_columns(89), 2);
assert_eq!(calculate_columns(90), 3);
assert_eq!(calculate_columns(119), 3);
assert_eq!(calculate_columns(120), 4);
assert_eq!(calculate_columns(159), 4);
assert_eq!(calculate_columns(160), 5);
assert_eq!(calculate_columns(200), 5);
}
#[test]
fn test_update_columns() {
let mut app = create_test_app();
app.update_columns(100);
assert_eq!(app.columns(), 3);
app.update_columns(50);
assert_eq!(app.columns(), 1);
app.update_columns(160);
assert_eq!(app.columns(), 5);
}
#[test]
fn test_calculate_column_width() {
assert_eq!(calculate_column_width(100, 3), 32);
assert_eq!(calculate_column_width(80, 2), 38);
assert_eq!(calculate_column_width(60, 1), 56);
assert_eq!(calculate_column_width(50, 0), 50); }
#[test]
fn test_empty_scripts() {
let scripts = Scripts::new();
let config = Config::default();
let history = History::new();
let app = App::new(
scripts,
config,
history,
"empty-project".to_string(),
PathBuf::from("/test/empty"),
Runner::Npm,
);
assert_eq!(app.visible_count(), 0);
assert!(app.selected_script().is_none());
}
#[test]
fn test_navigation_with_empty_scripts() {
let scripts = Scripts::new();
let config = Config::default();
let history = History::new();
let mut app = App::new(
scripts,
config,
history,
"empty-project".to_string(),
PathBuf::from("/test/empty"),
Runner::Npm,
);
app.move_up();
app.move_down();
app.move_left();
app.move_right();
app.move_to_first();
app.move_to_last();
assert_eq!(app.selected_index(), 0);
}
#[test]
fn test_filter_no_matches() {
let mut app = create_test_app();
app.set_filter("nonexistent_script_xyz".to_string());
assert_eq!(app.visible_count(), 0);
assert!(app.selected_script().is_none());
}
#[test]
fn test_navigation_last_row_partial() {
let mut scripts = Scripts::new();
for i in 0..7 {
scripts.add(Script::new(format!("script{}", i), format!("cmd{}", i)));
}
let config = Config::default();
let history = History::new();
let mut app = App::new(
scripts,
config,
history,
"test".to_string(),
PathBuf::from("/test"),
Runner::Npm,
);
app.update_columns(100); app.set_sort_mode(SortMode::Alpha);
app.select_by_number(3);
assert_eq!(app.selected_index(), 2);
app.move_down();
assert_eq!(app.selected_index(), 5);
app.move_down();
assert_eq!(app.selected_index(), 6);
}
}