diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -906,8 +906,10 @@ dependencies = [
  "log",
  "rfd",
  "serde_json",
+ "wayland-client",
  "winit",
  "winresource",
+ "x11rb",
 ]
 
 [[package]]
diff --git a/apps/deckcraft/Cargo.toml b/apps/deckcraft/Cargo.toml
--- a/apps/deckcraft/Cargo.toml
+++ b/apps/deckcraft/Cargo.toml
@@ -38,3 +38,6 @@ fmv-macos-events = "=0.1.0"
 
 [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
 winit = { version = "0.30", default-features = false, features = ["x11"] }
+# Counting displays for Presenter View (src/displays.rs); both already come with winit.
+wayland-client = "0.31"
+x11rb = { version = "0.13", features = ["randr"] }
diff --git a/apps/deckcraft/src/displays.rs b/apps/deckcraft/src/displays.rs
new file mode 100644
--- /dev/null
+++ b/apps/deckcraft/src/displays.rs
@@ -0,0 +1,51 @@
+//! How many displays are connected (Presenter View needs a second one), asked of the display
+//! server the window uses: Wayland's outputs or X11's RandR monitors. `None` when it can't tell.
+
+/// The connected displays, through X11 when `x11` (the window's backend) else Wayland.
+pub fn count(x11: bool) -> Option<usize> {
+    let n = if x11 { x11_monitors() } else { wayland_outputs() };
+    n.filter(|n| *n > 0)
+}
+
+fn wayland_outputs() -> Option<usize> {
+    use wayland_client::protocol::wl_registry;
+    use wayland_client::{Connection, Dispatch, QueueHandle};
+
+    struct Outputs(usize);
+    impl Dispatch<wl_registry::WlRegistry, ()> for Outputs {
+        fn event(state: &mut Self, _: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
+            if let wl_registry::Event::Global { interface, .. } = event
+                && interface == "wl_output"
+            {
+                state.0 += 1;
+            }
+        }
+    }
+
+    let conn = Connection::connect_to_env().ok()?;
+    let mut queue = conn.new_event_queue();
+    let _registry = conn.display().get_registry(&queue.handle(), ());
+    let mut outputs = Outputs(0);
+    queue.roundtrip(&mut outputs).ok()?;
+    Some(outputs.0)
+}
+
+fn x11_monitors() -> Option<usize> {
+    use x11rb::connection::Connection;
+    use x11rb::protocol::randr::ConnectionExt;
+
+    let (conn, screen) = x11rb::connect(None).ok()?;
+    let root = conn.setup().roots.get(screen)?.root;
+    let reply = conn.randr_get_monitors(root, true).ok()?.reply().ok()?;
+    Some(reply.monitors.len())
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn counting_never_fails_without_a_display_server() {
+        // CI has no display: the count is unknown, never a crash.
+        let _ = super::count(true);
+        let _ = super::count(false);
+    }
+}
diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs
--- a/apps/deckcraft/src/main.rs
+++ b/apps/deckcraft/src/main.rs
@@ -20,6 +20,8 @@ mod audio;
 mod control_server;
 #[cfg(all(unix, not(target_os = "macos")))]
 mod display;
+#[cfg(all(unix, not(target_os = "macos")))]
+mod displays;
 #[cfg(any(target_os = "windows", test))]
 mod graphics;
 mod logging;
@@ -256,6 +258,11 @@ fn main() -> eframe::Result {
         options,
         Box::new(move |cc| {
             let mut app = SlideApp::new(Session::new(), services());
+            // Presenter View asks the display server the window is on how many displays there are.
+            #[cfg(all(unix, not(target_os = "macos")))]
+            {
+                app.services.displays = Some(Box::new(move || displays::count(x11)));
+            }
             load_prefs(&mut app);
             // AutoRecover: reopen what a previous run left unsaved, then keep it current.
             if std::env::var_os("DECKCRAFT_NO_RECOVERY").is_none() {
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -220,6 +220,8 @@ pub struct Services {
     pub clipboard_set_html: Option<Box<dyn FnMut(&str, &str) -> bool>>,
     /// Audio output for media playback (cpal on desktop); without one media plays silently.
     pub audio_out: Option<Box<dyn deckcraft_media::AudioOut>>,
+    /// How many displays are connected, when the host can tell: Presenter View needs a second.
+    pub displays: Option<Box<dyn FnMut() -> Option<usize>>>,
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
@@ -1075,8 +1077,9 @@ impl SlideApp {
     pub fn start_show(&mut self, from: usize, reading: bool) {
         self.anim_preview = None;
         self.media.stop_owned(media::Owner::Editor);
+        let presenter = self.ui.presenter_view && !reading && self.second_display();
         if let Some(d) = self.session.active() {
-            self.show = Some(show::Show::new(&d.doc, from, reading, self.ui.presenter_view && !reading));
+            self.show = Some(show::Show::new(&d.doc, from, reading, presenter));
         }
     }
 
@@ -1169,6 +1172,14 @@ impl SlideApp {
         Ok(json!({"playing": true, "slide": slide, "duration": duration}))
     }
 
+    /// Whether a second display can take Presenter View. Like PowerPoint, one display shows only
+    /// the slides: the presenter window would open on the same screen, and whichever of the two
+    /// windows ends up behind the other stalls the show (the compositor stops pacing a hidden
+    /// window). Hosts that can't count displays keep Presenter View.
+    fn second_display(&mut self) -> bool {
+        self.services.displays.as_mut().and_then(|f| f()).is_none_or(|n| n >= 2)
+    }
+
     fn pick_and_open(&mut self) {
         if let Some(pick) = self.services.pick_open.as_mut() {
             if let Some(path) = pick("open") {
@@ -2203,6 +2214,26 @@ mod tests {
         assert_eq!(old.qat.len(), 6);
     }
 
+    #[test]
+    fn presenter_view_needs_a_second_display() {
+        // One display: the presenter window covered the show (or the show covered it) and the
+        // hidden window stalled every frame.
+        let presenter = |displays: Option<usize>| {
+            let services = Services { displays: Some(Box::new(move || displays)), ..Default::default() };
+            let mut app = SlideApp::new(Session::with_new(), services);
+            app.ui.presenter_view = true;
+            app.start_show(0, false);
+            app.show.as_ref().map(|s| s.presenter)
+        };
+        assert_eq!(presenter(Some(1)), Some(false));
+        assert_eq!(presenter(Some(2)), Some(true));
+        assert_eq!(presenter(None), Some(true), "a host that can't count displays keeps Presenter View");
+        let mut app = SlideApp::new(Session::with_new(), Services { displays: Some(Box::new(|| Some(2))), ..Default::default() });
+        app.ui.presenter_view = false;
+        app.start_show(0, false);
+        assert_eq!(app.show.as_ref().map(|s| s.presenter), Some(false), "the setting still turns it off");
+    }
+
     #[test]
     fn opening_a_file_with_missing_fonts_shows_a_toast_once() {
         use egui_kittest::kittest::Queryable;
