code-moniker 0.4.0

Standalone CLI / linter for the code-moniker symbol graph: file and directory probes, project-wide architecture rules.
Documentation
use crate::ui::app::{
	App, FocusRegion, PanelNavigationState, ShellAction, scope_label, sync_contextual_view,
};
use crate::ui::render::view;
use crate::ui::shell::ShellEvent;
use crate::ui::{clipboard, explorer, panel};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::ui) enum FocusCycle {
	Forward,
	Backward,
}

pub(in crate::ui) fn copy_panel_snapshot(app: &mut App) {
	let panel = explorer::active_panel(app);
	let snapshot = panel::panel_snapshot(&panel, view::current_panel_snapshot_width());
	let component = snapshot.component.as_str().to_string();
	let text = snapshot.to_text(crate::ui::app::view_mode(app).label(), &scope_label(app));
	let Some(tx) = app.runtime.event_tx.clone() else {
		crate::ui::app::set_status(app, "clipboard copy unavailable before event loop start");
		return;
	};
	match clipboard::copy_text_async(component.clone(), text, move |result| {
		let _ = tx.send(ShellEvent::Clipboard(result));
	}) {
		Ok(()) => {
			crate::ui::app::set_status(app, format!("copying {component} snapshot to clipboard"))
		}
		Err(error) => crate::ui::app::set_status(app, format!("clipboard copy failed: {error:#}")),
	}
}

pub(in crate::ui) fn cycle_focus_region(app: &mut App, direction: FocusCycle) {
	let usage_open = crate::ui::app::usage_lens(app).is_some();
	let current = crate::ui::app::focus_region(app);
	let next = match (current, usage_open, direction) {
		(FocusRegion::Navigator, true, FocusCycle::Forward) => FocusRegion::Panel,
		(FocusRegion::Panel, true, FocusCycle::Forward) => FocusRegion::UsageLens,
		(FocusRegion::UsageLens, true, FocusCycle::Forward) => FocusRegion::Navigator,
		(FocusRegion::Navigator, true, FocusCycle::Backward) => FocusRegion::UsageLens,
		(FocusRegion::UsageLens, true, FocusCycle::Backward) => FocusRegion::Panel,
		(FocusRegion::Panel, true, FocusCycle::Backward) => FocusRegion::Navigator,
		(FocusRegion::Navigator, false, _) => FocusRegion::Panel,
		(FocusRegion::Panel, false, _) => FocusRegion::Navigator,
		(FocusRegion::UsageLens, false, _) => FocusRegion::Navigator,
	};
	crate::ui::app::dispatch_shell(app, ShellAction::SetFocusRegion(next));
	match next {
		FocusRegion::Panel => {
			ensure_active_panel_selection(app);
			crate::ui::app::set_status(
				app,
				"panel focused; up/down moves within panel, Tab/Shift+Tab moves focus",
			);
		}
		FocusRegion::UsageLens => {
			crate::ui::app::set_status(
				app,
				"usage tree focused; Tab/Shift+Tab moves focus, Esc returns to navigator",
			);
			sync_contextual_view(app);
		}
		FocusRegion::Navigator => crate::ui::app::set_status(app, "navigator focused"),
	}
}

pub(in crate::ui) fn ensure_active_panel_selection(app: &mut App) {
	let panel = explorer::active_panel_nav(app);
	let count = panel.navigation_len;
	let component = panel.component;
	let current = &app.app_store.shell().panel_navigation;
	let selected = if count == 0 {
		None
	} else if current.component == Some(component) {
		current.selected.map(|idx| idx.min(count - 1)).or(Some(0))
	} else {
		Some(0)
	};
	let scroll = if current.component == Some(component) {
		current.scroll
	} else {
		0
	};
	let expanded = if current.component == Some(component) {
		current.expanded.clone()
	} else {
		explorer::active_panel_default_expanded(app)
	};
	crate::ui::app::dispatch_shell(
		app,
		ShellAction::SetPanelNavigation(PanelNavigationState {
			component: Some(component),
			selected,
			scroll,
			expanded,
		}),
	);
}

pub(in crate::ui) fn move_panel_selection(app: &mut App, direction: i8) {
	let panel = explorer::active_panel_nav(app);
	let count = panel.navigation_len;
	let component = panel.component;
	if count == 0 {
		scroll_panel_lines(app, direction);
		crate::ui::app::set_status(app, "panel has no navigable item; scrolled content");
		return;
	}
	let current = &app.app_store.shell().panel_navigation;
	let selected_idx = if current.component == Some(component) {
		current.selected.unwrap_or(0).min(count - 1)
	} else {
		0
	};
	let selected = if direction > 0 {
		(selected_idx + 1).min(count - 1)
	} else {
		selected_idx.saturating_sub(1)
	};
	crate::ui::app::dispatch_shell(
		app,
		ShellAction::SetPanelNavigation(PanelNavigationState {
			component: Some(component),
			selected: Some(selected),
			scroll: current.scroll,
			expanded: current.expanded.clone(),
		}),
	);
	crate::ui::app::set_status(app, format!("panel item {}/{}", selected + 1, count));
}

