beer/src/wayland.rs
NotAShelf 2d319b7e73
render: inner terminal padding (pad-x/pad-y)
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I190f63ca86a8cf976e4d018df73897ab6a6a6964
2026-06-26 10:21:39 +03:00

1709 lines
58 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Wayland front-end: connection, surface, and software-drawn window.
//!
//! Uses smithay-client-toolkit for protocol boilerplate and calloop for the
//! event loop, so the PTY master fd and timers share one loop.
use std::fs::File;
use std::io::{Read as _, Write as _};
use std::os::fd::OwnedFd;
use std::os::unix::process::ExitStatusExt;
use std::process::ExitCode;
use std::time::Duration;
use anyhow::Context;
use calloop::generic::Generic;
use calloop::timer::{TimeoutAction, Timer};
use calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction, RegistrationToken};
use calloop_wayland_source::WaylandSource;
use crate::config::Config;
use crate::font::Fonts;
use crate::grid::{Cell, CursorShape, Grid, MouseProtocol};
use crate::pty::Pty;
use crate::render::Renderer;
use crate::vt::Term;
use smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{
Shape, WpCursorShapeDeviceV1,
};
use smithay_client_toolkit::reexports::protocols::wp::primary_selection::zv1::client::{
zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
};
use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
data_device_manager::{
DataDeviceManagerState, WritePipe,
data_device::{DataDevice, DataDeviceHandler},
data_offer::{DataOfferHandler, DragOffer},
data_source::{CopyPasteSource, DataSourceHandler},
},
delegate_compositor, delegate_data_device, delegate_keyboard, delegate_output,
delegate_pointer, delegate_primary_selection, delegate_registry, delegate_seat, delegate_shm,
delegate_xdg_shell, delegate_xdg_window,
output::{OutputHandler, OutputState},
primary_selection::{
PrimarySelectionManagerState,
device::{PrimarySelectionDevice, PrimarySelectionDeviceHandler},
selection::{PrimarySelectionSource, PrimarySelectionSourceHandler},
},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{
Capability, SeatHandler, SeatState,
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},
pointer::{
BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, PointerEvent, PointerEventKind, PointerHandler,
cursor_shape::CursorShapeManager,
},
},
shell::{
WaylandSurface,
xdg::{
XdgShell,
window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
},
},
shm::{
Shm, ShmHandler,
slot::{Buffer, SlotPool},
},
};
use wayland_client::{
Connection, QueueHandle,
globals::registry_queue_init,
protocol::{
wl_data_device::WlDataDevice, wl_data_device_manager::DndAction,
wl_data_source::WlDataSource, wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm,
wl_surface,
},
};
/// MIME types beer offers and accepts for clipboard text.
const TEXT_MIMES: &[&str] = &[
"text/plain;charset=utf-8",
"text/plain;charset=UTF-8",
"UTF8_STRING",
"STRING",
"text/plain",
"TEXT",
];
/// Pick the first MIME type we understand from an offer's advertised set.
fn pick_mime(mimes: &[String]) -> Option<String> {
mimes
.iter()
.find(|m| TEXT_MIMES.contains(&m.as_str()))
.cloned()
}
/// Max gap between clicks counted as a multi-click (ms).
const MULTI_CLICK_MS: u32 = 400;
/// Blink half-period: cells/cursor toggle visibility this often.
const BLINK_MS: u64 = 500;
/// Buffers kept for double/triple buffering before we wait for a release.
const MAX_BUFFERS: usize = 3;
/// How long synchronized output (DECSET 2026) may hold the screen before we
/// present anyway, so a misbehaving app cannot freeze the window.
const SYNC_TIMEOUT_MS: u64 = 150;
/// Interval between autoscroll steps while a drag selection runs off an edge.
const AUTOSCROLL_MS: u64 = 40;
/// What determines one rendered row's pixels: its cells, the cursor on it, the
/// selection span over it, and the blink phase. Two equal `RowSnap`s render
/// identically, so a buffer holding an equal snapshot needs no repaint.
#[derive(Clone, PartialEq, Debug)]
struct RowSnap {
cells: Vec<Cell>,
/// `(col, shape, focused)` when the cursor is drawn on this row.
cursor: Option<(usize, CursorShape, bool)>,
/// Inclusive selected column span on this row.
sel: Option<(usize, usize)>,
/// Search-match spans `(lo, hi, is_current)` highlighted on this row.
search: Vec<(usize, usize, bool)>,
/// Search-prompt text drawn over this row (only the bottom row, when active).
overlay: Option<String>,
/// Blink phase, but only varied when the row actually has blinking ink, so
/// non-blinking rows stay equal across phase toggles.
blink: bool,
}
/// One shm buffer plus the per-row snapshot of what it currently displays.
#[derive(Debug)]
struct FrameBuf {
buffer: Buffer,
rows: Vec<RowSnap>,
}
/// Snapshot the determinants of viewport row `y`'s pixels.
fn row_snap(grid: &Grid, y: usize, focused: bool, blink_on: bool) -> RowSnap {
let abs = grid.view_to_abs(y);
let cells = grid.view_row(y).to_vec();
let cursor = if grid.view_at_bottom() && grid.cursor().1 == y {
let visible = grid.cursor_visible() && (!grid.cursor_blink() || blink_on);
visible.then(|| (grid.cursor().0, grid.cursor_shape(), focused))
} else {
None
};
let has_blink = cells
.iter()
.any(|c| c.flags.contains(crate::grid::Flags::BLINK));
RowSnap {
cells,
cursor,
sel: grid.selection_span_on(abs),
search: grid.search_spans_on(abs),
overlay: None,
blink: if has_blink { blink_on } else { true },
}
}
/// Fallback window size in pixels if the configured geometry yields nothing.
const DEFAULT_W: u32 = 800;
const DEFAULT_H: u32 = 600;
/// Run a single window until it is closed, returning the shell's exit code.
pub fn run(config: Config) -> anyhow::Result<ExitCode> {
let conn = Connection::connect_to_env().context("connect to Wayland compositor")?;
let (globals, event_queue) =
registry_queue_init(&conn).context("initialize Wayland registry")?;
let qh = event_queue.handle();
let mut event_loop: EventLoop<App> =
EventLoop::try_new().context("create calloop event loop")?;
WaylandSource::new(conn, event_queue)
.insert(event_loop.handle())
.map_err(|e| anyhow::anyhow!("insert Wayland source into event loop: {e}"))?;
let compositor = CompositorState::bind(&globals, &qh).context("compositor not available")?;
let xdg_shell = XdgShell::bind(&globals, &qh).context("xdg_wm_base not available")?;
let shm = Shm::bind(&globals, &qh).context("wl_shm not available")?;
let data_device_manager = DataDeviceManagerState::bind(&globals, &qh)
.context("wl_data_device_manager not available")?;
let primary_manager = PrimarySelectionManagerState::bind(&globals, &qh).ok();
let cursor_shape_manager = CursorShapeManager::bind(&globals, &qh).ok();
let surface = compositor.create_surface(&qh);
let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh);
window.set_title("beer");
window.set_app_id("dev.notashelf.beer");
window.set_min_size(Some((1, 1)));
// First commit with no buffer kicks off the initial configure.
window.commit();
let fonts = Fonts::new(&config.main.font, config.main.font_size).context("load font")?;
let mut renderer = Renderer::new(fonts);
renderer.set_padding(config.main.pad_x, config.main.pad_y);
// Start at the configured cell geometry plus padding; the compositor may
// override it on the first configure.
let m = renderer.metrics();
let width = (u32::from(config.main.initial_cols) * m.width + 2 * config.main.pad_x).max(1);
let height = (u32::from(config.main.initial_rows) * m.height + 2 * config.main.pad_y).max(1);
let pool = SlotPool::new(
(width * height * 4).max(DEFAULT_W * DEFAULT_H) as usize,
&shm,
)
.context("create shm slot pool")?;
let mut app = App {
registry_state: RegistryState::new(&globals),
output_state: OutputState::new(&globals, &qh),
seat_state: SeatState::new(&globals, &qh),
shm,
pool,
window,
renderer,
loop_handle: event_loop.handle(),
qh: qh.clone(),
data_device_manager,
primary_manager,
cursor_shape_manager,
cursor_shape_device: None,
data_device: None,
primary_device: None,
copy_source: None,
primary_source: None,
clipboard: String::new(),
primary_clip: String::new(),
selecting: false,
pressed_button: None,
last_report_cell: None,
autoscroll: 0,
autoscroll_timer: None,
pointer_pos: (0.0, 0.0),
last_click: None,
serial: 0,
keyboard: None,
pointer: None,
modifiers: Modifiers::default(),
// The PTY is spawned on the first configure, once the real window size
// is known, so the shell starts at the final size and is not hit by a
// startup SIGWINCH storm that makes it reprint its prompt.
session: None,
title: None,
config,
width,
height,
needs_draw: false,
frame_pending: false,
frames: Vec::new(),
buf_dims: (0, 0),
blink_on: true,
sync_timeout: None,
searching: false,
focused: true,
exit: false,
exit_code: ExitCode::SUCCESS,
};
// Toggle the blink phase on a timer so blinking text and cursors animate.
let blink = Timer::from_duration(Duration::from_millis(BLINK_MS));
let blink_registered = event_loop
.handle()
.insert_source(blink, |_, _, app: &mut App| {
app.blink_on = !app.blink_on;
app.needs_draw = true;
TimeoutAction::ToDuration(Duration::from_millis(BLINK_MS))
});
if let Err(err) = blink_registered {
tracing::warn!("register blink timer: {err}");
}
// Each iteration blocks until an event (PTY output, input, configure, frame
// callback, blink) arrives, then presents at most one frame; bursts of PTY
// output between frame callbacks coalesce into a single repaint.
while !app.exit {
event_loop
.dispatch(None, &mut app)
.context("dispatch event loop")?;
app.flush();
}
Ok(app.exit_code)
}
/// Columns and rows that fit a `width`×`height` px window at `metrics`, after
/// reserving `2 * pad` pixels of inner padding on each axis.
fn grid_size(
metrics: crate::font::CellMetrics,
width: u32,
height: u32,
pad: (u32, u32),
) -> (u16, u16) {
let cols = (width.saturating_sub(2 * pad.0) / metrics.width).max(1);
let rows = (height.saturating_sub(2 * pad.1) / metrics.height).max(1);
(cols as u16, rows as u16)
}
/// Write every byte to `fd`, retrying short writes and interrupts.
fn write_all(fd: &OwnedFd, mut buf: &[u8]) -> rustix::io::Result<()> {
while !buf.is_empty() {
match rustix::io::write(fd, buf) {
Ok(0) => return Err(rustix::io::Errno::IO),
Ok(n) => buf = &buf[n..],
Err(rustix::io::Errno::INTR) => {}
Err(e) => return Err(e),
}
}
Ok(())
}
/// The per-window terminal: the PTY and the parsed screen behind it. Created on
/// the first configure, once the real size is known.
#[derive(Debug)]
struct Session {
pty: Pty,
term: Term,
}
/// Window + Wayland client state shared across all protocol handlers.
#[derive(Debug)]
struct App {
registry_state: RegistryState,
output_state: OutputState,
seat_state: SeatState,
shm: Shm,
pool: SlotPool,
window: Window,
renderer: Renderer,
loop_handle: LoopHandle<'static, App>,
qh: QueueHandle<App>,
data_device_manager: DataDeviceManagerState,
primary_manager: Option<PrimarySelectionManagerState>,
/// Sets the pointer to an I-beam over the window (cursor-shape-v1).
cursor_shape_manager: Option<CursorShapeManager>,
cursor_shape_device: Option<WpCursorShapeDeviceV1>,
data_device: Option<DataDevice>,
primary_device: Option<PrimarySelectionDevice>,
/// Held while we own the clipboard / primary selection, serving paste reads.
copy_source: Option<CopyPasteSource>,
primary_source: Option<PrimarySelectionSource>,
clipboard: String,
primary_clip: String,
/// A left-button drag is in progress.
selecting: bool,
/// Button base code held down while mouse reporting, for drag reports.
pressed_button: Option<u8>,
/// Last cell a motion report was emitted for, to suppress duplicates.
last_report_cell: Option<(usize, usize)>,
/// Autoscroll direction while dragging past an edge: +1 back, -1 toward live.
autoscroll: isize,
/// Calloop token for the repeating autoscroll timer, when armed.
autoscroll_timer: Option<RegistrationToken>,
pointer_pos: (f64, f64),
/// Last click (time ms, abs row, col, count) for double/triple detection.
last_click: Option<(u32, usize, usize, u32)>,
/// Most recent input serial, used to claim selections.
serial: u32,
keyboard: Option<wl_keyboard::WlKeyboard>,
pointer: Option<wl_pointer::WlPointer>,
modifiers: Modifiers,
/// `None` until the first configure spawns the shell.
session: Option<Session>,
/// Last title applied to the toplevel, to avoid redundant requests.
title: Option<String>,
/// The active user configuration.
config: Config,
width: u32,
height: u32,
/// The grid changed and the window wants repainting on the next frame.
needs_draw: bool,
/// A `wl_surface.frame` callback is in flight; defer drawing until it fires.
frame_pending: bool,
/// Double/triple-buffer ring, each tagged with the rows it currently shows.
frames: Vec<FrameBuf>,
/// Pixel size the `frames` buffers were allocated for.
buf_dims: (u32, u32),
/// Current blink phase, toggled by a timer; off hides blinking ink.
blink_on: bool,
/// Armed while synchronized output holds the screen, to force it open.
sync_timeout: Option<RegistrationToken>,
/// Whether incremental search mode is active (the query lives in the grid).
searching: bool,
/// Whether the toplevel currently has keyboard focus (drives the cursor).
focused: bool,
exit: bool,
/// Exit code to return, taken from the shell when it exits.
exit_code: ExitCode,
}
impl App {
/// Spawn the shell at the current window size and start reading its output.
fn spawn_session(&mut self) {
let (cols, rows) = grid_size(
self.renderer.metrics(),
self.width,
self.height,
(self.config.main.pad_x, self.config.main.pad_y),
);
let pty = match Pty::spawn(cols, rows, &self.config.main.term) {
Ok(pty) => pty,
Err(err) => {
tracing::error!("spawn shell: {err:#}");
self.exit = true;
return;
}
};
let read_fd = match pty.master().try_clone() {
Ok(fd) => fd,
Err(err) => {
tracing::error!("clone pty master: {err}");
self.exit = true;
return;
}
};
let mut parser = vte::Parser::new();
let source = Generic::new(read_fd, Interest::READ, Mode::Level);
let registered = self
.loop_handle
.insert_source(source, move |_, fd, app: &mut App| {
let mut buf = [0u8; 4096];
let n = match rustix::io::read(&*fd, &mut buf) {
Ok(0) => {
app.child_exited();
return Ok(PostAction::Remove);
}
Ok(n) => n,
Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => {
return Ok(PostAction::Continue);
}
Err(_) => {
app.child_exited();
return Ok(PostAction::Remove);
}
};
if let Some(session) = app.session.as_mut() {
parser.advance(&mut session.term, &buf[..n]);
}
app.after_feed();
Ok(PostAction::Continue)
});
if let Err(err) = registered {
tracing::error!("register pty in event loop: {err}");
self.exit = true;
return;
}
let mut term = Term::new(cols as usize, rows as usize);
term.set_theme(crate::theme::Theme::from_config(&self.config.colors));
let grid = term.grid_mut();
grid.set_word_delimiters(self.config.main.word_delimiters.clone());
grid.set_scrollback_cap(self.config.scrollback.lines);
self.session = Some(Session { pty, term });
}
/// Handle a key (initial press or repeat): Shift+PageUp/PageDown scroll the
/// viewport locally; anything else is encoded to the shell and snaps the
/// viewport back to the live screen.
fn handle_key(&mut self, event: &KeyEvent) {
// Ctrl+Shift+F toggles incremental search mode.
if self.modifiers.ctrl
&& self.modifiers.shift
&& matches!(event.keysym, Keysym::F | Keysym::f)
{
self.toggle_search();
return;
}
// While searching, the keyboard edits the query and navigates matches.
if self.searching {
self.search_key(event);
return;
}
// Ctrl+Shift+C/V copy the selection and paste the clipboard; these take
// precedence over the control bytes the chord would otherwise encode.
if self.modifiers.ctrl && self.modifiers.shift {
match event.keysym {
Keysym::C | Keysym::c => {
let qh = self.qh.clone();
self.set_clipboard(&qh);
return;
}
Keysym::V | Keysym::v => {
self.paste_clipboard();
return;
}
_ => {}
}
}
if self.modifiers.shift && matches!(event.keysym, Keysym::Page_Up | Keysym::Page_Down) {
if let Some(session) = self.session.as_mut() {
let page = session.term.page() as isize;
let delta = if event.keysym == Keysym::Page_Up {
page
} else {
-page
};
session.term.scroll_view(delta);
self.needs_draw = true;
}
return;
}
let app_cursor = self
.session
.as_ref()
.is_some_and(|s| s.term.grid().app_cursor());
if let Some(bytes) = crate::input::encode(event, self.modifiers, app_cursor)
&& let Some(session) = self.session.as_mut()
{
session.term.scroll_to_bottom();
session.term.grid_mut().clear_selection();
self.needs_draw = true;
if let Err(err) = write_all(session.pty.master(), &bytes) {
tracing::warn!("write key to pty: {err}");
}
}
}
/// Enter or leave incremental search mode.
fn toggle_search(&mut self) {
self.searching = !self.searching;
if let Some(session) = self.session.as_mut() {
if self.searching {
session.term.grid_mut().set_search("");
} else {
session.term.grid_mut().clear_search();
}
}
self.needs_draw = true;
}
/// Handle a key while search mode is active: edit the query incrementally,
/// step between matches, or exit.
fn search_key(&mut self, event: &KeyEvent) {
let Some(session) = self.session.as_mut() else {
return;
};
let grid = session.term.grid_mut();
match event.keysym {
Keysym::Escape => {
grid.clear_search();
self.searching = false;
}
Keysym::Return | Keysym::KP_Enter | Keysym::Up | Keysym::Page_Up => {
grid.search_step(false);
}
Keysym::Down | Keysym::Page_Down => grid.search_step(true),
Keysym::BackSpace => {
let mut query = grid.search_query().unwrap_or("").to_string();
query.pop();
grid.set_search(&query);
}
_ => {
// Append typed text, ignoring control characters.
if let Some(text) = event.utf8.as_ref() {
let printable: String = text.chars().filter(|c| !c.is_control()).collect();
if !printable.is_empty() {
let mut query = grid.search_query().unwrap_or("").to_string();
query.push_str(&printable);
grid.set_search(&query);
}
}
}
}
self.needs_draw = true;
}
/// Inner padding `(x, y)` in pixels.
fn padding(&self) -> (f64, f64) {
(
f64::from(self.config.main.pad_x),
f64::from(self.config.main.pad_y),
)
}
/// Map window pixel coordinates to an absolute `(row, col)` grid point.
fn cell_at(&self, px: f64, py: f64) -> Option<(usize, usize)> {
let session = self.session.as_ref()?;
let m = self.renderer.metrics();
let (pad_x, pad_y) = self.padding();
let grid = session.term.grid();
let col =
((px - pad_x).max(0.0) as usize / m.width as usize).min(grid.cols().saturating_sub(1));
let vrow =
((py - pad_y).max(0.0) as usize / m.height as usize).min(grid.rows().saturating_sub(1));
Some((grid.view_to_abs(vrow), col))
}
/// Left-button press: start (or word/line-extend) a selection.
fn pointer_press(&mut self, time: u32) {
let Some((row, col)) = self.cell_at(self.pointer_pos.0, self.pointer_pos.1) else {
return;
};
let count = match self.last_click {
Some((t, r, c, n))
if time.wrapping_sub(t) <= MULTI_CLICK_MS && r == row && c == col =>
{
n % 3 + 1
}
_ => 1,
};
self.last_click = Some((time, row, col, count));
let Some(session) = self.session.as_mut() else {
return;
};
let ctrl = self.modifiers.ctrl;
let grid = session.term.grid_mut();
match count {
2 => grid.select_word(row, col),
3 => grid.select_line(row),
// Holding Ctrl starts a rectangular (block) selection.
_ if ctrl => grid.start_block_selection(row, col),
_ => grid.start_selection(row, col),
}
self.selecting = true;
self.needs_draw = true;
}
/// Pointer motion during a drag: extend the selection head and, if the
/// pointer has left the top/bottom edge, start autoscrolling.
fn pointer_drag(&mut self) {
if !self.selecting {
return;
}
let Some((row, col)) = self.cell_at(self.pointer_pos.0, self.pointer_pos.1) else {
return;
};
if let Some(session) = self.session.as_mut() {
session.term.grid_mut().extend_selection(row, col);
self.needs_draw = true;
}
self.update_autoscroll();
}
/// Arm or disarm edge autoscroll based on the pointer's vertical position.
fn update_autoscroll(&mut self) {
let dir = if self.pointer_pos.1 < 0.0 {
1 // above the top: reveal older lines
} else if self.pointer_pos.1 >= f64::from(self.height) {
-1 // below the bottom: advance toward the live screen
} else {
0
};
self.autoscroll = dir;
if dir != 0 && self.autoscroll_timer.is_none() {
let timer = Timer::immediate();
self.autoscroll_timer = self
.loop_handle
.insert_source(timer, |_, _, app: &mut App| app.autoscroll_step())
.ok();
}
}
/// One autoscroll step: scroll the viewport and drag the selection head to
/// the edge cell under the pointer. Reschedules until the drag ends or the
/// pointer returns inside the window.
fn autoscroll_step(&mut self) -> TimeoutAction {
if !self.selecting || self.autoscroll == 0 {
self.autoscroll_timer = None;
return TimeoutAction::Drop;
}
if let Some(session) = self.session.as_mut() {
session.term.scroll_view(self.autoscroll);
}
let edge_y = if self.autoscroll > 0 {
0.0
} else {
f64::from(self.height) - 1.0
};
if let Some((row, col)) = self.cell_at(self.pointer_pos.0, edge_y)
&& let Some(session) = self.session.as_mut()
{
session.term.grid_mut().extend_selection(row, col);
}
self.needs_draw = true;
TimeoutAction::ToDuration(Duration::from_millis(AUTOSCROLL_MS))
}
/// Left-button release: stop autoscrolling and publish the primary selection.
fn pointer_release(&mut self, qh: &QueueHandle<App>) {
if !self.selecting {
return;
}
self.selecting = false;
self.autoscroll = 0;
if let Some(token) = self.autoscroll_timer.take() {
self.loop_handle.remove(token);
}
self.set_primary(qh);
}
/// Whether the application wants mouse reports and the user is not holding
/// Shift (which forces local selection regardless of mode).
fn mouse_reporting(&self) -> bool {
self.session
.as_ref()
.is_some_and(|s| s.term.grid().mouse_protocol() != MouseProtocol::Off)
&& !self.modifiers.shift
}
/// The viewport cell `(col, row)` under the pointer, clamped to the screen.
fn report_screen_cell(&self) -> Option<(usize, usize)> {
let session = self.session.as_ref()?;
let m = self.renderer.metrics();
let (pad_x, pad_y) = self.padding();
let grid = session.term.grid();
let col = ((self.pointer_pos.0 - pad_x).max(0.0) as usize / m.width as usize)
.min(grid.cols().saturating_sub(1));
let row = ((self.pointer_pos.1 - pad_y).max(0.0) as usize / m.height as usize)
.min(grid.rows().saturating_sub(1));
Some((col, row))
}
/// Report a button press/release to the application, if reporting is active.
/// Returns whether the event was consumed (so local handling is skipped).
fn try_report_button(&mut self, code: u8, pressed: bool) -> bool {
let Some(session) = self.session.as_ref() else {
return false;
};
let grid = session.term.grid();
let proto = grid.mouse_protocol();
if proto == MouseProtocol::Off || self.modifiers.shift {
return false;
}
let enc = grid.mouse_encoding();
// X10 (mode 9) reports presses only; a release is swallowed, not sent.
if (pressed || proto != MouseProtocol::X10)
&& let Some((col, row)) = self.report_screen_cell()
{
let bytes =
crate::input::encode_mouse(enc, code, col, row, pressed, false, self.modifiers);
self.write_to_pty(&bytes);
self.last_report_cell = Some((col, row));
}
true
}
/// Report pointer motion to the application when the active mode wants it.
/// Returns whether reporting consumed the motion (suppressing local drag).
fn try_report_motion(&mut self) -> bool {
let Some(session) = self.session.as_ref() else {
return false;
};
let grid = session.term.grid();
let proto = grid.mouse_protocol();
if proto == MouseProtocol::Off || self.modifiers.shift {
return false;
}
let enc = grid.mouse_encoding();
let wants = match proto {
MouseProtocol::Any => true,
MouseProtocol::Button => self.pressed_button.is_some(),
_ => false,
};
if wants
&& let Some((col, row)) = self.report_screen_cell()
&& self.last_report_cell != Some((col, row))
{
// Any-event motion with no button held uses the "no button" code 3.
let code = self.pressed_button.unwrap_or(3);
let bytes = crate::input::encode_mouse(enc, code, col, row, true, true, self.modifiers);
self.write_to_pty(&bytes);
self.last_report_cell = Some((col, row));
}
true
}
/// Send focus in/out (DECSET 1004) to the application when it asked for it.
fn report_focus(&mut self, focused: bool) {
if self
.session
.as_ref()
.is_some_and(|s| s.term.grid().focus_events())
{
self.write_to_pty(if focused { b"\x1b[I" } else { b"\x1b[O" });
}
}
/// Write bytes to the PTY master, logging on failure.
fn write_to_pty(&mut self, bytes: &[u8]) {
if let Some(session) = self.session.as_mut()
&& let Err(err) = write_all(session.pty.master(), bytes)
{
tracing::warn!("write to pty: {err}");
}
}
/// The current selection text, if any and non-empty.
fn selection_text(&self) -> Option<String> {
let text = self.session.as_ref()?.term.grid().selection_text()?;
(!text.is_empty()).then_some(text)
}
/// Take ownership of the CLIPBOARD selection, serving `text` to pasters.
fn claim_clipboard(&mut self, text: String, qh: &QueueHandle<App>) {
let Some(device) = self.data_device.as_ref() else {
return;
};
let source = self
.data_device_manager
.create_copy_paste_source(qh, TEXT_MIMES.iter().copied());
source.set_selection(device, self.serial);
self.clipboard = text;
self.copy_source = Some(source);
}
/// Take ownership of the primary selection, serving `text` to pasters.
fn claim_primary(&mut self, text: String, qh: &QueueHandle<App>) {
let (Some(manager), Some(device)) =
(self.primary_manager.as_ref(), self.primary_device.as_ref())
else {
return;
};
let source = manager.create_selection_source(qh, TEXT_MIMES.iter().copied());
source.set_selection(device, self.serial);
self.primary_clip = text;
self.primary_source = Some(source);
}
/// Claim the clipboard (CLIPBOARD) with the current selection (Ctrl+Shift+C).
fn set_clipboard(&mut self, qh: &QueueHandle<App>) {
if let Some(text) = self.selection_text() {
self.claim_clipboard(text, qh);
}
}
/// Claim the primary selection with the current selection (select-to-copy).
fn set_primary(&mut self, qh: &QueueHandle<App>) {
if let Some(text) = self.selection_text() {
self.claim_primary(text, qh);
}
}
/// Act on the OSC 52 clipboard requests an application made: take ownership
/// of the selection it set, or answer a query with what we currently hold.
fn handle_clipboard_ops(&mut self, ops: Vec<crate::vt::ClipboardOp>) {
use crate::vt::ClipboardOp;
let qh = self.qh.clone();
for op in ops {
match op {
ClipboardOp::Set {
primary: true,
text,
} => self.claim_primary(text, &qh),
ClipboardOp::Set {
primary: false,
text,
} => self.claim_clipboard(text, &qh),
ClipboardOp::Query { primary } => {
let text = if primary {
&self.primary_clip
} else {
&self.clipboard
};
let kind = if primary { 'p' } else { 'c' };
let reply = format!(
"\x1b]52;{kind};{}\x07",
crate::vt::base64_encode(text.as_bytes())
);
self.write_to_pty(reply.as_bytes());
}
}
}
}
/// Paste the CLIPBOARD selection into the shell (Ctrl+Shift+V).
fn paste_clipboard(&mut self) {
let Some(offer) = self
.data_device
.as_ref()
.and_then(|d| d.data().selection_offer())
else {
return;
};
let Some(mime) = offer.with_mime_types(pick_mime) else {
return;
};
if let Ok(pipe) = offer.receive(mime) {
self.read_paste(pipe);
}
}
/// Paste the primary selection into the shell (middle click).
fn paste_primary(&mut self) {
let Some(offer) = self
.primary_device
.as_ref()
.and_then(|d| d.data().selection_offer())
else {
return;
};
let Some(mime) = offer.with_mime_types(pick_mime) else {
return;
};
if let Ok(pipe) = offer.receive(mime) {
self.read_paste(pipe);
}
}
/// Drain a clipboard read-pipe on the event loop, writing the bytes to the
/// PTY once the source closes its end.
fn read_paste(&mut self, pipe: smithay_client_toolkit::data_device_manager::ReadPipe) {
let mut data: Vec<u8> = Vec::new();
let registered = self
.loop_handle
.insert_source(pipe, move |_, file, app: &mut App| {
// SAFETY: the file is owned by the source and not closed while read.
let f: &mut File = unsafe { file.get_mut() };
let mut tmp = [0u8; 4096];
match f.read(&mut tmp) {
Ok(0) => {
app.paste_bytes(&data);
PostAction::Remove
}
Ok(n) => {
data.extend_from_slice(&tmp[..n]);
PostAction::Continue
}
Err(e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => {
PostAction::Continue
}
Err(e) => {
tracing::warn!("read paste pipe: {e}");
PostAction::Remove
}
}
});
if let Err(err) = registered {
tracing::warn!("register paste pipe: {err}");
}
}
/// Write pasted bytes to the PTY, framing them for bracketed-paste mode and
/// snapping the viewport to the live screen.
fn paste_bytes(&mut self, data: &[u8]) {
let Some(session) = self.session.as_mut() else {
return;
};
session.term.scroll_to_bottom();
self.needs_draw = true;
let bracketed = session.term.grid().bracketed_paste();
// Strip control bytes a terminal must never receive raw from a paste;
// keep tab and newlines (CR is what the shell expects for Enter).
let mut clean: Vec<u8> = Vec::with_capacity(data.len());
for &b in data {
match b {
b'\n' => clean.push(b'\r'),
b'\t' | b'\r' => clean.push(b),
0x20..=0xff => clean.push(b),
_ => {}
}
}
let fd = session.pty.master();
if bracketed {
let _ = write_all(fd, b"\x1b[200~");
let _ = write_all(fd, &clean);
let _ = write_all(fd, b"\x1b[201~");
} else if let Err(err) = write_all(fd, &clean) {
tracing::warn!("write paste to pty: {err}");
}
}
/// After parsing child output: send any replies, sync the title, repaint.
fn after_feed(&mut self) {
let Some(session) = self.session.as_mut() else {
return;
};
let reply = session.term.take_response();
if !reply.is_empty()
&& let Err(err) = write_all(session.pty.master(), &reply)
{
tracing::warn!("write to pty: {err}");
}
let new_title = session.term.title().map(str::to_owned);
if new_title.as_deref() != self.title.as_deref() {
self.title = new_title;
self.window
.set_title(self.title.clone().unwrap_or_default());
}
let ops = session.term.take_clipboard_ops();
if !ops.is_empty() {
self.handle_clipboard_ops(ops);
}
self.needs_draw = true;
}
/// Recompute the grid size for the current window and tell the grid and the
/// PTY about it if it changed.
fn resize_grid(&mut self) {
let (cols, rows) = grid_size(
self.renderer.metrics(),
self.width,
self.height,
(self.config.main.pad_x, self.config.main.pad_y),
);
let Some(session) = self.session.as_mut() else {
return;
};
if (cols as usize, rows as usize)
== (session.term.grid().cols(), session.term.grid().rows())
{
return;
}
session.term.resize(cols as usize, rows as usize);
if let Err(err) = session.pty.resize(cols, rows) {
tracing::warn!("resize pty: {err}");
}
}
/// The child shell has gone away; reap it, capture its code, and tear the
/// window down.
fn child_exited(&mut self) {
if let Some(session) = self.session.as_mut() {
match session.pty.wait() {
Ok(status) => {
tracing::info!("shell exited: {status}");
// Mirror the shell's status: its code, or 128+signal if killed.
let code = status
.code()
.unwrap_or_else(|| 128 + status.signal().unwrap_or(0));
self.exit_code = ExitCode::from(code as u8);
}
Err(err) => tracing::warn!("reap shell: {err}"),
}
}
self.exit = true;
}
/// Present a frame if one is wanted and the compositor is ready for it.
/// Called after every event-loop wake; the frame-callback gate keeps draws
/// paced to the display instead of one per PTY read. While the app holds
/// synchronized output (DECSET 2026) we withhold the frame, but arm a
/// timeout so a stuck `2026h` cannot freeze the window.
fn flush(&mut self) {
let sync = self
.session
.as_ref()
.is_some_and(|s| s.term.grid().sync_active());
if sync {
if self.sync_timeout.is_none() {
let timer = Timer::from_duration(Duration::from_millis(SYNC_TIMEOUT_MS));
self.sync_timeout = self
.loop_handle
.insert_source(timer, |_, _, app: &mut App| {
if let Some(session) = app.session.as_mut() {
session.term.grid_mut().set_sync(false);
}
app.sync_timeout = None;
app.needs_draw = true;
TimeoutAction::Drop
})
.ok();
}
return;
}
if let Some(token) = self.sync_timeout.take() {
self.loop_handle.remove(token);
}
if self.needs_draw && !self.frame_pending && self.session.is_some() {
self.present();
}
}
/// Render only the rows that changed since the chosen buffer last displayed
/// them, damage just those rows, and commit with a frame-callback request.
fn present(&mut self) {
self.needs_draw = false;
let (w, h) = (self.width, self.height);
let m = self.renderer.metrics();
let (focused, blink_on) = (self.focused, self.blink_on);
// A resize invalidates every buffer's contents and size.
if self.buf_dims != (w, h) {
self.frames.clear();
self.buf_dims = (w, h);
}
let Some(session) = self.session.as_ref() else {
return;
};
let grid = session.term.grid();
let theme = session.term.theme();
let rows = grid.rows();
let mut cur: Vec<RowSnap> = (0..rows)
.map(|y| row_snap(grid, y, focused, blink_on))
.collect();
// The search prompt occupies the bottom row while search mode is active.
// Recording it in the snapshot keeps the row's damage/diff correct.
let bar_text = self.searching.then(|| {
let (n, total) = grid.search_count();
format!(
"search: {} [{n}/{total}]",
grid.search_query().unwrap_or("")
)
});
if let Some(text) = &bar_text
&& rows > 0
{
cur[rows - 1].overlay = Some(text.clone());
}
// Reuse a buffer the compositor has released, else grow the ring.
let stride = w as i32 * 4;
let mut idx = None;
for i in 0..self.frames.len() {
if self.pool.canvas(&self.frames[i].buffer).is_some() {
idx = Some(i);
break;
}
}
let idx = match idx {
Some(i) => i,
None if self.frames.len() < MAX_BUFFERS => {
match self
.pool
.create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
{
Ok((buffer, _)) => {
self.frames.push(FrameBuf {
buffer,
rows: Vec::new(),
});
self.frames.len() - 1
}
Err(err) => {
tracing::error!("allocate shm buffer: {err}");
return;
}
}
}
// All buffers are still held by the compositor; a release event will
// wake us and `needs_draw` (re-set below) retries then.
None => {
self.needs_draw = true;
return;
}
};
// Rows that differ from what this buffer last showed (all, if fresh).
let prev = &self.frames[idx].rows;
let dirty: Vec<usize> = (0..rows)
.filter(|&y| prev.get(y) != Some(&cur[y]))
.collect();
if dirty.is_empty() {
return;
}
// A buffer used for the first time has uninitialized margins; paint the
// whole thing (background + padding) once, then damage it in full below.
let fresh = self.frames[idx].rows.is_empty();
let pad_y = self.config.main.pad_y as i32;
let Some(canvas) = self.pool.canvas(&self.frames[idx].buffer) else {
return;
};
let dims = (w as usize, h as usize);
let frame = crate::render::Frame {
theme,
focused,
blink_on,
};
if fresh {
self.renderer.clear(canvas, dims, theme);
}
for &y in &dirty {
self.renderer.render_row(canvas, dims, grid, &frame, y);
}
// Draw the search prompt over the (now repainted) bottom row.
if let Some(text) = &bar_text
&& dirty.contains(&(rows - 1))
{
self.renderer
.render_search_bar(canvas, dims, theme, rows - 1, text);
}
self.frames[idx].rows = cur;
let surface = self.window.wl_surface();
if let Err(err) = self.frames[idx].buffer.attach_to(surface) {
tracing::error!("attach buffer: {err}");
return;
}
if fresh {
surface.damage_buffer(0, 0, w as i32, h as i32);
} else {
for &y in &dirty {
let top = pad_y + y as i32 * m.height as i32;
surface.damage_buffer(0, top, w as i32, m.height as i32);
}
}
surface.frame(&self.qh, surface.clone());
self.window.commit();
self.frame_pending = true;
}
}
impl CompositorHandler for App {
fn scale_factor_changed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: i32,
) {
}
fn transform_changed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: wl_output::Transform,
) {
}
fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {
// The compositor is ready for another frame; `flush` will repaint if the
// grid has changed since the last present.
self.frame_pending = false;
}
fn surface_enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_surface::WlSurface,
_: &wl_output::WlOutput,
) {
}
}
impl WindowHandler for App {
fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
self.exit = true;
}
fn configure(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &Window,
configure: WindowConfigure,
_serial: u32,
) {
if let (Some(w), Some(h)) = configure.new_size {
self.width = w.get();
self.height = h.get();
}
self.focused = configure.is_activated();
if self.session.is_none() {
self.spawn_session();
} else {
self.resize_grid();
}
self.needs_draw = true;
}
}
impl ShmHandler for App {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
impl SeatHandler for App {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
if self.data_device.is_none() {
self.data_device = Some(self.data_device_manager.get_data_device(qh, &seat));
}
if self.primary_device.is_none() {
self.primary_device = self
.primary_manager
.as_ref()
.map(|m| m.get_selection_device(qh, &seat));
}
if capability == Capability::Keyboard && self.keyboard.is_none() {
// get_keyboard_with_repeat drives key repeat off a calloop timer and
// delivers each repeat through the callback.
let loop_handle = self.loop_handle.clone();
let keyboard = self.seat_state.get_keyboard_with_repeat(
qh,
&seat,
None,
loop_handle,
Box::new(|app: &mut App, _kbd, event| app.handle_key(&event)),
);
match keyboard {
Ok(keyboard) => self.keyboard = Some(keyboard),
Err(err) => tracing::warn!("get keyboard: {err}"),
}
}
if capability == Capability::Pointer && self.pointer.is_none() {
match self.seat_state.get_pointer(qh, &seat) {
Ok(pointer) => {
self.cursor_shape_device = self
.cursor_shape_manager
.as_ref()
.map(|m| m.get_shape_device(&pointer, qh));
self.pointer = Some(pointer);
}
Err(err) => tracing::warn!("get pointer: {err}"),
}
}
}
fn remove_capability(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
capability: Capability,
) {
match capability {
Capability::Keyboard => {
if let Some(keyboard) = self.keyboard.take() {
keyboard.release();
}
}
Capability::Pointer => {
if let Some(pointer) = self.pointer.take() {
pointer.release();
}
}
_ => {}
}
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl KeyboardHandler for App {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: &wl_surface::WlSurface,
serial: u32,
_: &[u32],
_: &[Keysym],
) {
self.serial = serial;
self.focused = true;
self.report_focus(true);
self.needs_draw = true;
}
fn leave(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: &wl_surface::WlSurface,
_: u32,
) {
self.focused = false;
self.report_focus(false);
self.needs_draw = true;
}
fn press_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
serial: u32,
event: KeyEvent,
) {
self.serial = serial;
self.handle_key(&event);
}
fn repeat_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
_: KeyEvent,
) {
// Repeats are delivered through the get_keyboard_with_repeat callback;
// this non-calloop hook is unused.
}
fn release_key(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
_: KeyEvent,
) {
}
fn update_modifiers(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: u32,
modifiers: Modifiers,
_: RawModifiers,
_: u32,
) {
self.modifiers = modifiers;
}
fn update_repeat_info(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wl_keyboard::WlKeyboard,
_: RepeatInfo,
) {
}
}
/// Map a Wayland button code to the terminal mouse base code, if reportable.
fn button_code(button: u32) -> Option<u8> {
match button {
BTN_LEFT => Some(0),
BTN_MIDDLE => Some(1),
BTN_RIGHT => Some(2),
_ => None,
}
}
impl PointerHandler for App {
fn pointer_frame(
&mut self,
_: &Connection,
qh: &QueueHandle<Self>,
_: &wl_pointer::WlPointer,
events: &[PointerEvent],
) {
let cell_h = f64::from(self.renderer.metrics().height);
for event in events {
match &event.kind {
PointerEventKind::Enter { serial } => {
self.pointer_pos = event.position;
if let Some(device) = self.cursor_shape_device.as_ref() {
device.set_shape(*serial, Shape::Text);
}
self.pointer_drag();
}
PointerEventKind::Motion { .. } => {
self.pointer_pos = event.position;
if self.try_report_motion() {
continue;
}
self.pointer_drag();
}
PointerEventKind::Press {
button,
serial,
time,
..
} => {
self.serial = *serial;
self.pointer_pos = event.position;
if let Some(code) = button_code(*button)
&& self.try_report_button(code, true)
{
self.pressed_button = Some(code);
continue;
}
match *button {
BTN_LEFT => self.pointer_press(*time),
BTN_MIDDLE => self.paste_primary(),
_ => {}
}
}
PointerEventKind::Release { button, .. } => {
self.pointer_pos = event.position;
let code = button_code(*button);
if let Some(code) = code
&& self.try_report_button(code, false)
{
if self.pressed_button == Some(code) {
self.pressed_button = None;
}
continue;
}
if *button == BTN_LEFT {
self.pointer_release(qh);
}
}
PointerEventKind::Axis { vertical, .. } => {
// Wheel notches arrive as value120 (÷120) or legacy discrete
// steps, ~3 lines each; touchpads send pixels per cell height.
let (raw, scale) = if vertical.value120 != 0 {
(f64::from(vertical.value120) / 120.0, 3.0)
} else if vertical.discrete != 0 {
(f64::from(vertical.discrete), 3.0)
} else if cell_h > 0.0 {
(vertical.absolute / cell_h, 1.0)
} else {
continue;
};
if raw == 0.0 {
continue;
}
let lines = (raw.abs() * scale).ceil().max(1.0) as isize;
// Reporting apps get wheel buttons (64 up / 65 down) as
// presses, one per line, capped so a flick cannot flood.
if self.mouse_reporting() {
let code = if raw < 0.0 { 64 } else { 65 };
for _ in 0..lines.clamp(1, 8) {
self.try_report_button(code, true);
}
continue;
}
// Positive axis = scroll down (toward live); the viewport
// scrolls the opposite way (negative offset delta).
let delta = if raw < 0.0 { lines } else { -lines };
if let Some(session) = self.session.as_mut() {
session.term.scroll_view(delta);
self.needs_draw = true;
}
}
_ => {}
}
}
}
}
impl OutputHandler for App {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
}
impl ProvidesRegistryState for App {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
/// Serve the held clipboard text when a paste target requests it.
fn serve(text: &str, fd: WritePipe) {
let mut file = File::from(OwnedFd::from(fd));
let _ = file.write_all(text.as_bytes());
}
impl DataDeviceHandler for App {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataDevice,
_: f64,
_: f64,
_: &wl_surface::WlSurface,
) {
}
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
fn motion(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice, _: f64, _: f64) {}
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
fn drop_performed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
}
impl DataOfferHandler for App {
fn source_actions(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &mut DragOffer,
_: DndAction,
) {
}
fn selected_action(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &mut DragOffer,
_: DndAction,
) {
}
}
impl DataSourceHandler for App {
fn accept_mime(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
_: Option<String>,
) {
}
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &WlDataSource,
_mime: String,
fd: WritePipe,
) {
if self
.copy_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
serve(&self.clipboard, fd);
}
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, source: &WlDataSource) {
if self
.copy_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
self.copy_source = None;
}
}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: DndAction) {}
}
impl PrimarySelectionDeviceHandler for App {
fn selection(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &ZwpPrimarySelectionDeviceV1,
) {
}
}
impl PrimarySelectionSourceHandler for App {
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &ZwpPrimarySelectionSourceV1,
_mime: String,
fd: WritePipe,
) {
if self
.primary_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
serve(&self.primary_clip, fd);
}
}
fn cancelled(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
source: &ZwpPrimarySelectionSourceV1,
) {
if self
.primary_source
.as_ref()
.is_some_and(|s| s.inner() == source)
{
self.primary_source = None;
}
}
}
delegate_compositor!(App);
delegate_output!(App);
delegate_shm!(App);
delegate_seat!(App);
delegate_keyboard!(App);
delegate_pointer!(App);
delegate_xdg_shell!(App);
delegate_xdg_window!(App);
delegate_data_device!(App);
delegate_primary_selection!(App);
delegate_registry!(App);