| 33 |
33 |
|
use shop_pty::{Pty, PtySize};
|
| 34 |
34 |
|
use shop_render::{BgFill, CellMetrics, CellText, ImagePlacement, ImageRenderer, TextRenderer};
|
| 35 |
35 |
|
use shop_wayland::{
|
| 36 |
|
- |
Capability, CompositorHandler, CompositorState, Connection, OutputHandler, OutputState,
|
| 37 |
|
- |
Pending, ProvidesRegistryState, QueueHandle, RegistryState, SeatHandler, SeatState,
|
|
36 |
+ |
Capability, CompositorHandler, CompositorState, Connection, Layer, LayerConfig, LayerShell,
|
|
37 |
+ |
LayerShellHandler, LayerSurface, LayerSurfaceConfigure, OutputHandler, OutputState, Pending,
|
|
38 |
+ |
ProvidesRegistryState, QueueHandle, Region, RegistryState, SeatHandler, SeatState, ShopSurface,
|
| 38 |
39 |
|
WaylandChrome, WaylandSurface, WindowConfigure, WindowDecorations, WindowHandler, WindowSpec,
|
| 39 |
40 |
|
XdgShell, XdgWindow, registry_queue_init,
|
| 40 |
41 |
|
};
|
| 160 |
161 |
|
/// matches it, and a terminal that cannot say who it is makes that rule
|
| 161 |
162 |
|
/// unwritable: every shop window would be the same window to sway.
|
| 162 |
163 |
|
app_id: Option<String>,
|
|
164 |
+ |
/// `--layer background|bottom|top|overlay` opens on wlr-layer-shell instead
|
|
165 |
+ |
/// of as an ordinary window: sized to the output, anchored to all four
|
|
166 |
+ |
/// edges, and taking no input at all.
|
|
167 |
+ |
///
|
|
168 |
+ |
/// What Alloy's desktop background is. The surface never takes keyboard or
|
|
169 |
+ |
/// pointer focus, so managing the instance is somebody else's job; a
|
|
170 |
+ |
/// background that swallowed clicks across the whole output would be worse
|
|
171 |
+ |
/// than no background.
|
|
172 |
+ |
layer: Option<Layer>,
|
|
173 |
+ |
}
|
|
174 |
+ |
|
|
175 |
+ |
/// `--layer` values, spelled as the protocol names them.
|
|
176 |
+ |
fn parse_layer(value: &str) -> Result<Layer, CliExit> {
|
|
177 |
+ |
match value {
|
|
178 |
+ |
"background" => Ok(Layer::Background),
|
|
179 |
+ |
"bottom" => Ok(Layer::Bottom),
|
|
180 |
+ |
"top" => Ok(Layer::Top),
|
|
181 |
+ |
"overlay" => Ok(Layer::Overlay),
|
|
182 |
+ |
other => Err(CliExit::Reject(format!(
|
|
183 |
+ |
"unknown layer {other}\nExpected background, bottom, top or overlay."
|
|
184 |
+ |
))),
|
|
185 |
+ |
}
|
| 163 |
186 |
|
}
|
| 164 |
187 |
|
|
| 165 |
188 |
|
/// Every way argv can end the process before a window exists.
|
| 182 |
205 |
|
--exec CMD Run CMD through sh -c instead of the login shell.
|
| 183 |
206 |
|
--theme ID Use theme ID for this run, ignoring the config file.
|
| 184 |
207 |
|
--app-id ID Report ID as the window's app id, for window rules.
|
|
208 |
+ |
--layer LAYER Open on the wlr-layer-shell layer LAYER, one of
|
|
209 |
+ |
background, bottom, top or overlay, sized to the
|
|
210 |
+ |
output and taking no input. Needs a compositor with
|
|
211 |
+ |
wlr-layer-shell.
|
| 185 |
212 |
|
--record PATH Tee the terminal output to PATH as raw bytes.
|
| 186 |
213 |
|
-h, --help Print this help.
|
| 187 |
214 |
|
-V, --version Print the version.
|
| 255 |
282 |
|
"--theme" => cli.theme = Some(value("--theme")?),
|
| 256 |
283 |
|
"--record" => cli.record_path = Some(value("--record")?),
|
| 257 |
284 |
|
"--app-id" => cli.app_id = Some(value("--app-id")?),
|
|
285 |
+ |
"--layer" => cli.layer = Some(parse_layer(&value("--layer")?)?),
|
| 258 |
286 |
|
other => {
|
| 259 |
287 |
|
return Err(CliExit::Reject(format!(
|
| 260 |
288 |
|
"unknown option {other}\nTry 'shop --help'."
|
| 312 |
340 |
|
record_path,
|
| 313 |
341 |
|
theme: theme_arg,
|
| 314 |
342 |
|
app_id,
|
|
343 |
+ |
layer,
|
| 315 |
344 |
|
} = cli;
|
| 316 |
345 |
|
let config = Config::load().with_theme(theme_arg);
|
| 317 |
346 |
|
let scrollback_lines = config.scrollback_lines;
|
| 350 |
379 |
|
app_id: app_id.as_deref().unwrap_or("dev.makecreative.shop"),
|
| 351 |
380 |
|
min_size: (320, 240),
|
| 352 |
381 |
|
initial_size: INITIAL,
|
|
382 |
+ |
layer: layer.map(LayerConfig::background),
|
| 353 |
383 |
|
};
|
| 354 |
384 |
|
let (globals, event_queue) = registry_queue_init::<App>(&conn)?;
|
| 355 |
385 |
|
let qh = event_queue.handle();
|
| 356 |
386 |
|
let compositor = CompositorState::bind(&globals, &qh)?;
|
| 357 |
|
- |
let xdg_shell = XdgShell::bind(&globals, &qh)?;
|
| 358 |
387 |
|
let wl_surface = compositor.create_surface(&qh);
|
| 359 |
|
- |
let xdg_window = xdg_shell.create_window(wl_surface, WindowDecorations::ServerDefault, &qh);
|
| 360 |
|
- |
xdg_window.set_title(spec.title.to_string());
|
| 361 |
|
- |
xdg_window.set_app_id(spec.app_id.to_string());
|
| 362 |
|
- |
xdg_window.set_min_size(Some(spec.min_size));
|
| 363 |
|
- |
xdg_window.commit();
|
|
388 |
+ |
let (xdg_shell, layer_shell, surface) = match spec.layer {
|
|
389 |
+ |
None => {
|
|
390 |
+ |
let xdg_shell = XdgShell::bind(&globals, &qh)?;
|
|
391 |
+ |
let window = xdg_shell.create_window(wl_surface, WindowDecorations::ServerDefault, &qh);
|
|
392 |
+ |
window.set_title(spec.title.to_string());
|
|
393 |
+ |
window.set_app_id(spec.app_id.to_string());
|
|
394 |
+ |
window.set_min_size(Some(spec.min_size));
|
|
395 |
+ |
window.commit();
|
|
396 |
+ |
(Some(xdg_shell), None, ShopSurface::Toplevel(window))
|
|
397 |
+ |
}
|
|
398 |
+ |
Some(config) => {
|
|
399 |
+ |
// Named rather than passed through, because falling back to a
|
|
400 |
+ |
// toplevel would drop a background in the middle of the screen and
|
|
401 |
+ |
// look like shop, not the compositor, is broken. What to do
|
|
402 |
+ |
// instead is the caller's call.
|
|
403 |
+ |
let layer_shell = LayerShell::bind(&globals, &qh).map_err(|e| {
|
|
404 |
+ |
anyhow::anyhow!(
|
|
405 |
+ |
"--layer needs a compositor with wlr-layer-shell (zwlr_layer_shell_v1): {e}"
|
|
406 |
+ |
)
|
|
407 |
+ |
})?;
|
|
408 |
+ |
let layer = layer_shell.create_layer_surface(
|
|
409 |
+ |
&qh,
|
|
410 |
+ |
wl_surface,
|
|
411 |
+ |
config.layer,
|
|
412 |
+ |
Some(spec.app_id),
|
|
413 |
+ |
None,
|
|
414 |
+ |
);
|
|
415 |
+ |
// Zero size plus every anchor is how the protocol spells "the
|
|
416 |
+ |
// whole output"; the compositor answers with the real size in the
|
|
417 |
+ |
// first configure.
|
|
418 |
+ |
layer.set_size(0, 0);
|
|
419 |
+ |
layer.set_anchor(config.anchor);
|
|
420 |
+ |
layer.set_exclusive_zone(config.exclusive_zone);
|
|
421 |
+ |
layer.set_keyboard_interactivity(config.keyboard_interactivity);
|
|
422 |
+ |
// An empty input region, committed with the surface, so no pointer
|
|
423 |
+ |
// event ever reaches a surface that covers the whole output. The
|
|
424 |
+ |
// region is a one-shot: the compositor copies it at commit, so
|
|
425 |
+ |
// dropping it after is right.
|
|
426 |
+ |
let empty = if config.no_input {
|
|
427 |
+ |
let region = Region::new(&compositor)?;
|
|
428 |
+ |
layer.set_input_region(Some(region.wl_region()));
|
|
429 |
+ |
Some(region)
|
|
430 |
+ |
} else {
|
|
431 |
+ |
None
|
|
432 |
+ |
};
|
|
433 |
+ |
layer.commit();
|
|
434 |
+ |
drop(empty);
|
|
435 |
+ |
(None, Some(layer_shell), ShopSurface::Layer(layer))
|
|
436 |
+ |
}
|
|
437 |
+ |
};
|
| 364 |
438 |
|
let chrome = WaylandChrome {
|
| 365 |
439 |
|
registry: RegistryState::new(&globals),
|
| 366 |
440 |
|
seat: SeatState::new(&globals, &qh),
|
| 367 |
441 |
|
output: OutputState::new(&globals, &qh),
|
| 368 |
442 |
|
compositor,
|
| 369 |
443 |
|
xdg_shell,
|
| 370 |
|
- |
xdg_window,
|
|
444 |
+ |
layer_shell,
|
|
445 |
+ |
surface,
|
| 371 |
446 |
|
pending: VecDeque::new(),
|
| 372 |
447 |
|
width: spec.initial_size.0,
|
| 373 |
448 |
|
height: spec.initial_size.1,
|
| 566 |
641 |
|
}
|
| 567 |
642 |
|
}
|
| 568 |
643 |
|
if let Some(title) = app.grid.take_pending_title() {
|
| 569 |
|
- |
app.chrome.xdg_window.set_title(title);
|
|
644 |
+ |
app.chrome.surface.set_title(title);
|
| 570 |
645 |
|
}
|
| 571 |
646 |
|
// Answers the program is waiting on, from queries in
|
| 572 |
647 |
|
// the bytes just parsed. Written before the frame,
|
| 621 |
696 |
|
app.text.resize(&app.queue, physical_w, physical_h);
|
| 622 |
697 |
|
app.images.resize(&app.queue, physical_w, physical_h);
|
| 623 |
698 |
|
// Tell the compositor our buffer is N logical px per buffer px.
|
| 624 |
|
- |
app.chrome
|
| 625 |
|
- |
.xdg_window
|
| 626 |
|
- |
.wl_surface()
|
| 627 |
|
- |
.set_buffer_scale(s as i32);
|
|
699 |
+ |
app.chrome.surface.wl_surface().set_buffer_scale(s as i32);
|
| 628 |
700 |
|
let cols = grid_cols(logical_w, app.cell);
|
| 629 |
701 |
|
let rows = grid_rows(logical_h, app.cell);
|
| 630 |
702 |
|
app.grid.resize(cols, rows);
|
| 1258 |
1330 |
|
// Requested before presenting so it rides the commit `present` issues.
|
| 1259 |
1331 |
|
// Both go down the same connection in the order they were made, so this
|
| 1260 |
1332 |
|
// is the last point at which the request still lands on this frame.
|
| 1261 |
|
- |
app.chrome.xdg_window.wl_surface().frame(&app.qh, ());
|
|
1333 |
+ |
app.chrome.surface.wl_surface().frame(&app.qh, ());
|
| 1262 |
1334 |
|
app.queue.present(frame);
|
| 1263 |
1335 |
|
Ok(())
|
| 1264 |
1336 |
|
}
|
| 1742 |
1814 |
|
// A drag that ends outside the window still delivers its release
|
| 1743 |
1815 |
|
// to us, but anything addressed to another surface is not ours to
|
| 1744 |
1816 |
|
// act on.
|
| 1745 |
|
- |
if &event.surface != self.chrome.xdg_window.wl_surface() {
|
|
1817 |
+ |
if &event.surface != self.chrome.surface.wl_surface() {
|
| 1746 |
1818 |
|
continue;
|
| 1747 |
1819 |
|
}
|
| 1748 |
1820 |
|
self.pointer_at = event.position;
|
| 2371 |
2443 |
|
}
|
| 2372 |
2444 |
|
}
|
| 2373 |
2445 |
|
|
|
2446 |
+ |
impl LayerShellHandler for App {
|
|
2447 |
+ |
fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
|
|
2448 |
+ |
self.chrome.exit = true;
|
|
2449 |
+ |
self.chrome.pending.push_back(Pending::CloseRequested);
|
|
2450 |
+ |
}
|
|
2451 |
+ |
|
|
2452 |
+ |
fn configure(
|
|
2453 |
+ |
&mut self,
|
|
2454 |
+ |
_: &Connection,
|
|
2455 |
+ |
_: &QueueHandle<Self>,
|
|
2456 |
+ |
_: &LayerSurface,
|
|
2457 |
+ |
configure: LayerSurfaceConfigure,
|
|
2458 |
+ |
_serial: u32,
|
|
2459 |
+ |
) {
|
|
2460 |
+ |
// Zero on an axis means the compositor left the choice to us, so keep
|
|
2461 |
+ |
// what we already have rather than collapsing the surface.
|
|
2462 |
+ |
let (new_w, new_h) = configure.new_size;
|
|
2463 |
+ |
let width = if new_w == 0 { self.chrome.width } else { new_w };
|
|
2464 |
+ |
let height = if new_h == 0 {
|
|
2465 |
+ |
self.chrome.height
|
|
2466 |
+ |
} else {
|
|
2467 |
+ |
new_h
|
|
2468 |
+ |
};
|
|
2469 |
+ |
self.chrome.width = width;
|
|
2470 |
+ |
self.chrome.height = height;
|
|
2471 |
+ |
self.chrome
|
|
2472 |
+ |
.pending
|
|
2473 |
+ |
.push_back(Pending::Resized { width, height });
|
|
2474 |
+ |
}
|
|
2475 |
+ |
}
|
|
2476 |
+ |
|
| 2374 |
2477 |
|
impl ProvidesRegistryState for App {
|
| 2375 |
2478 |
|
fn registry(&mut self) -> &mut RegistryState {
|
| 2376 |
2479 |
|
&mut self.chrome.registry
|
| 2602 |
2705 |
|
}
|
| 2603 |
2706 |
|
}
|
| 2604 |
2707 |
|
|
|
2708 |
+ |
#[test]
|
|
2709 |
+ |
fn the_layer_flag_takes_the_four_protocol_names() {
|
|
2710 |
+ |
for (name, want) in [
|
|
2711 |
+ |
("background", Layer::Background),
|
|
2712 |
+ |
("bottom", Layer::Bottom),
|
|
2713 |
+ |
("top", Layer::Top),
|
|
2714 |
+ |
("overlay", Layer::Overlay),
|
|
2715 |
+ |
] {
|
|
2716 |
+ |
let cli = parse_args(cmdline(&["--layer", name])).expect("a layer we have");
|
|
2717 |
+ |
assert_eq!(cli.layer, Some(want));
|
|
2718 |
+ |
}
|
|
2719 |
+ |
assert_eq!(
|
|
2720 |
+ |
parse_args(cmdline(&["--layer=overlay"]))
|
|
2721 |
+ |
.expect("the equals spelling too")
|
|
2722 |
+ |
.layer,
|
|
2723 |
+ |
Some(Layer::Overlay)
|
|
2724 |
+ |
);
|
|
2725 |
+ |
}
|
|
2726 |
+ |
|
|
2727 |
+ |
#[test]
|
|
2728 |
+ |
fn an_unknown_layer_is_refused_by_name() {
|
|
2729 |
+ |
let Err(CliExit::Reject(message)) = parse_args(cmdline(&["--layer", "wallpaper"])) else {
|
|
2730 |
+ |
panic!("an unknown layer must not open a window");
|
|
2731 |
+ |
};
|
|
2732 |
+ |
assert!(message.contains("wallpaper"), "{message}");
|
|
2733 |
+ |
}
|
|
2734 |
+ |
|
|
2735 |
+ |
#[test]
|
|
2736 |
+ |
fn nothing_opens_on_a_layer_by_default() {
|
|
2737 |
+ |
assert_eq!(parse_args(cmdline(&[])).expect("no args").layer, None);
|
|
2738 |
+ |
}
|
|
2739 |
+ |
|
| 2605 |
2740 |
|
#[test]
|
| 2606 |
2741 |
|
fn an_unknown_option_is_refused_by_name() {
|
| 2607 |
2742 |
|
let Err(CliExit::Reject(message)) = parse_args(cmdline(&["--colour", "red"])) else {
|