Generalize pane visibility with ShowPane/HidePane actions

Replace PopupHelp and ToggleFinder with a general pane visibility system:
- Add PanePosition enum (Main, Floating, future: Sidebar, Split)
- ShowPane { buffer, position } displays a buffer in specified layout
- HidePane { buffer, delete_buffer } removes from layout, optionally deletes

Commands now compose LoadBuffer + ShowPane for popups. Removed FinderBuffer
and Buffer::Finder variant - all buffers are now TextBuffer.

Also adds jj preference to CLAUDE.md, and does some other refactoring
This commit is contained in:
Greg Shuflin
2026-02-10 13:23:20 -08:00
parent a84e5b9d3d
commit 0a9f0b89a7
7 changed files with 119 additions and 79 deletions
+15
View File
@@ -2,6 +2,21 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Version Control
This repository uses both jujutsu (jj) and git. **Prefer jj commands** for all VCS operations:
```bash
jj status # Instead of git status
jj diff # Instead of git diff
jj log # Instead of git log
jj new -m "message" # Create new change with description
jj describe -m "msg" # Update current change's description
jj squash # Fold into parent
```
Use git only when jj doesn't support the operation (e.g., `git push`).
## Build Commands
```bash
+12 -29
View File
@@ -10,16 +10,14 @@ mod pane_layout;
use std::collections::HashMap;
use tokio::sync::mpsc::UnboundedReceiver;
use tracing::{error, info};
use tracing::error;
use crate::{
configuration::Configuration,
core::{
actions::{BufferDestination, BufferSource, CommandAction, EditorAction},
buffer::FinderBuffer,
buffer_query::BufferQuery,
error::BasicErrorKind,
pane_layout::PaneDescriptor,
},
};
@@ -112,7 +110,6 @@ impl EditorCore {
// Get a query interface to the active buffer for movement computation
let buffer_query: Option<&dyn BufferQuery> = match self.active_buffer() {
Buffer::Text(tb) => Some(tb),
Buffer::Finder(_) => None, // Finder doesn't support cursor movement yet
};
let action = self
@@ -218,34 +215,20 @@ impl EditorCore {
tb.set_cursor(x, y);
}
}
PopupHelp(toggle) => {
info!(%toggle, "invoke help");
let keys = self.buffers.keys();
info!(?keys, "buffers");
//TODO change this hardcoding of the buffer id
let buffer_id = BufferId(1);
if toggle {
self.buffers
.insert(buffer_id, Buffer::Text(TextBuffer::help()));
let descriptor = PaneDescriptor::Floating;
self.pane_layout.add(buffer_id, descriptor);
ShowPane { buffer, position } => {
if self.buffers.contains_key(&buffer) {
self.pane_layout.add(buffer, position);
} else {
self.buffers.remove(&buffer_id);
self.pane_layout.remove(buffer_id);
error!(?buffer, "ShowPane: buffer does not exist");
}
}
ToggleFinder(toggle) => {
//TODO change this hardcoding of the buffer id
let buffer_id = BufferId(100);
if toggle {
let buffer = Buffer::Finder(FinderBuffer::new());
self.buffers.insert(buffer_id, buffer);
let descriptor = PaneDescriptor::Floating;
self.pane_layout.add(buffer_id, descriptor);
} else {
self.buffers.remove(&buffer_id);
self.pane_layout.remove(buffer_id);
HidePane {
buffer,
delete_buffer,
} => {
self.pane_layout.remove(buffer);
if delete_buffer {
self.buffers.remove(&buffer);
}
}
+29 -4
View File
@@ -5,6 +5,21 @@ use std::path::PathBuf;
/// `EditorAction`s are defined here as well.
use crate::core::{buffer::BufferId, Mode};
/// Describes how and where a pane should be displayed.
///
/// This allows scripts to control pane positioning without EditorCore
/// needing specific knowledge of each UI pattern (popups, sidebars, splits, etc.)
#[derive(Debug, Clone)]
pub(crate) enum PanePosition {
/// Main editing area, with tab ordering
Main { tab: u16 },
/// Floating popup/dialog centered over the main area
Floating,
// Future possibilities:
// Sidebar { side: Side, width: u16 },
// Split { direction: Direction, ratio: f32 },
}
/// Describes where buffer content comes from.
///
/// This abstraction allows the scripting layer to populate buffers from various sources
@@ -79,10 +94,20 @@ pub(crate) enum EditorAction {
destination: Option<BufferDestination>,
},
/// Show or hide the popup help
PopupHelp(bool),
/// Show or hide the Finder
ToggleFinder(bool),
/// Show a buffer in a pane at the specified position.
/// The buffer must already exist (use LoadBuffer first if needed).
ShowPane {
buffer: BufferId,
position: PanePosition,
},
/// Hide a pane, optionally deleting the buffer.
/// If `delete_buffer` is true, the buffer is also removed from memory.
HidePane {
buffer: BufferId,
delete_buffer: bool,
},
//TODO rename this to something like commanderror
Error(String),
}
+1 -22
View File
@@ -13,23 +13,9 @@ use super::{buffer_settings::BufferSettings, cursor::Cursor, error::BasicError};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub(crate) struct BufferId(pub(crate) usize);
// TODO: Consider whether Buffer enum is still needed, or if everything is just TextBuffer
pub(crate) enum Buffer {
Text(TextBuffer),
Finder(FinderBuffer),
}
#[derive(Debug)]
pub(crate) struct FinderBuffer {
title: Option<String>,
pub(crate) input_buffer: Option<String>,
}
impl FinderBuffer {
pub(crate) fn new() -> Self {
Self {
title: None,
input_buffer: None,
}
}
}
//TODO make these settings nonpublic, except for some setting that indicates style
@@ -70,13 +56,6 @@ impl TextBuffer {
}
}
pub(crate) fn help() -> Self {
let mut buf = Self::from_text("Dummy help text\nA newline\nSome other stuff desu");
buf.writeable = false;
buf.title = Some("Help".to_string());
buf
}
pub(crate) fn title(&self) -> Option<&str> {
self.title.as_ref().map(|x| x.as_str())
}
+55 -6
View File
@@ -3,10 +3,32 @@ use std::path::PathBuf;
use tracing::info;
use crate::core::{
actions::{BufferDestination, BufferSource},
actions::{BufferDestination, BufferSource, PanePosition},
buffer::BufferId,
EditorAction,
};
// Well-known buffer IDs for standard panes
// TODO: these should probably be managed more dynamically
const HELP_BUFFER_ID: BufferId = BufferId(1);
const FINDER_BUFFER_ID: BufferId = BufferId(100);
const HELP_TEXT: &str = "\
Pane Editor - Help
Navigation (Normal mode):
h/j/k/l - Move cursor left/down/up/right
i - Enter Insert mode
: - Enter Command mode
q - Quit
Commands:
:open <path> - Open a file
:write [path] - Save buffer (to path or backing file)
:help - Show this help
:quit - Quit the editor
";
#[derive(Debug)]
pub(crate) struct CommandAPI {}
@@ -50,16 +72,43 @@ impl CommandAPI {
Command::Quit => vec![EditorAction::Quit],
Command::Help => {
let toggle = commands.next();
let toggle = match toggle {
Some("on") => true,
None => true,
let show = match toggle {
Some("on") | None => true,
Some("off") => false,
_ => return vec![],
};
info!("showing help");
vec![EditorAction::PopupHelp(toggle)]
if show {
vec![
EditorAction::LoadBuffer {
buffer: Some(HELP_BUFFER_ID),
source: BufferSource::Text(HELP_TEXT.to_string()),
},
EditorAction::ShowPane {
buffer: HELP_BUFFER_ID,
position: PanePosition::Floating,
},
]
} else {
vec![EditorAction::HidePane {
buffer: HELP_BUFFER_ID,
delete_buffer: true,
}]
}
}
Command::Finder => {
// TODO: Finder needs special buffer type, for now just show empty
vec![
EditorAction::LoadBuffer {
buffer: Some(FINDER_BUFFER_ID),
source: BufferSource::Empty,
},
EditorAction::ShowPane {
buffer: FINDER_BUFFER_ID,
position: PanePosition::Floating,
},
]
}
Command::Finder => vec![EditorAction::ToggleFinder(true)],
}
}
}
+6 -11
View File
@@ -1,21 +1,22 @@
use std::collections::HashMap;
use super::actions::PanePosition;
use super::buffer::BufferId;
pub(crate) struct PaneLayout {
map: HashMap<BufferId, PaneDescriptor>,
map: HashMap<BufferId, PanePosition>,
}
impl PaneLayout {
pub(super) fn new(initial_id: BufferId) -> Self {
let mut map = HashMap::new();
map.insert(initial_id, PaneDescriptor::Main { tab: 0 });
map.insert(initial_id, PanePosition::Main { tab: 0 });
Self { map }
}
pub(super) fn add(&mut self, id: BufferId, descriptor: PaneDescriptor) {
self.map.insert(id, descriptor);
pub(super) fn add(&mut self, id: BufferId, position: PanePosition) {
self.map.insert(id, position);
}
pub(super) fn remove(&mut self, id: BufferId) {
@@ -26,14 +27,8 @@ impl PaneLayout {
// TODO this needs to be ordered
self.map
.iter()
.find(|(_, item)| matches!(item, PaneDescriptor::Floating))
.find(|(_, item)| matches!(item, PanePosition::Floating))
.map(|(id, _)| id)
.copied()
}
}
#[derive(Debug)]
pub(crate) enum PaneDescriptor {
Main { tab: u16 },
Floating,
}
+1 -7
View File
@@ -11,7 +11,7 @@ use tracing::warn;
use crate::{
core::{
buffer::{Buffer, BufferId, FinderBuffer, TextBuffer},
buffer::{Buffer, BufferId, TextBuffer},
EditorCore,
},
tui::render::styles::{cursor_style, line_number_style},
@@ -60,11 +60,6 @@ impl TerminalState {
}
}
fn render_finder(&mut self, _frame: &mut Frame, _outer_rect: Rect, _buffer: &FinderBuffer) {
let _title = "FINDER";
//TODO
}
fn render_popup(&mut self, frame: &mut Frame, outer_rect: Rect, buffer: &TextBuffer) {
let title = buffer.title().unwrap_or("<no title>");
@@ -106,7 +101,6 @@ impl TerminalState {
let buf = core.get_buffer_by_id(floating);
match buf {
Some(Buffer::Text(floating)) => self.render_popup(frame, rect, floating),
Some(Buffer::Finder(finder)) => self.render_finder(frame, rect, finder),
None => {
warn!(id=?floating, "Buffer not found");
}