pub(in crate::ui) fn move_panel_to_edge(app: &mut App, home: bool) {
	let panel = explorer::active_panel_nav(app);
	let count = panel.navigation_len;
	let component = panel.component;
	let selected = if count == 0 {
		None
	} else if home {
		Some(0)
	} else {
		Some(count - 1)
	};
	let current = &app.app_store.shell().panel_navigation;
	crate::ui::app::dispatch_shell(
		app,
		ShellAction::SetPanelNavigation(PanelNavigationState {
			component: Some(component),
			selected,
			scroll: if home { 0 } else { current.scroll },
			expanded: current.expanded.clone(),
		}),
	);
	if count == 0 {
		crate::ui::app::set_status(app, "panel has no navigable item");
	} else {
		crate::ui::app::set_status(
			app,
			format!("panel item {}/{}", selected.unwrap_or(0) + 1, count),
		);
	}
}

fn scroll_panel_lines(app: &mut App, direction: i8) {
	let next = if direction > 0 {
		app.app_store
			.shell()
			.panel_navigation
			.scroll
			.saturating_add(1)
	} else {
		app.app_store
			.shell()
			.panel_navigation
			.scroll
			.saturating_sub(1)
	};
	crate::ui::app::dispatch_shell(app, ShellAction::SetPanelScroll(next));
}

pub(in crate::ui) fn toggle_panel_tree_node(app: &mut App) {
	let Some(row) = selected_panel_tree_row(app) else {
		crate::ui::app::set_status(app, "panel item has no child node");
		return;
	};
	if !row.has_children {
		crate::ui::app::set_status(app, "panel item has no child node");
		return;
	}
	let mut expanded = app.app_store.shell().panel_navigation.expanded.clone();
	let opened = if expanded.remove(&row.key) {
		false
	} else {
		expanded.insert(row.key.clone());
		true
	};
	apply_panel_expanded(app, expanded, None);
	crate::ui::app::set_status(
		app,
		format!(
			"{} panel node {}",
			if opened { "opened" } else { "closed" },
			row.label
		),
	);
}

pub(in crate::ui) fn open_panel_tree_node(app: &mut App) {
	let Some(row) = selected_panel_tree_row(app) else {
		crate::ui::app::set_status(app, "panel item has no child node");
		return;
	};
	if !row.has_children {
		crate::ui::app::set_status(app, "panel item has no child node");
		return;
	}
	if row.expanded {
		crate::ui::app::set_status(app, format!("panel node already open: {}", row.label));
		return;
	}
	let mut expanded = app.app_store.shell().panel_navigation.expanded.clone();
	expanded.insert(row.key.clone());
	apply_panel_expanded(app, expanded, None);
	crate::ui::app::set_status(app, format!("opened panel node {}", row.label));
}

pub(in crate::ui) fn close_panel_tree_node(app: &mut App) {
	let Some(row) = selected_panel_tree_row(app) else {
		crate::ui::app::set_status(app, "panel focused; Tab/Shift+Tab moves focus");
		return;
	};
	if row.has_children && row.expanded {
		let mut expanded = app.app_store.shell().panel_navigation.expanded.clone();
		expanded.remove(&row.key);
		apply_panel_expanded(app, expanded, None);
		crate::ui::app::set_status(app, format!("closed panel node {}", row.label));
		return;
	}
	if row.depth == 0 {
		crate::ui::app::set_status(app, "panel focused; Tab/Shift+Tab moves focus");
		return;
	}
	let rows = explorer::active_panel_tree_rows(app);
	let selected = app
		.app_store
		.shell()
		.panel_navigation
		.selected
		.unwrap_or(0)
		.min(rows.len() - 1);
	let parent_depth = row.depth - 1;
	let parent = rows[..selected]
		.iter()
		.rposition(|candidate| candidate.depth == parent_depth);
	apply_panel_expanded(
		app,
		app.app_store.shell().panel_navigation.expanded.clone(),
		parent,
	);
	crate::ui::app::set_status(app, "panel parent selected");
}

fn selected_panel_tree_row(app: &App) -> Option<crate::ui::render::tree::TreeRowVm> {
	let rows = explorer::active_panel_tree_rows(app);
	let selected = app.app_store.shell().panel_navigation.selected?;
	rows.get(selected).cloned()
}

fn apply_panel_expanded(
	app: &mut App,
	expanded: std::collections::BTreeSet<String>,
	selected: Option<usize>,
) {
	let panel = explorer::active_panel_nav(app);
	let count = explorer::active_panel_tree_rows_with_expanded(app, &expanded).len();
	let selected = selected
		.or(app.app_store.shell().panel_navigation.selected)
		.map(|idx| {
			if count == 0 {
				0
			} else {
				idx.min(count.saturating_sub(1))
			}
		});
	crate::ui::app::dispatch_shell(
		app,
		ShellAction::SetPanelNavigation(PanelNavigationState {
			component: Some(panel.component),
			selected,
			scroll: app.app_store.shell().panel_navigation.scroll,
			expanded,
		}),
	);
}