git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commitf2a89d57a58aa6c196f646c14254103033933ee6
parent805be06798
authorLucas Galante <[email protected]>
date2026-08-25 15:31
Revert "feat: the preview pane detaches into its own window (cce-ui RFC 7c)"

This reverts commit 805be06 — a product decision, not a defect: detach
is designer-only for now. The implementation worked and was verified
end-to-end (in a shadow session and on the live desktop), and the RFC
keeps the answers it produced — the CSD-packaging conventions and the
finding that detach dictates no new PlateSpec fields were derived from
this code and stand on the record at 805be06 for whichever app adopts
detach next.

What returns to pre-detach state: the corner menu offers Collapse only
(can_detach = false), the --preview-window mode and its sync-file
protocol are gone, and the stub band is the collapse affordance alone.
The 7c-2 plate-dock adoption (collapse) predates detach and stays.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/lib.rs            |   1 -
 src/main.rs           | 126 ++------------------
 src/preview_window.rs | 320 --------------------------------------------------
 3 files changed, 10 insertions(+), 437 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index fafc785..7c3ce77 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,5 @@
 pub mod pages;
 pub mod preview_pane;
-pub mod preview_window;
 pub mod row_list;
 pub mod services;
 pub mod util;
diff --git a/src/main.rs b/src/main.rs
index 8b416f3..45c35ad 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -364,14 +364,6 @@ struct FilesystemApp {
     /// Rows of the OPEN plate-dock corner menu (empty = not ours); routed
     /// before the generic context-menu dispatch.
     plate_menu_actions: Vec<cce_ui::widget::plate_dock::PlateDockAction>,
-    /// The detached preview window's process, while one is out (cce-ui RFC
-    /// 7c). Reaped from tick — the child's own Reattach is exiting, so a
-    /// closed window takes the pane back by the same path.
-    preview_child: Option<std::process::Child>,
-    /// The sync file the detached preview polls: parent pid, then the
-    /// selected path. Written before the spawn, rewritten per selection,
-    /// unlinked on reattach — the child exits when it disappears.
-    preview_sync_path: Option<std::path::PathBuf>,
     // Space's double-click is tracked by path, not row index: its tiles are
     // renumbered by every relayout, so an index would not survive a resize.
     last_space_click_time: std::time::Instant,
@@ -477,82 +469,13 @@ impl FilesystemApp {
                 }
                 self.preview_dock.collapsed = false;
             }
-            PlateDockAction::Detach => self.detach_preview(),
-            PlateDockAction::Reattach => self.reattach_preview(),
+            // No detach model here (yet): standard_menu is called with
+            // can_detach = false, so these rows never appear.
+            PlateDockAction::Detach | PlateDockAction::Reattach => {}
         }
         self.needs_rebuild = true;
     }
 
