feat: add 't' key to transpose row/column axes

Pressing 't' swaps all Row-axis categories to Column and all
Column-axis categories to Row, leaving Page categories unchanged.
Selection and scroll offsets are reset to (0,0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ed L
2026-03-24 09:40:02 -07:00
parent c42553fa97
commit cb43a8130e
2 changed files with 46 additions and 1 deletions

View File

@ -283,6 +283,11 @@ impl App {
}
}
// ── Grid transpose ─────────────────────────────────────────────
(KeyCode::Char('t'), KeyModifiers::NONE) => {
self.model.active_view_mut().transpose_axes();
}
// ── Tile movement ──────────────────────────────────────────────
// T = enter tile select mode (single key, no Ctrl needed)
(KeyCode::Char('T'), _) => {
@ -1135,7 +1140,7 @@ impl App {
/// Hint text for the status bar (context-sensitive)
pub fn hint_text(&self) -> &'static str {
match &self.mode {
AppMode::Normal => "hjkl:nav Enter:advance i:edit x:clear /:search F/C/V:panels T:tiles [:]:page ::cmd",
AppMode::Normal => "hjkl:nav Enter:advance i:edit x:clear t:transpose /:search F/C/V:panels T:tiles [:]:page ::cmd",
AppMode::Editing { .. } => "Enter:commit Esc:cancel",
AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back",
AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression",

View File

@ -112,6 +112,18 @@ impl View {
self.hidden_items.get(cat_name).map(|s| s.contains(item_name)).unwrap_or(false)
}
/// Swap all Row categories to Column and all Column categories to Row.
/// Page categories are unaffected.
pub fn transpose_axes(&mut self) {
let rows: Vec<String> = self.categories_on(Axis::Row).iter().map(|s| s.to_string()).collect();
let cols: Vec<String> = self.categories_on(Axis::Column).iter().map(|s| s.to_string()).collect();
for cat in &rows { self.set_axis(cat, Axis::Column); }
for cat in &cols { self.set_axis(cat, Axis::Row); }
self.selected = (0, 0);
self.row_offset = 0;
self.col_offset = 0;
}
/// Cycle axis for a category: Row → Column → Page → Row
pub fn cycle_axis(&mut self, cat_name: &str) {
let next = match self.axis_of(cat_name) {
@ -171,6 +183,34 @@ mod tests {
assert_eq!(v.categories_on(Axis::Page), vec!["Time"]);
}
#[test]
fn transpose_axes_swaps_row_and_column() {
let mut v = view_with_cats(&["Region", "Product"]);
// Default: Region=Row, Product=Column
assert_eq!(v.axis_of("Region"), Axis::Row);
assert_eq!(v.axis_of("Product"), Axis::Column);
v.transpose_axes();
assert_eq!(v.axis_of("Region"), Axis::Column);
assert_eq!(v.axis_of("Product"), Axis::Row);
}
#[test]
fn transpose_axes_leaves_page_categories_unchanged() {
let mut v = view_with_cats(&["Region", "Product", "Time"]);
// Default: Region=Row, Product=Column, Time=Page
v.transpose_axes();
assert_eq!(v.axis_of("Time"), Axis::Page);
}
#[test]
fn transpose_axes_is_its_own_inverse() {
let mut v = view_with_cats(&["Region", "Product"]);
v.transpose_axes();
v.transpose_axes();
assert_eq!(v.axis_of("Region"), Axis::Row);
assert_eq!(v.axis_of("Product"), Axis::Column);
}
#[test]
#[should_panic(expected = "axis_of called for category not registered")]
fn axis_of_unknown_category_panics() {