open_sesame/app/
mod.rs

1//! Application orchestration
2//!
3//! Clean architecture with:
4//! - Pure state machine (state.rs)
5//! - Frame-callback rendering (renderer.rs)
6//! - Thin Wayland integration layer (this file)
7
8mod renderer;
9mod state;
10
11pub use state::{ActivationResult, AppState};
12
13use crate::config::Config;
14use crate::core::WindowHint;
15use crate::util::{IpcCommand, IpcServer, Result};
16use renderer::Renderer;
17use smithay_client_toolkit::{
18    compositor::{CompositorHandler, CompositorState},
19    delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_registry,
20    delegate_seat, delegate_shm,
21    output::{OutputHandler, OutputState},
22    registry::{ProvidesRegistryState, RegistryState},
23    registry_handlers,
24    seat::{
25        Capability, SeatHandler, SeatState,
26        keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers},
27    },
28    shell::{
29        WaylandSurface,
30        wlr_layer::{
31            Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
32            LayerSurfaceConfigure,
33        },
34    },
35    shm::{Shm, ShmHandler},
36};
37use state::{Action, Event, Transition};
38use std::sync::Arc;
39use wayland_client::{
40    Connection, QueueHandle,
41    globals::registry_queue_init,
42    protocol::{wl_keyboard, wl_output, wl_seat, wl_surface},
43};
44
45/// Main application - thin wrapper around state machine
46pub struct App {
47    // Wayland state
48    registry_state: RegistryState,
49    seat_state: SeatState,
50    output_state: OutputState,
51    compositor_state: CompositorState,
52    layer_shell: LayerShell,
53    shm: Shm,
54
55    // Application state machine
56    state: AppState,
57    config: Arc<Config>,
58    hints: Vec<WindowHint>,
59    previous_window_id: Option<String>,
60
61    // Rendering
62    renderer: Renderer,
63    layer_surface: Option<LayerSurface>,
64
65    // Wayland event loop control
66    running: bool,
67
68    // Modifier state tracking for Alt release detection
69    alt_held: bool,
70    shift_held: bool,
71
72    // IPC server for receiving commands from other instances
73    ipc_server: Option<IpcServer>,
74}
75
76impl App {
77    /// Create and run the application
78    pub fn run(
79        config: Config,
80        hints: Vec<WindowHint>,
81        previous_window_id: Option<String>,
82        launcher_mode: bool,
83        ipc_server: Option<IpcServer>,
84    ) -> Result<Option<(usize, String)>> {
85        let conn = Connection::connect_to_env()
86            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
87
88        let (globals, mut event_queue) = registry_queue_init(&conn)
89            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
90        let qh = event_queue.handle();
91
92        let registry_state = RegistryState::new(&globals);
93        let seat_state = SeatState::new(&globals, &qh);
94        let output_state = OutputState::new(&globals, &qh);
95        let compositor_state = CompositorState::bind(&globals, &qh)
96            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
97        let shm = Shm::bind(&globals, &qh)
98            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
99        let layer_shell = LayerShell::bind(&globals, &qh)
100            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
101
102        let config = Arc::new(config);
103
104        tracing::info!(
105            "App::run starting with {} hints, mode={}",
106            hints.len(),
107            if launcher_mode {
108                "launcher"
109            } else {
110                "switcher"
111            }
112        );
113        tracing::info!("  Previous window: {:?}", previous_window_id);
114        for (i, hint) in hints.iter().enumerate() {
115            tracing::info!(
116                "  Hint[{}]: {} -> {} ({})",
117                i,
118                hint.hint,
119                hint.app_id,
120                hint.window_id
121            );
122        }
123
124        let initial_state = AppState::initial(launcher_mode, &hints, previous_window_id.as_deref());
125
126        let mut app = App {
127            registry_state,
128            seat_state,
129            output_state,
130            compositor_state,
131            layer_shell,
132            shm,
133            state: initial_state,
134            config,
135            hints,
136            previous_window_id,
137            renderer: Renderer::new(),
138            layer_surface: None,
139            running: true,
140            alt_held: !launcher_mode, // Alt held state initialized based on mode (switcher assumes held)
141            shift_held: false,
142            ipc_server,
143        };
144
145        // Create layer surface
146        app.create_layer_surface(&qh);
147        tracing::info!("Layer surface created");
148
149        // Event loop
150        let mut event_loop = calloop::EventLoop::try_new()
151            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
152        let loop_handle = event_loop.handle();
153        tracing::info!("Event loop created");
154
155        // Insert Wayland source
156        loop_handle
157            .insert_source(
158                calloop::generic::Generic::new(conn, calloop::Interest::READ, calloop::Mode::Level),
159                move |_, conn, app: &mut App| {
160                    if let Some(guard) = conn.prepare_read() {
161                        match guard.read() {
162                            Ok(_) => {}
163                            Err(wayland_client::backend::WaylandError::Io(io_err))
164                                if io_err.kind() == std::io::ErrorKind::WouldBlock =>
165                            {
166                                // EAGAIN is normal - just means no data ready
167                            }
168                            Err(e) => {
169                                tracing::error!("Wayland read error: {}. Shutting down.", e);
170                                app.running = false;
171                                return Ok(calloop::PostAction::Remove);
172                            }
173                        }
174                    }
175                    Ok(calloop::PostAction::Continue)
176                },
177            )
178            .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
179
180        tracing::info!("Entering event loop, running={}", app.running);
181        let mut loop_count = 0u64;
182        while app.running {
183            loop_count += 1;
184            if loop_count <= 5 || loop_count.is_multiple_of(100) {
185                tracing::debug!("Event loop iteration {}", loop_count);
186            }
187
188            // Dispatch pending Wayland events
189            event_queue
190                .dispatch_pending(&mut app)
191                .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
192
193            // Flush outgoing requests
194            event_queue
195                .flush()
196                .map_err(|e| crate::util::Error::WaylandConnection(Box::new(e)))?;
197
198            // Process tick event (for timeouts)
199            app.process_event(Event::Tick, &qh);
200
201            // Process IPC commands from other instances
202            // Commands collected before processing to avoid borrow conflict
203            let ipc_commands: Vec<IpcCommand> = app
204                .ipc_server
205                .as_ref()
206                .map(|s| {
207                    let mut cmds = Vec::new();
208                    while let Some(cmd) = s.try_recv() {
209                        cmds.push(cmd);
210                    }
211                    cmds
212                })
213                .unwrap_or_default();
214
215            for cmd in ipc_commands {
216                match cmd {
217                    IpcCommand::CycleForward => {
218                        tracing::info!("IPC: FORWARD command received");
219                        app.process_event(Event::CycleForward, &qh);
220                    }
221                    IpcCommand::CycleBackward => {
222                        tracing::info!("IPC: BACKWARD command received");
223                        app.process_event(Event::CycleBackward, &qh);
224                    }
225                    IpcCommand::Ping => {
226                        // Ping is handled by the server automatically
227                    }
228                }
229            }
230
231            // Render if needed
232            if app.renderer.needs_redraw() {
233                app.draw(&qh);
234            }
235
236            // Poll for events (10ms timeout)
237            event_loop
238                .dispatch(std::time::Duration::from_millis(10), &mut app)
239                .ok();
240        }
241        tracing::info!("Exited event loop after {} iterations", loop_count);
242
243        // Log exit
244        tracing::info!("BORDER DEACTIVATING");
245
246        // Return result based on final state
247        match app.state.activation_result() {
248            Some(ActivationResult::Window(idx)) if *idx < app.hints.len() => {
249                let hint = &app.hints[*idx];
250                tracing::info!("Activating window: {} ({})", hint.app_id, hint.window_id);
251                Ok(Some((*idx, hint.window_id.to_string())))
252            }
253            Some(ActivationResult::Window(idx)) => {
254                // Index out of bounds - fallback to first window
255                tracing::warn!("Window index {} out of bounds, falling back", idx);
256                if !app.hints.is_empty() {
257                    let hint = &app.hints[0];
258                    Ok(Some((0, hint.window_id.to_string())))
259                } else {
260                    Ok(None)
261                }
262            }
263            Some(ActivationResult::QuickSwitch) => {
264                // Quick switch to previous or first window
265                if let Some(ref prev_id) = app.previous_window_id
266                    && let Some((idx, hint)) = app
267                        .hints
268                        .iter()
269                        .enumerate()
270                        .find(|(_, h)| h.window_id.as_str() == prev_id)
271                {
272                    tracing::info!("Quick switch to: {} ({})", hint.app_id, hint.window_id);
273                    return Ok(Some((idx, hint.window_id.to_string())));
274                }
275                // Fallback to first
276                if !app.hints.is_empty() {
277                    let hint = &app.hints[0];
278                    tracing::info!(
279                        "Quick switch fallback: {} ({})",
280                        hint.app_id,
281                        hint.window_id
282                    );
283                    Ok(Some((0, hint.window_id.to_string())))
284                } else {
285                    Ok(None)
286                }
287            }
288            Some(ActivationResult::Launch(key)) => {
289                tracing::info!("Launching: {}", key);
290                Ok(Some((usize::MAX, key.clone())))
291            }
292            Some(ActivationResult::Cancelled) => {
293                tracing::info!("Cancelled");
294                Ok(None)
295            }
296            None => {
297                tracing::info!("No result");
298                Ok(None)
299            }
300        }
301    }
302
303    /// Process an event through the state machine
304    fn process_event(&mut self, event: Event, qh: &QueueHandle<Self>) {
305        let Transition { new_state, actions } = self.state.handle_event(
306            event,
307            &self.config,
308            &self.hints,
309            self.previous_window_id.as_deref(),
310        );
311
312        self.state = new_state;
313
314        let had_actions = !actions.is_empty();
315        for action in actions {
316            match action {
317                Action::ScheduleRedraw => {
318                    self.renderer.schedule_redraw();
319                }
320                Action::Exit => {
321                    self.running = false;
322                }
323            }
324        }
325
326        // Redraw triggered when state transitions produce visual changes
327        if had_actions {
328            self.draw(qh);
329        }
330    }
331
332    /// Create the layer surface
333    fn create_layer_surface(&mut self, qh: &QueueHandle<Self>) {
334        let surface = self.compositor_state.create_surface(qh);
335
336        let layer_surface = self.layer_shell.create_layer_surface(
337            qh,
338            surface,
339            Layer::Overlay,
340            Some("sesame"),
341            None,
342        );
343
344        layer_surface.set_anchor(Anchor::all());
345        layer_surface.set_exclusive_zone(-1);
346        layer_surface.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
347        layer_surface.commit();
348
349        self.layer_surface = Some(layer_surface);
350    }
351
352    /// Draw current state
353    fn draw(&mut self, _qh: &QueueHandle<Self>) {
354        let Some(layer_surface) = &self.layer_surface else {
355            return;
356        };
357
358        let show_full = self.state.is_full_overlay();
359        let selected = self.state.selected_hint_index();
360        let input = self.state.input();
361
362        if let Some(result) = self.renderer.render(
363            &self.shm,
364            &self.config,
365            &self.hints,
366            input,
367            selected,
368            show_full,
369        ) {
370            layer_surface
371                .wl_surface()
372                .attach(Some(result.buffer.wl_buffer()), 0, 0);
373            layer_surface
374                .wl_surface()
375                .damage_buffer(0, 0, result.width, result.height);
376            layer_surface.commit();
377
378            tracing::debug!(
379                "Frame rendered: {}x{}, full={}, selected={}",
380                result.width,
381                result.height,
382                show_full,
383                selected
384            );
385        }
386    }
387}
388
389// === Wayland protocol implementations ===
390
391impl CompositorHandler for App {
392    fn scale_factor_changed(
393        &mut self,
394        _conn: &Connection,
395        _qh: &QueueHandle<Self>,
396        _surface: &wl_surface::WlSurface,
397        new_factor: i32,
398    ) {
399        self.renderer.set_scale(new_factor as f32);
400    }
401
402    fn transform_changed(
403        &mut self,
404        _conn: &Connection,
405        _qh: &QueueHandle<Self>,
406        _surface: &wl_surface::WlSurface,
407        _new_transform: wl_output::Transform,
408    ) {
409    }
410
411    fn frame(
412        &mut self,
413        _conn: &Connection,
414        qh: &QueueHandle<Self>,
415        _surface: &wl_surface::WlSurface,
416        _time: u32,
417    ) {
418        // Frame callback - process through state machine
419        self.process_event(Event::FrameCallback, qh);
420    }
421
422    fn surface_enter(
423        &mut self,
424        _conn: &Connection,
425        _qh: &QueueHandle<Self>,
426        _surface: &wl_surface::WlSurface,
427        _output: &wl_output::WlOutput,
428    ) {
429    }
430
431    fn surface_leave(
432        &mut self,
433        _conn: &Connection,
434        _qh: &QueueHandle<Self>,
435        _surface: &wl_surface::WlSurface,
436        _output: &wl_output::WlOutput,
437    ) {
438    }
439}
440
441impl OutputHandler for App {
442    fn output_state(&mut self) -> &mut OutputState {
443        &mut self.output_state
444    }
445
446    fn new_output(
447        &mut self,
448        _conn: &Connection,
449        _qh: &QueueHandle<Self>,
450        _output: wl_output::WlOutput,
451    ) {
452    }
453    fn update_output(
454        &mut self,
455        _conn: &Connection,
456        _qh: &QueueHandle<Self>,
457        _output: wl_output::WlOutput,
458    ) {
459    }
460    fn output_destroyed(
461        &mut self,
462        _conn: &Connection,
463        _qh: &QueueHandle<Self>,
464        _output: wl_output::WlOutput,
465    ) {
466    }
467}
468
469impl LayerShellHandler for App {
470    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
471        self.running = false;
472    }
473
474    fn configure(
475        &mut self,
476        _conn: &Connection,
477        qh: &QueueHandle<Self>,
478        layer: &LayerSurface,
479        configure: LayerSurfaceConfigure,
480        _serial: u32,
481    ) {
482        tracing::info!(
483            "CONFIGURE EVENT: {}x{}",
484            configure.new_size.0,
485            configure.new_size.1
486        );
487
488        self.renderer
489            .configure(configure.new_size.0, configure.new_size.1);
490        layer.set_size(configure.new_size.0, configure.new_size.1);
491
492        // Process configure as an event
493        self.process_event(
494            Event::Configure {
495                width: configure.new_size.0,
496                height: configure.new_size.1,
497            },
498            qh,
499        );
500
501        // Initial draw
502        self.draw(qh);
503        tracing::info!("CONFIGURE done, draw called");
504    }
505}
506
507impl SeatHandler for App {
508    fn seat_state(&mut self) -> &mut SeatState {
509        &mut self.seat_state
510    }
511
512    fn new_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {}
513
514    fn new_capability(
515        &mut self,
516        _conn: &Connection,
517        qh: &QueueHandle<Self>,
518        seat: wl_seat::WlSeat,
519        capability: Capability,
520    ) {
521        if capability == Capability::Keyboard
522            && let Err(e) = self.seat_state.get_keyboard(qh, &seat, None)
523        {
524            tracing::error!("Failed to get keyboard: {}", e);
525        }
526    }
527
528    fn remove_capability(
529        &mut self,
530        _conn: &Connection,
531        _qh: &QueueHandle<Self>,
532        _seat: wl_seat::WlSeat,
533        _capability: Capability,
534    ) {
535    }
536
537    fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _seat: wl_seat::WlSeat) {
538    }
539}
540
541impl KeyboardHandler for App {
542    fn enter(
543        &mut self,
544        _conn: &Connection,
545        _qh: &QueueHandle<Self>,
546        _keyboard: &wl_keyboard::WlKeyboard,
547        _surface: &wl_surface::WlSurface,
548        _serial: u32,
549        raw: &[u32],
550        keysyms: &[Keysym],
551    ) {
552        tracing::info!(
553            "KEYBOARD ENTER: {} raw keys, {} keysyms",
554            raw.len(),
555            keysyms.len()
556        );
557    }
558
559    fn leave(
560        &mut self,
561        _conn: &Connection,
562        _qh: &QueueHandle<Self>,
563        _keyboard: &wl_keyboard::WlKeyboard,
564        _surface: &wl_surface::WlSurface,
565        _serial: u32,
566    ) {
567        tracing::info!("KEYBOARD LEAVE");
568    }
569
570    fn press_key(
571        &mut self,
572        _conn: &Connection,
573        qh: &QueueHandle<Self>,
574        _keyboard: &wl_keyboard::WlKeyboard,
575        _serial: u32,
576        event: KeyEvent,
577    ) {
578        tracing::info!(
579            "KEY PRESS: keysym={:?} raw={:#x} shift={}",
580            event.keysym,
581            event.keysym.raw(),
582            self.shift_held
583        );
584
585        self.process_event(
586            Event::KeyPress {
587                keysym: event.keysym,
588                shift: self.shift_held,
589            },
590            qh,
591        );
592    }
593
594    fn release_key(
595        &mut self,
596        _conn: &Connection,
597        _qh: &QueueHandle<Self>,
598        _keyboard: &wl_keyboard::WlKeyboard,
599        _serial: u32,
600        _event: KeyEvent,
601    ) {
602    }
603
604    fn repeat_key(
605        &mut self,
606        _conn: &Connection,
607        qh: &QueueHandle<Self>,
608        _keyboard: &wl_keyboard::WlKeyboard,
609        _serial: u32,
610        event: KeyEvent,
611    ) {
612        self.process_event(
613            Event::KeyPress {
614                keysym: event.keysym,
615                shift: self.shift_held,
616            },
617            qh,
618        );
619    }
620
621    fn update_modifiers(
622        &mut self,
623        _conn: &Connection,
624        qh: &QueueHandle<Self>,
625        _keyboard: &wl_keyboard::WlKeyboard,
626        _serial: u32,
627        modifiers: Modifiers,
628        _raw_modifiers: RawModifiers,
629        _layout: u32,
630    ) {
631        let was_alt_held = self.alt_held;
632        self.alt_held = modifiers.alt;
633        self.shift_held = modifiers.shift;
634
635        tracing::debug!(
636            "Modifiers: alt={} (was {}), shift={}",
637            self.alt_held,
638            was_alt_held,
639            self.shift_held
640        );
641
642        // Alt released state processed through state machine
643        if was_alt_held && !self.alt_held {
644            self.process_event(Event::AltReleased, qh);
645        }
646    }
647}
648
649impl ShmHandler for App {
650    fn shm_state(&mut self) -> &mut Shm {
651        &mut self.shm
652    }
653}
654
655impl ProvidesRegistryState for App {
656    fn registry(&mut self) -> &mut RegistryState {
657        &mut self.registry_state
658    }
659
660    registry_handlers!(OutputState, SeatState);
661}
662
663delegate_compositor!(App);
664delegate_output!(App);
665delegate_shm!(App);
666delegate_seat!(App);
667delegate_keyboard!(App);
668delegate_layer!(App);
669delegate_registry!(App);