-    /// Move the preview pane out into its own window (cce-ui RFC 7c): write
-    /// the sync file FIRST (the designer's rule — shared state is on disk
-    /// before the child starts), spawn ourselves in `--preview-window` mode,
-    /// and stub the pane. The pane column narrows exactly as collapse does,
-    /// through the same prior-fracs machinery.
-    fn detach_preview(&mut self) {
-        if self.preview_dock.detached {
-            return;
-        }
-        let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
-        let sync = std::path::Path::new(&dir)
-            .join(format!("cce-files-preview-{}", std::process::id()));
-        if let Err(e) = self.write_preview_sync(&sync) {
-            log::error!("preview detach: cannot write sync file: {e}");
-            return;
-        }
-        let exe = match std::env::current_exe() {
-            Ok(e) => e,
-            Err(e) => {
-                log::error!("preview detach: cannot locate own executable: {e}");
-                return;
-            }
-        };
-        match std::process::Command::new(exe).arg("--preview-window").arg(&sync).spawn() {
-            Ok(child) => {
-                self.preview_child = Some(child);
-                self.preview_sync_path = Some(sync);
-                self.preview_prior_fracs =
-                    Some((self.browse_split.frac, self.network_split.frac, self.space_split.frac));
-                self.preview_dock.detached = true;
-            }
-            // Leave the pane in place if the child never started, rather than
-            // hiding it into a window that does not exist (the designer's rule).
-            Err(e) => {
-                let _ = std::fs::remove_file(&sync);
-                log::error!("preview detach: spawn failed: {e}");
-            }
-        }
-    }
-
-    /// Take the pane back. Killing the child is best-effort — a window the
-    /// user already closed is simply gone, and reattach must work anyway.
-    fn reattach_preview(&mut self) {
-        if let Some(mut child) = self.preview_child.take() {
-            let _ = child.kill();
-            // Reap, or the process table keeps a zombie for the session.
-            let _ = child.wait();
-        }
-        if let Some(p) = self.preview_sync_path.take() {
-            let _ = std::fs::remove_file(p);
-        }
-        self.preview_dock.detached = false;
-        if let Some((b, n, sp)) = self.preview_prior_fracs.take() {
-            self.browse_split.frac = b;
-            self.network_split.frac = n;
-            self.space_split.frac = sp;
-        }
-    }
-
-    /// (Re)write the detached preview's sync file: parent pid, then the
-    /// selected path when there is one.
-    fn write_preview_sync(&self, path: &std::path::Path) -> std::io::Result<()> {
-        let mut content = format!("{}\n", std::process::id());
-        if let Some(sel) = self.browse.selected_path() {
-            content.push_str(&sel.to_string_lossy());
-            content.push('\n');
-        }
-        std::fs::write(path, content)
-    }
-
     fn rebuild_layout(&mut self) {
 
         self.ui_context.clear_hierarchy();
@@ -623,7 +546,7 @@ impl FilesystemApp {
         // Collapsed preview (plate-dock, cce-ui RFC 7c-2): every page's pane
         // column narrows to the stub's width — re-forced each layout so a
         // window resize keeps the stub fixed while the list takes the rest.
-        if self.preview_dock.stubbed() {
+        if self.preview_dock.collapsed {
             for split in [&mut self.browse_split, &mut self.network_split, &mut self.space_split] {
                 let combined = (split.w - split.gap).max(1.0);
                 split.frac = (1.0 - PREVIEW_STUB_W / combined).clamp(0.0, 1.0);
@@ -666,11 +589,10 @@ impl FilesystemApp {
                         Page::Network => &self.network_split,
                         Page::Space => &self.space_split,
                     };
-                    // A stubbed preview (collapsed or detached) draws neither
-                    // divider nor content — the stub band and its corner
-                    // control paint in display_list, over everything (cce-ui
-                    // RFC 7c-2/7c).
-                    if !self.preview_dock.stubbed() {
+                    // A collapsed preview draws neither divider nor content —
+                    // the stub band and its corner control paint in
+                    // display_list, over everything (cce-ui RFC 7c-2).
+                    if !self.preview_dock.collapsed {
                         if let Some((dx, dy, dw, dh, dc)) = split.divider_quad() {
                             plain_pc.rects.push((dc, dx, dy, dw, dh, 0.0, (true, true, true, true)));
                         }
@@ -1185,8 +1107,6 @@ impl Application for FilesystemApp {
             preview_dock: Default::default(),
             preview_prior_fracs: None,
             plate_menu_actions: Vec::new(),
-            preview_child: None,
-            preview_sync_path: None,
             select_mode,
             select_directory,
             save_mode,
@@ -1325,13 +1245,6 @@ impl Application for FilesystemApp {
                 } else {
                     pages::preview::update(&mut self.preview, pages::preview::PreviewMessage::Clear);
                 }
-                // A detached preview follows the selection through its sync
-                // file (it polls; see preview_window.rs).
-                if let Some(sync) = self.preview_sync_path.clone() {
-                    if let Err(e) = self.write_preview_sync(&sync) {
-                        log::warn!("preview sync write failed: {e}");
-                    }
-                }
 
                 if let Some(idx) = self.browse.selected {
                     if let Some(entry) = self.browse.entries.get(idx) {
@@ -1481,19 +1394,6 @@ impl Application for FilesystemApp {
             *needs_rebuild = true;
             self.needs_rebuild = true;
         }
-
-        // Notice a detached preview the user closed themselves and take the
-        // pane back — its Reattach IS exiting. try_wait, NOT a signal probe:
-        // the child is ours and unreaped, so once it exits it is a zombie the
-        // probe would report alive forever (the designer's lesson, verbatim).
-        if let Some(child) = self.preview_child.as_mut() {
-            if matches!(child.try_wait(), Ok(Some(_)) | Err(_)) {
-                self.preview_child = None;
-                self.reattach_preview();
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        }
     }
 
     fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
@@ -1857,7 +1757,7 @@ impl Application for FilesystemApp {
             let band = self.preview_dock_band();
             if let Some(c) = dock::corner_center(band, self.preview_dock.stubbed()) {
                 if dock::corner_hit(c, pos.x as f32, pos.y as f32) {
-                    let rows = dock::standard_menu(self.preview_dock, true);
+                    let rows = dock::standard_menu(self.preview_dock, false);
                     let (labels, actions): (Vec<String>, Vec<_>) = rows.into_iter().unzip();
                     cce_ui::widget::context_menu::show(
                         c.0 - dock::CORNER_R,
@@ -2095,7 +1995,7 @@ impl Application for FilesystemApp {
         // focus like the legacy ctx.set_focused_ptr / release's clear_focus pair did),
         // a release ends the drag. A collapsed preview owns its column width;
         // the divider is not draggable until the pane expands.
-        if button == MouseButton::Left && !self.preview_dock.stubbed() {
+        if button == MouseButton::Left && !self.preview_dock.collapsed {
             let split = match self.current_page {
                 Page::Browse => &mut self.browse_split,
                 Page::Network => &mut self.network_split,
@@ -2537,11 +2437,5 @@ impl Application for FilesystemApp {
 
 #[tokio::main]
 async fn main() {
-    // The detached preview window (cce-ui RFC 7c) — same binary, second
-    // Application; see the lib's preview_window.rs for the process/sync model.
-    if std::env::args().any(|a| a == "--preview-window") {
-        cce_ui::engine::run::<cce_files::preview_window::PreviewWindowApp>();
-        return;
-    }
     cce_ui::engine::run::<FilesystemApp>();
 }
diff --git a/src/preview_window.rs b/src/preview_window.rs
deleted file mode 100644
index 98a5bf1..0000000
--- a/src/preview_window.rs
+++ /dev/null
@@ -1,320 +0,0 @@
-//! The detached preview window — cce-files run as `--preview-window <sync-file>`
-//! (cce-ui RFC 7c: the first non-designer detach).
-//!
-//! Process model (app policy, mirroring the designer's): the parent writes the
-//! sync file BEFORE spawning this process and rewrites it on every selection
-//! change; this window polls it. Reattach is EXIT — the parent `try_wait`s its
-//! child from tick and takes the pane back when it goes, so closing this
-//! window by any means (corner menu, compositor close, crash) reattaches. The
-//! same rule runs in reverse: this process exits when the sync file disappears
-//! (the parent reattached or quit) or the parent pid is gone (it crashed, or a
-//! session restore respawned us without one) — a preview with no parent is an
-//! orphan, not a window worth keeping.
-//!
-//! Sync-file format, one field per line: parent pid, then the selected path
-//! (absent while nothing is selected).
-
-use crate::preview_pane::PreviewPane;
-use cce_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings};
-use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
-use wayland_client::QueueHandle;
-use cce_ui::widget::plate_dock as dock;
-
-/// Poll cadence for the sync file. Selection changes are user-paced; 100ms is
-/// invisible against the parent's own preview-load latency.
-const POLL_S: f32 = 0.1;
-
-/// One menu row's height — matches the toolkit context menu's row pitch.
-const MENU_ROW_H: f32 = 24.0;
-const MENU_W: f32 = 120.0;
-
-pub struct PreviewWindowApp {
-    preview: PreviewPane,
-    fs_service: crate::services::fs::FsService,
-    sync_path: std::path::PathBuf,
-    parent_pid: Option<u32>,
-    shown_path: Option<std::path::PathBuf>,
-    poll_accum: f32,
-    width: u32,
-    height: u32,
-    cursor: (f32, f32),
-    /// The Reattach menu (drawn by this app — the toolkit context menu
-    /// dispatches through a widget tree this window does not have): the
-    /// top-left of the open menu, `None` while closed.
-    menu_at: Option<(f32, f32)>,
-    needs_rebuild: bool,
-}
-
-impl PreviewWindowApp {
-    /// The rect the pane fills and the corner control anchors to.
-    fn pane_rect(&self) -> (f32, f32, f32, f32) {
-        let pad = cce_ui::layout::root_plate_padding();
-        (
-            pad,
-            pad,
-            (self.width as f32 - 2.0 * pad).max(1.0),
-            (self.height as f32 - 2.0 * pad).max(1.0),
-        )
-    }
-
-    fn dock_state() -> dock::PlateDockState {
-        dock::PlateDockState { collapsed: false, detached: true }
-    }
-
-    /// Re-read the sync file; exit when it (or the parent) is gone.
-    fn poll_sync(&mut self, needs_rebuild: &mut bool) {
-        let content = match std::fs::read_to_string(&self.sync_path) {
-            Ok(c) => c,
-            // The parent reattached (it unlinks on reattach and on its own
-            // exit path) or never existed (stale session restore).
-            Err(_) => std::process::exit(0),
-        };
-        let mut lines = content.lines();
-        if self.parent_pid.is_none() {
-            self.parent_pid = lines.next().and_then(|l| l.trim().parse().ok());
-        } else {
-            lines.next();
-        }
-        if let Some(pid) = self.parent_pid {
-            if !std::path::Path::new(&format!("/proc/{pid}")).exists() {
-                std::process::exit(0);
-            }
-        }
-        let path = lines.next().map(str::trim).filter(|l| !l.is_empty());
-        let path = path.map(std::path::PathBuf::from);
-        if path != self.shown_path {
-            self.shown_path = path.clone();
-            match path {
-                Some(p) => self
-                    .fs_service
-                    .send(crate::services::fs::FsRequest::ReadPreview(p)),
-                None => crate::pages::preview::update(
-                    &mut self.preview,
-                    crate::pages::preview::PreviewMessage::Clear,
-                ),
-            }
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-}
-
-impl Application for PreviewWindowApp {
-    type Message = crate::Message;
-
-    fn new(
-        _qh: &QueueHandle<cce_ui::engine::EngineState<Self>>,
-        sender: calloop::channel::Sender<Self::Message>,
-    ) -> Self {
-        let args: Vec<String> = std::env::args().collect();
-        let sync_path = args
-            .iter()
-            .position(|a| a == "--preview-window")
-            .and_then(|i| args.get(i + 1))
-            .map(std::path::PathBuf::from)
-            .unwrap_or_default();
-        cce_ui::scale::set_scale_factor(1.0);
-        cce_ui::scale::set_app_id("cce-files-preview".to_string());
-        let mut app = Self {
-            preview: PreviewPane::default(),
-            fs_service: crate::services::fs::FsService::new(sender),
-            sync_path,
-            parent_pid: None,
-            shown_path: None,
-            poll_accum: 0.0,
-            width: 460,
-            height: 680,
-            cursor: (0.0, 0.0),
-            menu_at: None,
-            needs_rebuild: true,
-        };
-        // First read now, not a poll tick later: the parent wrote the file
-        // before spawning, so the pane has content on its first frame.
-        let mut rb = false;
-        app.poll_sync(&mut rb);
-        app
-    }
-
-    fn settings(&self) -> WindowSettings {
-        WindowSettings {
-            title: "Preview".to_string(),
-            // The cce- prefix keys the compositor's decorated-window treatment
-            // (silhouette clip, blur-behind, shadow), same as the parent.
-            app_id: "cce-files-preview".to_string(),
-            width: 460,
-            height: 680,
-            fullscreen: false,
-            min_size: Some((280, 320)),
-        }
-    }
-
-    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, _exit: &mut bool) {
-        if let crate::Message::Preview(m) = msg {
-            crate::pages::preview::update(&mut self.preview, m);
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
-        self.poll_accum += dt;
-        if self.poll_accum >= POLL_S {
-            self.poll_accum = 0.0;
-            self.poll_sync(needs_rebuild);
-        }
-    }
-
-    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
-        let (px, py) = (pos.x as f32, pos.y as f32);
-        // Redraw across the dot's emphasis flips, cheaply: only when hover
-        // over the control changes, not on every motion.
-        let before = dock::corner_center(self.pane_rect(), false)
-            .map(|c| dock::corner_hit(c, self.cursor.0, self.cursor.1));
-        let after = dock::corner_center(self.pane_rect(), false)
-            .map(|c| dock::corner_hit(c, px, py));
-        self.cursor = (px, py);
-        if before != after {
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-        }
-    }
-
-    fn handle_mouse_input(
-        &mut self,
-        button: MouseButton,
-        state: ElementState,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) -> Option<Self::Message> {
-        if button != MouseButton::Left || state != ElementState::Released {
-            return None;
-        }
-        let (px, py) = (pos.x as f32, pos.y as f32);
-        if let Some((mx, my)) = self.menu_at {
-            // One row: Reattach — which for the detached window IS exit; the
-            // parent reaps the child and takes the pane back (see module doc).
-            if px >= mx && px <= mx + MENU_W && py >= my && py <= my + MENU_ROW_H {
-                std::process::exit(0);
-            }
-            self.menu_at = None;
-            *needs_rebuild = true;
-            self.needs_rebuild = true;
-            return None;
-        }
-        if let Some(c) = dock::corner_center(self.pane_rect(), false) {
-            if dock::corner_hit(c, px, py) {
-                self.menu_at = Some((c.0 - dock::CORNER_R, c.1 + dock::CORNER_R));
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        }
-        None
-    }
-
-    fn handle_mouse_wheel(
-        &mut self,
-        delta: &MouseScrollDelta,
-        pos: LogicalPosition,
-        needs_rebuild: &mut bool,
-    ) {
-        if let Some(changed) = self.preview.wheel(delta, pos.x as f32, pos.y as f32) {
-            if changed {
-                *needs_rebuild = true;
-                self.needs_rebuild = true;
-            }
-        }
-    }
-
-    fn handle_key_input(&mut self, _event: &KeyEvent, _needs_rebuild: &mut bool) -> Option<Self::Message> {
-        None
-    }
-
-    fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
-        use cce_ui::scene::layout::Rect;
-        self.width = size.width as u32;
-        self.height = size.height as u32;
-        cce_ui::scale::set_scale_factor(scale as f32);
-
-        let mut pc = cce_ui::scene::paint::PaintCtx::new();
-
-        // The root plate: a detached pane's plate IS the window's base surface
-        // (PlateSpec::detached's role flip) — all four corners on the
-        // silhouette, fill mirroring the parent's root plate.
-        let mut plate = cce_ui::color::page_low_color();
-        if plate[3] > 0.001 {
-            plate[3] = cce_ui::color::root_plate_opacity();
-        }
-        pc.plate_spec(&cce_ui::scene::paint::PlateSpec {
-            rect: Rect { x: 0.0, y: 0.0, width: size.width as f32, height: size.height as f32 },
-            color: plate,
-            blur: false,
-            window_corners: (true, true, true, true),
-            depth: cce_ui::layout::bevel_width(),
-        });
-
-        // The pane, through its own PageContent, bridged onto the PaintCtx via
-        // the RenderTarget methods (PaintCtx implements the trait) — the small
-        // flat subset of the parent's page bridge, with no viewport clipping
-        // because this window IS the pane.
-        let (rx, ry, rw, rh) = self.pane_rect();
-        self.preview.set_rect(rx, ry, rw, rh);
-        let mut content = crate::pages::PageContent::new();
-        self.preview.push_prims(&mut content);
-        {
-            use cce_ui::layout::RenderTarget;
-            let crate::pages::PageContent { rects, texts, buttons: _, reliefs, grooves, images } = content;
-            for (c, x, y, w, h, r, corners) in rects {
-                pc.rect_with_radius_corners(c, x, y, w, h, r, corners);
-            }
-            for (x, y, w, h, radius, depth, kind) in reliefs {
-                let rect = Rect { x, y, width: w, height: h };
-                let radii = (radius, radius, radius, radius);
-                match kind {
-                    crate::pages::RELIEF_RAISED => pc.boss(rect, radii, depth),
-                    _ => pc.recess(rect, radii, depth),
-                }
-            }
-            for (ax, ay, bx, by, w, d, hx, hy, hw, hh) in grooves {
-                pc.groove((ax, ay), (bx, by), w, d, Rect { x: hx, y: hy, width: hw, height: hh });
-            }
-            for (id, x, y, w, h, alpha) in images {
-                pc.image(id, Rect { x, y, width: w, height: h }, alpha);
-            }
-            for (text, sz, x, y, col, font, bounds) in texts {
-                match font {
-                    Some(f) => pc.text_with_font_and_bounds(&text, x, y, sz, col, &f, bounds),
-                    None => pc.text_with_bounds(&text, x, y, sz, col, bounds),
-                }
-            }
-        }
-
-        // The corner control, over everything the pane drew.
-        if let Some(c) = dock::corner_center((rx, ry, rw, rh), false) {
-            let emphasized = dock::corner_hit(c, self.cursor.0, self.cursor.1) || self.menu_at.is_some();
-            dock::draw_corner_dot(&mut pc, c, emphasized);
-        }
-
-        // The one-row Reattach menu.
-        if let Some((mx, my)) = self.menu_at {
-            let rows = dock::standard_menu(Self::dock_state(), false);
-            pc.quad(Rect { x: mx, y: my, width: MENU_W, height: MENU_ROW_H * rows.len() as f32 },
-                cce_ui::color::popover_bg_color());
-            for (i, (label, _)) in rows.iter().enumerate() {
-                let hy = my + i as f32 * MENU_ROW_H;
-                if self.cursor.0 >= mx && self.cursor.0 <= mx + MENU_W
-                    && self.cursor.1 >= hy && self.cursor.1 <= hy + MENU_ROW_H
-                {
-                    pc.quad(Rect { x: mx, y: hy, width: MENU_W, height: MENU_ROW_H },
-                        [0.20, 0.40, 0.65, 0.6]);
-                }
-                pc.text(label, mx + 8.0, hy + 6.0, 12.0, [204, 204, 217]);
-            }
-        }
-
-        Some(pc.finish())
-    }
-
-    fn display_list_text(&self) -> bool {
-        true
-    }
-}