Skip to content

Commit 3dacee2

Browse files
trepidityclaude
andcommitted
Fix schema loading to use discovered subschemaSubentry DN and add mouse-draggable panel resizing
Schema bug: the subschemaSubentry DN discovered from the root DSE was being discarded after reading the server type. Now it is stored in ConnectionTab and passed through to load_schema(), fixing schema loading for Active Directory and other servers where the schema DN differs from the hardcoded "cn=Subschema" fallback. Panel resizing: all three panel splits (browser tree/detail, detail/command, connections tree/form) can now be resized by clicking and dragging the divider between panels. Split percentages are clamped to 10-90%. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d3414de commit 3dacee2

2 files changed

Lines changed: 147 additions & 17 deletions

File tree

crates/loom-core/src/schema.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,18 @@ impl LdapConnection {
154154
&mut self,
155155
subschema_dn: Option<&str>,
156156
) -> Result<SchemaCache, CoreError> {
157-
// Determine schema DN
157+
// Determine schema DN — prefer the one discovered from root DSE
158158
let schema_dn = match subschema_dn {
159-
Some(dn) => dn.to_string(),
160-
None => "cn=Subschema".to_string(),
159+
Some(dn) => {
160+
debug!("Loading schema from subschemaSubentry: {}", dn);
161+
dn.to_string()
162+
}
163+
None => {
164+
debug!("No subschemaSubentry found in root DSE, falling back to cn=Subschema");
165+
"cn=Subschema".to_string()
166+
}
161167
};
162168

163-
debug!("Loading schema from: {}", schema_dn);
164-
165169
let result = self
166170
.ldap
167171
.search(

crates/loom-tui/src/app.rs

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ use crate::keymap::Keymap;
4242
use crate::theme::Theme;
4343
use crate::tui;
4444

45+
/// Which divider the user is dragging.
46+
#[derive(Debug, Clone, Copy)]
47+
enum DragTarget {
48+
/// Vertical divider between tree panel and detail/command panels.
49+
Tree,
50+
/// Horizontal divider between detail panel and command panel.
51+
Detail,
52+
/// Vertical divider in connections layout.
53+
ConnTree,
54+
}
55+
4556
/// Backend for a connection tab — either live LDAP or offline/example.
4657
enum TabBackend {
4758
Live(Arc<Mutex<LdapConnection>>),
@@ -54,6 +65,7 @@ struct ConnectionTab {
5465
label: String,
5566
host: String,
5667
server_type: String,
68+
subschema_dn: Option<String>,
5769
backend: TabBackend,
5870
directory_tree: DirectoryTree,
5971
schema: Option<SchemaCache>,
@@ -116,6 +128,12 @@ pub struct App {
116128
conn_tree_area: Option<Rect>,
117129
conn_form_area: Option<Rect>,
118130

131+
// Resizable panel splits (percentages, 10..=90)
132+
tree_split_pct: u16, // tree panel width as % of content area
133+
detail_split_pct: u16, // detail panel height as % of right area
134+
conn_tree_split_pct: u16, // connections tree width as % of content area
135+
drag_target: Option<DragTarget>,
136+
119137
// Async communication
120138
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
121139
action_rx: tokio::sync::mpsc::UnboundedReceiver<Action>,
@@ -166,6 +184,10 @@ impl App {
166184
layout_bar_area: None,
167185
conn_tree_area: None,
168186
conn_form_area: None,
187+
tree_split_pct: 25,
188+
detail_split_pct: 75,
189+
conn_tree_split_pct: 30,
190+
drag_target: None,
169191
action_tx,
170192
action_rx,
171193
}
@@ -245,6 +267,7 @@ impl App {
245267
label: "Example Directory".to_string(),
246268
host: "contoso.example".to_string(),
247269
server_type: "Active Directory (Example)".to_string(),
270+
subschema_dn: None,
248271
backend: TabBackend::Offline(offline),
249272
directory_tree: DirectoryTree::new(base_dn.clone()),
250273
schema: Some(schema),
@@ -280,16 +303,16 @@ impl App {
280303
}
281304

282305
// Read RootDSE to detect server type and auto-discover base DN
283-
let server_type_str = match conn.read_root_dse().await {
306+
let (server_type_str, subschema_dn) = match conn.read_root_dse().await {
284307
Ok(root_dse) => {
285308
let st = root_dse.server_type.to_string();
286309
self.command_panel
287310
.push_message(format!("Server type: {}", st));
288-
st
311+
(st, root_dse.subschema_subentry)
289312
}
290313
Err(e) => {
291314
debug!("RootDSE read failed (non-fatal): {}", e);
292-
"LDAP".to_string()
315+
("LDAP".to_string(), None)
293316
}
294317
};
295318

@@ -310,6 +333,7 @@ impl App {
310333
label: label.clone(),
311334
host,
312335
server_type: server_type_str,
336+
subschema_dn,
313337
backend: TabBackend::Live(connection),
314338
directory_tree,
315339
schema: None,
@@ -539,9 +563,10 @@ impl App {
539563
}
540564
TabBackend::Live(connection) => {
541565
let connection = connection.clone();
566+
let subschema_dn = tab.subschema_dn.clone();
542567
tokio::spawn(async move {
543568
let mut conn = connection.lock().await;
544-
match conn.load_schema(None).await {
569+
match conn.load_schema(subschema_dn.as_deref()).await {
545570
Ok(schema) => {
546571
let _ = tx.send(Action::SchemaLoaded(conn_id, Box::new(schema)));
547572
}
@@ -980,14 +1005,21 @@ impl App {
9801005
Ok(())
9811006
}
9821007

983-
fn handle_mouse(&self, mouse: crossterm::event::MouseEvent) -> Action {
984-
// Only handle click events, ignore popups
1008+
fn handle_mouse(&mut self, mouse: crossterm::event::MouseEvent) -> Action {
1009+
// Popups block mouse events; also clear any drag
9851010
if self.popup_active() {
1011+
self.drag_target = None;
9861012
return Action::None;
9871013
}
9881014

9891015
match mouse.kind {
9901016
MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
1017+
// Check if click is on a panel divider (start drag)
1018+
if let Some(target) = self.divider_hit(mouse.column, mouse.row) {
1019+
self.drag_target = Some(target);
1020+
return Action::None;
1021+
}
1022+
9911023
let pos = Rect::new(mouse.column, mouse.row, 1, 1);
9921024

9931025
// Check layout bar clicks
@@ -1035,10 +1067,101 @@ impl App {
10351067
}
10361068
Action::None
10371069
}
1070+
MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
1071+
if let Some(target) = self.drag_target {
1072+
self.apply_drag(target, mouse.column, mouse.row);
1073+
}
1074+
Action::None
1075+
}
1076+
MouseEventKind::Up(_) => {
1077+
self.drag_target = None;
1078+
Action::None
1079+
}
10381080
_ => Action::None,
10391081
}
10401082
}
10411083

1084+
/// Check if a mouse position is on (or within 1 cell of) a panel divider.
1085+
fn divider_hit(&self, col: u16, row: u16) -> Option<DragTarget> {
1086+
match self.active_layout {
1087+
ActiveLayout::Browser => {
1088+
// Vertical divider: right edge of tree panel
1089+
if let Some(tree) = self.tree_area {
1090+
let divider_col = tree.x + tree.width;
1091+
if col.abs_diff(divider_col) <= 1
1092+
&& row >= tree.y
1093+
&& row < tree.y + tree.height
1094+
{
1095+
return Some(DragTarget::Tree);
1096+
}
1097+
}
1098+
// Horizontal divider: bottom edge of detail panel
1099+
if let Some(detail) = self.detail_area {
1100+
let divider_row = detail.y + detail.height;
1101+
if row.abs_diff(divider_row) <= 1
1102+
&& col >= detail.x
1103+
&& col < detail.x + detail.width
1104+
{
1105+
return Some(DragTarget::Detail);
1106+
}
1107+
}
1108+
}
1109+
ActiveLayout::Connections => {
1110+
// Vertical divider: right edge of connections tree
1111+
if let Some(ct) = self.conn_tree_area {
1112+
let divider_col = ct.x + ct.width;
1113+
if col.abs_diff(divider_col) <= 1
1114+
&& row >= ct.y
1115+
&& row < ct.y + ct.height
1116+
{
1117+
return Some(DragTarget::ConnTree);
1118+
}
1119+
}
1120+
}
1121+
}
1122+
None
1123+
}
1124+
1125+
/// Update split percentages based on the current drag position.
1126+
fn apply_drag(&mut self, target: DragTarget, col: u16, row: u16) {
1127+
// We need a reference area to compute the percentage from pixel position.
1128+
match target {
1129+
DragTarget::Tree => {
1130+
if let (Some(tree), Some(detail)) = (self.tree_area, self.detail_area) {
1131+
let total_w = (tree.width + detail.width) as u32;
1132+
if total_w == 0 {
1133+
return;
1134+
}
1135+
let offset = col.saturating_sub(tree.x) as u32;
1136+
let pct = ((offset * 100) / total_w) as u16;
1137+
self.tree_split_pct = pct.clamp(10, 90);
1138+
}
1139+
}
1140+
DragTarget::Detail => {
1141+
if let (Some(detail), Some(cmd)) = (self.detail_area, self.command_area) {
1142+
let total_h = (detail.height + cmd.height) as u32;
1143+
if total_h == 0 {
1144+
return;
1145+
}
1146+
let offset = row.saturating_sub(detail.y) as u32;
1147+
let pct = ((offset * 100) / total_h) as u16;
1148+
self.detail_split_pct = pct.clamp(10, 90);
1149+
}
1150+
}
1151+
DragTarget::ConnTree => {
1152+
if let (Some(ct), Some(cf)) = (self.conn_tree_area, self.conn_form_area) {
1153+
let total_w = (ct.width + cf.width) as u32;
1154+
if total_w == 0 {
1155+
return;
1156+
}
1157+
let offset = col.saturating_sub(ct.x) as u32;
1158+
let pct = ((offset * 100) / total_w) as u16;
1159+
self.conn_tree_split_pct = pct.clamp(10, 90);
1160+
}
1161+
}
1162+
}
1163+
}
1164+
10421165
async fn process_action(&mut self, action: Action) {
10431166
match action {
10441167
Action::Quit => {
@@ -1724,17 +1847,19 @@ impl App {
17241847
ActiveLayout::Browser => {
17251848
self.tab_area = Some(layout_bar_area);
17261849

1727-
// Horizontal: tree (25%) | right panels (75%)
1850+
// Horizontal: tree | right panels (draggable split)
1851+
let tp = self.tree_split_pct;
17281852
let horizontal =
1729-
Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)])
1853+
Layout::horizontal([Constraint::Percentage(tp), Constraint::Percentage(100 - tp)])
17301854
.split(content_area);
17311855

17321856
let tree_area = horizontal[0];
17331857
let right_area = horizontal[1];
17341858

1735-
// Right side: detail (75%) | command (25%)
1859+
// Right side: detail | command (draggable split)
1860+
let dp = self.detail_split_pct;
17361861
let right_vertical =
1737-
Layout::vertical([Constraint::Percentage(75), Constraint::Percentage(25)])
1862+
Layout::vertical([Constraint::Percentage(dp), Constraint::Percentage(100 - dp)])
17381863
.split(right_area);
17391864

17401865
let detail_area = right_vertical[0];
@@ -1783,9 +1908,10 @@ impl App {
17831908
let panels_area = conn_vertical[0];
17841909
let conn_status_area = conn_vertical[1];
17851910

1786-
// Horizontal: connections tree (30%) | connection form (70%)
1911+
// Horizontal: connections tree | connection form (draggable split)
1912+
let cp = self.conn_tree_split_pct;
17871913
let horizontal =
1788-
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)])
1914+
Layout::horizontal([Constraint::Percentage(cp), Constraint::Percentage(100 - cp)])
17891915
.split(panels_area);
17901916

17911917
let conn_tree_area = horizontal[0];

0 commit comments

Comments
 (0)