alt-tab
This commit is contained in:
+43
-2
@@ -37,12 +37,17 @@ use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1::WpCursorShapeDeviceV1,
|
||||
wp_cursor_shape_manager_v1::WpCursorShapeManagerV1,
|
||||
};
|
||||
use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::client::{
|
||||
zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1,
|
||||
zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1,
|
||||
};
|
||||
use wayland_protocols::wp::viewporter::client::{
|
||||
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
|
||||
};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1;
|
||||
|
||||
use crate::capture::{Live, Tile};
|
||||
use crate::config::AltTabMode;
|
||||
use crate::overlay;
|
||||
use crate::shm;
|
||||
use crate::target::Target;
|
||||
@@ -60,6 +65,7 @@ pub struct Settings {
|
||||
/// the overlay maps there rather than wherever the compositor would put it.
|
||||
pub scale: i32,
|
||||
pub output: String,
|
||||
pub alt_tab: AltTabMode,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -113,6 +119,14 @@ pub struct App {
|
||||
pub(crate) focused: bool,
|
||||
pub(crate) picked: Option<Target>,
|
||||
pub(crate) stats: Stats,
|
||||
|
||||
pub(crate) seat: Option<WlSeat>,
|
||||
pub(crate) inhibit_mgr: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
|
||||
pub(crate) inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
||||
pub(crate) alt_tab: AltTabMode,
|
||||
pub(crate) is_alt_tab: bool,
|
||||
pub(crate) latched_modifiers: std::collections::BTreeSet<u32>,
|
||||
pub(crate) initial_stepped: bool,
|
||||
}
|
||||
|
||||
/// Counters worth reporting with --verbose. Live capture is easy to get subtly
|
||||
@@ -166,7 +180,15 @@ impl App {
|
||||
fps,
|
||||
scale,
|
||||
output,
|
||||
alt_tab,
|
||||
} = settings;
|
||||
let focused_idx = targets.iter().position(|t| t.focused).unwrap_or(0);
|
||||
let sel = if alt_tab == AltTabMode::Yes && targets.len() > 1 {
|
||||
(focused_idx + 1) % targets.len()
|
||||
} else {
|
||||
focused_idx
|
||||
};
|
||||
let is_alt_tab = alt_tab == AltTabMode::Yes;
|
||||
// Bind everything up front so a compositor missing a protocol fails
|
||||
// here, with a name, rather than halfway through a capture.
|
||||
let mut app = Self {
|
||||
@@ -187,7 +209,7 @@ impl App {
|
||||
live,
|
||||
fps,
|
||||
scale,
|
||||
sel: 0,
|
||||
sel,
|
||||
scroll: 0,
|
||||
shift: false,
|
||||
hover: None,
|
||||
@@ -204,6 +226,13 @@ impl App {
|
||||
focused: false,
|
||||
picked: None,
|
||||
stats: Stats::default(),
|
||||
seat: None,
|
||||
inhibit_mgr: None,
|
||||
inhibitor: None,
|
||||
alt_tab,
|
||||
is_alt_tab,
|
||||
latched_modifiers: std::collections::BTreeSet::new(),
|
||||
initial_stepped: alt_tab == AltTabMode::Yes,
|
||||
};
|
||||
let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?;
|
||||
// One wl_output per display, bound at v4 so it tells us its name.
|
||||
@@ -216,7 +245,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
let _: WlSeat = globals.bind(qh, 1..=7, ())?;
|
||||
let seat: WlSeat = globals.bind(qh, 1..=7, ())?;
|
||||
app.seat = Some(seat);
|
||||
app.inhibit_mgr = globals.bind(qh, 1..=1, ()).ok();
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
@@ -387,3 +418,13 @@ delegate_noop!(App: ignore WlSurface);
|
||||
delegate_noop!(App: WpCursorShapeManagerV1);
|
||||
delegate_noop!(App: WpCursorShapeDeviceV1);
|
||||
delegate_noop!(App: ignore WlBuffer);
|
||||
delegate_noop!(App: ZwpKeyboardShortcutsInhibitManagerV1);
|
||||
delegate_noop!(App: ignore ZwpKeyboardShortcutsInhibitorV1);
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
if let Some(inhibitor) = self.inhibitor.take() {
|
||||
inhibitor.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
-8
@@ -5,8 +5,8 @@ use std::time::Duration;
|
||||
|
||||
use crate::app::Settings;
|
||||
use crate::capture::Live;
|
||||
use crate::config::{Config, Length};
|
||||
use crate::sway::Display;
|
||||
use crate::config::{AltTabMode, Config, Length};
|
||||
use crate::sway::{Display, Order};
|
||||
use crate::target::Format;
|
||||
use crate::theme::Theme;
|
||||
|
||||
@@ -20,8 +20,11 @@ usage: wl-pick [options]
|
||||
--live all|current|none which tiles keep updating live [all]
|
||||
(displays are always a single snapshot)
|
||||
--fps N cap on live updates per tile per second [12]
|
||||
--outputs, --no-outputs include whole displays as tiles [yes]
|
||||
--outputs, --no-outputs include whole displays as tiles [no]
|
||||
--labels, --no-labels a label under each thumbnail [yes]
|
||||
--order mru|tree window ordering: mru or layout tree [mru]
|
||||
--alt-tab, --no-alt-tab alt-tab switcher mode (commit on release) [auto]
|
||||
--focus, --no-focus focus the picked target in sway directly [no]
|
||||
--font FAMILY label font family [the system monospace font]
|
||||
--font-size PX label size in logical px [13.3]
|
||||
--timeout SECS exit anyway after SECS, in case the keyboard
|
||||
@@ -30,7 +33,8 @@ usage: wl-pick [options]
|
||||
-h, --help this
|
||||
|
||||
keys: arrows, hjkl or Tab/Shift+Tab move; PgUp/PgDn and Home/End jump;
|
||||
Enter picks; Escape or q cancels
|
||||
Enter picks; Escape or q cancels; in Alt+Tab mode, releasing
|
||||
the modifier (Alt/Super) picks the selection.
|
||||
mouse: click a tile to pick it, scroll to move. Hovering does not move the
|
||||
selection, and a click outside a tile does nothing.
|
||||
|
||||
@@ -60,7 +64,10 @@ config:
|
||||
font = monospace # also --font
|
||||
font-size = 13.3
|
||||
labels = yes
|
||||
outputs = yes # include whole displays as tiles
|
||||
outputs = no # include whole displays as tiles
|
||||
order = mru # mru or tree
|
||||
alt-tab = auto # auto, yes or no
|
||||
focus = no # focus picked target in sway directly
|
||||
live = all
|
||||
fps = 12
|
||||
format = tsv
|
||||
@@ -89,6 +96,8 @@ formats:
|
||||
|
||||
focusing on sway:
|
||||
|
||||
wl-pick --focus or:
|
||||
|
||||
IFS=$'\\t' read -r type id toplevel app title < <(wl-pick) &&
|
||||
case $type in
|
||||
window) swaymsg \"[con_id=$id] focus\" ;;
|
||||
@@ -110,6 +119,9 @@ pub struct Args {
|
||||
pub(crate) live: Option<Live>,
|
||||
pub(crate) fps: Option<u32>,
|
||||
pub(crate) timeout: Option<Duration>,
|
||||
pub(crate) order: Option<Order>,
|
||||
pub(crate) alt_tab: Option<AltTabMode>,
|
||||
pub(crate) focus: Option<bool>,
|
||||
}
|
||||
|
||||
/// Every setting resolved, with sizes turned into pixels for the display the
|
||||
@@ -119,6 +131,8 @@ pub struct Options {
|
||||
pub format: Format,
|
||||
pub outputs: bool,
|
||||
pub timeout: Option<Duration>,
|
||||
pub order: Order,
|
||||
pub focus: bool,
|
||||
/// The logical size of the display the grid will be laid out for.
|
||||
pub display: (i32, i32),
|
||||
pub settings: Settings,
|
||||
@@ -183,8 +197,10 @@ impl Args {
|
||||
Options {
|
||||
verbose: self.verbose,
|
||||
format: self.format.or(cfg.format).unwrap_or(Format::Tsv),
|
||||
outputs: self.outputs.or(cfg.outputs).unwrap_or(true),
|
||||
outputs: self.outputs.or(cfg.outputs).unwrap_or(false),
|
||||
timeout: self.timeout.or(cfg.timeout),
|
||||
order: self.order.or(cfg.order).unwrap_or(Order::Mru),
|
||||
focus: self.focus.or(cfg.focus).unwrap_or(false),
|
||||
display: (display.width, display.height),
|
||||
settings: Settings {
|
||||
theme,
|
||||
@@ -192,6 +208,7 @@ impl Args {
|
||||
fps: self.fps.or(cfg.fps).unwrap_or(12),
|
||||
scale: display.scale,
|
||||
output: display.name.clone(),
|
||||
alt_tab: self.alt_tab.or(cfg.alt_tab).unwrap_or(AltTabMode::Auto),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -212,6 +229,14 @@ fn parse(it: impl Iterator<Item = String>) -> Result<Args, String> {
|
||||
}
|
||||
"--outputs" => args.outputs = Some(true),
|
||||
"--no-outputs" => args.outputs = Some(false),
|
||||
"--order" => {
|
||||
let v = it.next().ok_or("--order needs mru|tree")?;
|
||||
args.order = Some(Order::parse(&v)?);
|
||||
}
|
||||
"--alt-tab" => args.alt_tab = Some(AltTabMode::Yes),
|
||||
"--no-alt-tab" => args.alt_tab = Some(AltTabMode::No),
|
||||
"--focus" => args.focus = Some(true),
|
||||
"--no-focus" => args.focus = Some(false),
|
||||
"--config" => {
|
||||
args.config = Some(PathBuf::from(it.next().ok_or("--config needs a path")?))
|
||||
}
|
||||
@@ -282,6 +307,8 @@ mod tests {
|
||||
assert_eq!(args(&["--labels"]).labels, Some(true));
|
||||
assert_eq!(args(&["--no-labels"]).labels, Some(false));
|
||||
assert_eq!(args(&["--hide-labels"]).labels, Some(false), "old spelling");
|
||||
assert_eq!(args(&["--alt-tab"]).alt_tab, Some(AltTabMode::Yes));
|
||||
assert_eq!(args(&["--no-alt-tab"]).alt_tab, Some(AltTabMode::No));
|
||||
// Unset is what lets the file have its say.
|
||||
assert_eq!(args(&[]).outputs, None);
|
||||
assert_eq!(args(&[]).labels, None);
|
||||
@@ -300,8 +327,14 @@ mod tests {
|
||||
assert!(!off.outputs);
|
||||
assert!(!off.settings.theme.labels);
|
||||
|
||||
// With no flag the file decides, and with no file either, the default.
|
||||
// With no flag the file decides, and with no file either, the default (outputs: false).
|
||||
assert!(!args(&[]).resolve(&file(false), &display()).outputs);
|
||||
assert!(args(&[]).resolve(&Config::default(), &display()).outputs);
|
||||
assert!(!args(&[]).resolve(&Config::default(), &display()).outputs);
|
||||
assert!(args(&["--outputs"]).resolve(&Config::default(), &display()).outputs);
|
||||
assert_eq!(args(&[]).resolve(&Config::default(), &display()).order, Order::Mru);
|
||||
assert_eq!(
|
||||
args(&["--order", "tree"]).resolve(&Config::default(), &display()).order,
|
||||
Order::Tree
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,29 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::capture::Live;
|
||||
use crate::sway::Order;
|
||||
use crate::target::Format;
|
||||
use crate::theme::Argb;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum AltTabMode {
|
||||
#[default]
|
||||
Auto,
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl AltTabMode {
|
||||
pub fn parse(s: &str) -> Result<Self, String> {
|
||||
match s.trim() {
|
||||
"auto" => Ok(AltTabMode::Auto),
|
||||
"yes" | "true" | "on" | "1" => Ok(AltTabMode::Yes),
|
||||
"no" | "false" | "off" | "0" => Ok(AltTabMode::No),
|
||||
other => Err(format!("{other:?} is not auto, yes or no")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A size, either absolute or relative to the display it will be shown on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Length {
|
||||
@@ -98,6 +118,9 @@ pub struct Config {
|
||||
pub fps: Option<u32>,
|
||||
pub format: Option<Format>,
|
||||
pub timeout: Option<Duration>,
|
||||
pub order: Option<Order>,
|
||||
pub alt_tab: Option<AltTabMode>,
|
||||
pub focus: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -157,6 +180,9 @@ impl Config {
|
||||
let secs: f64 = number(value)?;
|
||||
self.timeout = (secs > 0.0).then(|| Duration::from_secs_f64(secs));
|
||||
}
|
||||
"order" => self.order = Some(Order::parse(value)?),
|
||||
"alt-tab" => self.alt_tab = Some(AltTabMode::parse(value)?),
|
||||
"focus" => self.focus = Some(boolean(value)?),
|
||||
other => return Err(format!("unknown setting {other:?}")),
|
||||
}
|
||||
Ok(())
|
||||
@@ -242,6 +268,8 @@ live = current
|
||||
fps = 30
|
||||
labels = no
|
||||
timeout = 0
|
||||
order = mru
|
||||
alt-tab = yes
|
||||
",
|
||||
)
|
||||
.expect("should parse");
|
||||
@@ -254,6 +282,8 @@ timeout = 0
|
||||
assert_eq!(cfg.fps, Some(30));
|
||||
assert_eq!(cfg.labels, Some(false));
|
||||
assert_eq!(cfg.timeout, None, "zero means no timeout");
|
||||
assert_eq!(cfg.order, Some(Order::Mru));
|
||||
assert_eq!(cfg.alt_tab, Some(AltTabMode::Yes));
|
||||
assert!(cfg.live.is_some());
|
||||
// Untouched settings stay unset, so defaults survive.
|
||||
assert_eq!(cfg.foreground, None);
|
||||
@@ -294,4 +324,16 @@ timeout = 0
|
||||
unsafe { std::env::set_var("XDG_CONFIG_HOME", "/nonexistent") };
|
||||
assert!(Config::load(None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_tab_mode_parses() {
|
||||
assert_eq!(AltTabMode::parse("auto"), Ok(AltTabMode::Auto));
|
||||
assert_eq!(AltTabMode::parse("yes"), Ok(AltTabMode::Yes));
|
||||
assert_eq!(AltTabMode::parse("true"), Ok(AltTabMode::Yes));
|
||||
assert_eq!(AltTabMode::parse("1"), Ok(AltTabMode::Yes));
|
||||
assert_eq!(AltTabMode::parse("no"), Ok(AltTabMode::No));
|
||||
assert_eq!(AltTabMode::parse("false"), Ok(AltTabMode::No));
|
||||
assert_eq!(AltTabMode::parse("0"), Ok(AltTabMode::No));
|
||||
assert!(AltTabMode::parse("maybe").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -82,7 +82,7 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
let displays = sway::displays(&mut sway)?;
|
||||
let display = sway::focused(&displays).ok_or("sway reports no active display")?;
|
||||
let opts = args.resolve(&config, display);
|
||||
let mut targets = sway::windows(&mut sway)?;
|
||||
let mut targets = sway::windows(&mut sway, opts.order)?;
|
||||
if opts.outputs {
|
||||
// Displays go last, after the windows, so window positions are
|
||||
// stable as windows come and go.
|
||||
@@ -198,6 +198,20 @@ fn run() -> Result<ExitCode, Box<dyn Error>> {
|
||||
let Some(target) = app.picked() else {
|
||||
return Ok(ExitCode::FAILURE); // cancelled: nothing on stdout
|
||||
};
|
||||
if opts.focus {
|
||||
if let Ok(mut sway) = sway::connect() {
|
||||
match target.kind {
|
||||
target::Kind::Window => {
|
||||
if let Some(con_id) = target.con_id {
|
||||
let _ = sway.run_command(format!("[con_id={con_id}] focus"));
|
||||
}
|
||||
}
|
||||
target::Kind::Output => {
|
||||
let _ = sway.run_command(format!("focus output {}", target.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match target.render(opts.format) {
|
||||
Some(line) => println!("{line}"),
|
||||
// Only the portal format can fail to name something: it identifies a
|
||||
|
||||
+85
-8
@@ -24,6 +24,7 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
|
||||
use crate::app::{App, Ending};
|
||||
use crate::config::AltTabMode;
|
||||
use crate::shm;
|
||||
use crate::theme::{Rect, fit_centred};
|
||||
|
||||
@@ -32,23 +33,41 @@ use crate::theme::{Rect, fit_centred};
|
||||
const KEY_ESC: u32 = 1;
|
||||
const KEY_TAB: u32 = 15;
|
||||
const KEY_Q: u32 = 16;
|
||||
const KEY_ENTER: u32 = 28;
|
||||
const KEY_LEFTCTRL: u32 = 29;
|
||||
// hjkl, by physical position: the same keys as vim on a qwerty layout.
|
||||
const KEY_H: u32 = 35;
|
||||
const KEY_J: u32 = 36;
|
||||
const KEY_K: u32 = 37;
|
||||
const KEY_L: u32 = 38;
|
||||
const KEY_ENTER: u32 = 28;
|
||||
const KEY_LEFTSHIFT: u32 = 42;
|
||||
const KEY_RIGHTSHIFT: u32 = 54;
|
||||
const KEY_LEFTALT: u32 = 56;
|
||||
const KEY_KPENTER: u32 = 96;
|
||||
const KEY_RIGHTCTRL: u32 = 97;
|
||||
const KEY_RIGHTALT: u32 = 100;
|
||||
const KEY_HOME: u32 = 102;
|
||||
const KEY_UP: u32 = 103;
|
||||
const KEY_PGUP: u32 = 104;
|
||||
const KEY_LEFT: u32 = 105;
|
||||
const KEY_RIGHT: u32 = 106;
|
||||
const KEY_END: u32 = 107;
|
||||
const KEY_DOWN: u32 = 108;
|
||||
const KEY_PGUP: u32 = 104;
|
||||
const KEY_PGDN: u32 = 109;
|
||||
const KEY_LEFTMETA: u32 = 125;
|
||||
const KEY_RIGHTMETA: u32 = 126;
|
||||
|
||||
fn is_trigger_modifier(code: u32) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
KEY_LEFTALT
|
||||
| KEY_RIGHTALT
|
||||
| KEY_LEFTMETA
|
||||
| KEY_RIGHTMETA
|
||||
| KEY_LEFTCTRL
|
||||
| KEY_RIGHTCTRL
|
||||
)
|
||||
}
|
||||
|
||||
/// evdev button code, as wl_pointer reports it.
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
@@ -105,6 +124,9 @@ impl App {
|
||||
}
|
||||
pool.destroy();
|
||||
self.chrome = Some(shm::Chrome::new(&file, pw, ph)?);
|
||||
if let (Some(mgr), Some(seat)) = (&self.inhibit_mgr, &self.seat) {
|
||||
self.inhibitor = Some(mgr.inhibit_shortcuts(&surface, seat, qh, ()));
|
||||
}
|
||||
self.surface = Some(surface);
|
||||
Ok(())
|
||||
}
|
||||
@@ -296,6 +318,9 @@ impl App {
|
||||
}
|
||||
|
||||
fn key(&mut self, code: u32, qh: &QueueHandle<Self>) {
|
||||
if self.is_alt_tab && is_trigger_modifier(code) {
|
||||
self.latched_modifiers.insert(code);
|
||||
}
|
||||
match code {
|
||||
KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true,
|
||||
KEY_ESC | KEY_Q => self.ending = Ending::Cancelled,
|
||||
@@ -315,6 +340,62 @@ impl App {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_up(&mut self, code: u32) {
|
||||
if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT {
|
||||
self.shift = false;
|
||||
}
|
||||
if self.is_alt_tab
|
||||
&& self.latched_modifiers.remove(&code)
|
||||
&& self.latched_modifiers.is_empty()
|
||||
{
|
||||
if self.ending == Ending::Running {
|
||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
||||
self.ending = Ending::Picked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_enter(&mut self, keys: Vec<u8>, qh: &QueueHandle<Self>) {
|
||||
self.focused = true;
|
||||
let held_keys: Vec<u32> = keys
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
|
||||
.collect();
|
||||
|
||||
if held_keys.iter().any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT) {
|
||||
self.shift = true;
|
||||
}
|
||||
|
||||
let held_modifiers: Vec<u32> = held_keys
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&k| is_trigger_modifier(k))
|
||||
.collect();
|
||||
|
||||
if !held_modifiers.is_empty() && self.alt_tab != AltTabMode::No {
|
||||
self.is_alt_tab = true;
|
||||
for &m in &held_modifiers {
|
||||
self.latched_modifiers.insert(m);
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_alt_tab && !self.initial_stepped {
|
||||
self.initial_stepped = true;
|
||||
if self.alt_tab == AltTabMode::Yes && self.latched_modifiers.is_empty() {
|
||||
// In explicit alt-tab mode, if no modifier was held on enter,
|
||||
// the modifier was released before focus was acquired: commit immediately!
|
||||
if self.ending == Ending::Running {
|
||||
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
||||
self.ending = Ending::Picked;
|
||||
}
|
||||
} else if self.alt_tab == AltTabMode::Auto {
|
||||
// In auto mode, step selection now that we know a modifier was held:
|
||||
let step = if self.shift { -1 } else { 1 };
|
||||
self.move_sel(step, qh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- event plumbing -------------------------------------------------------
|
||||
@@ -379,18 +460,14 @@ impl Dispatch<WlKeyboard, ()> for App {
|
||||
match event {
|
||||
wl_keyboard::Event::Key { key, state, .. } => match state {
|
||||
WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key, qh),
|
||||
WEnum::Value(wl_keyboard::KeyState::Released)
|
||||
if key == KEY_LEFTSHIFT || key == KEY_RIGHTSHIFT =>
|
||||
{
|
||||
app.shift = false
|
||||
}
|
||||
WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key),
|
||||
_ => {}
|
||||
},
|
||||
// Focus is only tracked here. sway sends leave immediately
|
||||
// followed by enter on the same surface when the pointer crosses
|
||||
// it, so whether the grab is really gone is decided by the main
|
||||
// loop, once the event batch has been dispatched.
|
||||
wl_keyboard::Event::Enter { .. } => app.focused = true,
|
||||
wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh),
|
||||
wl_keyboard::Event::Leave { .. } => app.focused = false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+72
-10
@@ -99,32 +99,87 @@ fn live_sockets() -> Vec<PathBuf> {
|
||||
found
|
||||
}
|
||||
|
||||
/// Every view in the tree, in tree order (the same traversal the jq filter did,
|
||||
/// so the grid keeps the ordering the muscle memory expects).
|
||||
pub fn windows(conn: &mut Connection) -> Result<Vec<Target>, swayipc::Error> {
|
||||
/// How to order windows in the grid.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum Order {
|
||||
/// Most-Recently-Used (MRU) focus order: current window first, then previous, etc.
|
||||
#[default]
|
||||
Mru,
|
||||
/// Traversal of sway's layout tree (workspace by workspace).
|
||||
Tree,
|
||||
}
|
||||
|
||||
impl Order {
|
||||
pub fn parse(s: &str) -> Result<Self, String> {
|
||||
match s.trim() {
|
||||
"mru" => Ok(Order::Mru),
|
||||
"tree" => Ok(Order::Tree),
|
||||
other => Err(format!("{other:?} is not mru or tree")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Views in the tree, either in MRU focus order or tree layout order.
|
||||
pub fn windows(conn: &mut Connection, order: Order) -> Result<Vec<Target>, swayipc::Error> {
|
||||
let mut out = Vec::new();
|
||||
collect(&conn.get_tree()?, &mut out);
|
||||
let tree = conn.get_tree()?;
|
||||
match order {
|
||||
Order::Mru => {
|
||||
collect_mru(&tree, &mut out);
|
||||
// Ensure the currently focused window is at index 0
|
||||
if let Some(pos) = out.iter().position(|t| t.focused) {
|
||||
if pos > 0 {
|
||||
let focused = out.remove(pos);
|
||||
out.insert(0, focused);
|
||||
}
|
||||
}
|
||||
}
|
||||
Order::Tree => collect_tree(&tree, &mut out),
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn collect(node: &Node, out: &mut Vec<Target>) {
|
||||
fn collect_target(node: &Node) -> Option<Target> {
|
||||
let is_con = matches!(node.node_type, NodeType::Con | NodeType::FloatingCon);
|
||||
let class = node
|
||||
.window_properties
|
||||
.as_ref()
|
||||
.and_then(|p| p.class.clone());
|
||||
if is_con && (node.app_id.is_some() || class.is_some()) {
|
||||
// A view with no identifier can't be captured, but it still belongs in
|
||||
// the list: it gets a tile with no thumbnail.
|
||||
out.push(Target::window(
|
||||
Some(Target::window(
|
||||
node.id,
|
||||
node.foreign_toplevel_identifier.clone().unwrap_or_default(),
|
||||
node.app_id.clone().or(class).unwrap_or_default(),
|
||||
node.name.clone().unwrap_or_default(),
|
||||
));
|
||||
node.focused,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_tree(node: &Node, out: &mut Vec<Target>) {
|
||||
if let Some(target) = collect_target(node) {
|
||||
out.push(target);
|
||||
}
|
||||
for child in node.nodes.iter().chain(node.floating_nodes.iter()) {
|
||||
collect(child, out);
|
||||
collect_tree(child, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_mru(node: &Node, out: &mut Vec<Target>) {
|
||||
if let Some(target) = collect_target(node) {
|
||||
out.push(target);
|
||||
}
|
||||
let mut children: Vec<&Node> = node.nodes.iter().chain(node.floating_nodes.iter()).collect();
|
||||
children.sort_by_key(|child| {
|
||||
node.focus
|
||||
.iter()
|
||||
.position(|&id| id == child.id)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
for child in children {
|
||||
collect_mru(child, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,4 +239,11 @@ mod tests {
|
||||
assert_eq!(socket_pid("sway-ipc.1000..sock"), None);
|
||||
assert_eq!(socket_pid("sway-ipc.1000.notapid.sock"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_parses() {
|
||||
assert_eq!(super::Order::parse("mru"), Ok(super::Order::Mru));
|
||||
assert_eq!(super::Order::parse("tree"), Ok(super::Order::Tree));
|
||||
assert!(super::Order::parse("invalid").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -55,10 +55,12 @@ pub struct Target {
|
||||
pub ft_id: String,
|
||||
pub app: String,
|
||||
pub title: String,
|
||||
/// Whether this window was the focused container when sway was queried.
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn window(con_id: i64, ft_id: String, app: String, title: String) -> Self {
|
||||
pub fn window(con_id: i64, ft_id: String, app: String, title: String, focused: bool) -> Self {
|
||||
Self {
|
||||
kind: Kind::Window,
|
||||
id: con_id.to_string(),
|
||||
@@ -66,6 +68,7 @@ impl Target {
|
||||
ft_id,
|
||||
app,
|
||||
title,
|
||||
focused,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +82,7 @@ impl Target {
|
||||
ft_id: String::new(),
|
||||
app: "display".to_string(),
|
||||
title: name,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn win() -> Target {
|
||||
Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into())
|
||||
Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into(), false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -228,7 +232,7 @@ mod tests {
|
||||
Some("Monitor: DP-1")
|
||||
);
|
||||
// No identifier means the portal cannot be told about this window.
|
||||
let anon = Target::window(7, String::new(), "x".into(), "y".into());
|
||||
let anon = Target::window(7, String::new(), "x".into(), "y".into(), false);
|
||||
assert_eq!(anon.portal(), None);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user